AlpsPoolCreator
0xbd2dfe4bfe5b46110b590d8b8462fab0dfc7a42c
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
7,584 bytes
ABI entries
21
7 read · 2 write
License
none
Contract information
- Address
- 0xbd2dfe4bfe5b46110b590d8b8462fab0dfc7a42c
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x5EcC02c691…75386ad57d
- Creation tx
- 0xf0249666b5…1e50a4becc
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (7)
BPS() → uint256
PLATFORM_FEE_BPS() → uint256
feeSink() → address
npm() → address
permit2() → address
poolManager() → address
posm() → address
Events (2)
PoolSeededV3PoolSeededV4
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "npm_",
"type": "address"
},
{
"internalType": "address",
"name": "posm_",
"type": "address"
},
{
"internalType": "address",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "address",
"name": "permit2_",
"type": "address"
},
{
"internalType": "address",
"name": "feeSink_",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "BadParams",
"type": "error"
},
{
"inputs": [],
"name": "HooksNotAllowed",
"type": "error"
},
{
"inputs": [],
"name": "NativeMismatch",
"type": "error"
},
{
"inputs": [],
"name": "PriceMismatch",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "SlippageOrDust",
"type": "error"
},
{
"inputs": [],
"name": "T",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "token1",
"type": "address"
},
{
"indexed": false,
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"name": "PoolSeededV3",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "currency0",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "currency1",
"type": "address"
},
{
"indexed": false,
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"name": "PoolSeededV4",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "PLATFORM_FEE_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
"name": "token1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount0Min",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1Min",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"internalType": "struct AlpsPoolCreator.SeedV3Params",
"name": "p",
"type": "tuple"
}
],
"name": "createAndSeedV3",
"outputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"components": [
{
"internalType": "address",
"name": "currency0",
"type": "address"
},
{
"internalType": "address",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "address",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct V4PoolKey",
"name": "poolKey",
"type": "tuple"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount0Min",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1Min",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"internalType": "struct AlpsPoolCreator.SeedV4Params",
"name": "p",
"type": "tuple"
}
],
"name": "createAndSeedV4",
"outputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "feeSink",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "npm",
"outputs": [
{
"internalType": "contract INPMCreate",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "permit2",
"outputs": [
{
"internalType": "contract IPermit2",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "contract IV4PoolManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "posm",
"outputs": [
{
"internalType": "contract IV4PositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20 as OZIERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol";
import {LiquidityAmounts} from "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol";
import {V4PoolKey, IV4PositionManager, IPermit2} from "./interfaces/IV4.sol";
interface INPMCreate {
struct MintParams {
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
function createAndInitializePoolIfNecessary(
address token0,
address token1,
uint24 fee,
uint160 sqrtPriceX96
) external payable returns (address pool);
function mint(MintParams calldata params)
external
payable
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
}
interface IV4PoolManager {
function initialize(V4PoolKey memory key, uint160 sqrtPriceX96) external returns (int24 tick);
}
interface IV3Pool {
function slot0()
external
view
returns (uint160 sqrtPriceX96, int24, uint16, uint16, uint16, uint8, bool);
}
interface IV4Next {
function nextTokenId() external view returns (uint256);
}
/// @title AlpsPoolCreator — create + seed the first liquidity of a new pool
///
/// The opt-in launch lane: initialize a brand-new Uniswap v3 or v4 pool at a
/// creator-set price, take the immutable 0.1% platform fee on each seeded side,
/// and mint the first position straight to the creator. A fresh pool has no
/// liquidity, so there is NO swap here — the contract only initializes, skims
/// the fee, and mints; every non-fee wei is deposited or refunded in the tx.
///
/// v4 pools created here are forced hooks == 0 (no custom hook), so this
/// contract can never mint a honeypot/withdraw-tax pool into existence.
///
/// Trust surface: PLATFORM_FEE_BPS and feeSink are immutable; no owner, no
/// keeper, no pause, no upgrade, no fund access. Holds nothing at rest.
contract AlpsPoolCreator is ReentrancyGuard {
using SafeERC20 for OZIERC20;
uint256 public constant PLATFORM_FEE_BPS = 10; // 0.1% each seeded side
uint256 public constant BPS = 10_000;
uint8 internal constant MINT_POSITION = 0x02;
uint8 internal constant SETTLE_PAIR = 0x0d;
uint8 internal constant SWEEP = 0x14;
address internal constant NATIVE = address(0);
uint160 internal constant MAX_UINT160 = type(uint160).max;
uint48 internal constant MAX_UINT48 = type(uint48).max;
INPMCreate public immutable npm;
IV4PositionManager public immutable posm;
IV4PoolManager public immutable poolManager;
IPermit2 public immutable permit2;
address public immutable feeSink;
event PoolSeededV3(
address indexed creator,
address indexed token0,
address indexed token1,
uint24 fee,
uint256 tokenId,
uint128 liquidity
);
event PoolSeededV4(
address indexed creator,
address indexed currency0,
address indexed currency1,
uint24 fee,
uint256 tokenId,
uint128 liquidity
);
error BadParams();
error HooksNotAllowed();
error NativeMismatch();
error SlippageOrDust();
error PriceMismatch();
struct SeedV3Params {
address token0; // must be < token1 (Uniswap ordering); both ERC20 (wrap WETH off-chain)
address token1;
uint24 fee;
uint160 sqrtPriceX96; // initial price the creator sets
int24 tickLower;
int24 tickUpper;
uint256 amount0; // seed amounts; one may be 0 for a single-sided seed
uint256 amount1;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
struct SeedV4Params {
V4PoolKey poolKey; // hooks MUST be 0; currency0 < currency1; currency0 may be native
uint160 sqrtPriceX96;
int24 tickLower;
int24 tickUpper;
uint256 amount0;
uint256 amount1;
uint256 amount0Min;
uint256 amount1Min;
uint256 deadline;
}
constructor(
address npm_,
address posm_,
address poolManager_,
address permit2_,
address feeSink_
) {
if (
npm_ == address(0) || posm_ == address(0) || poolManager_ == address(0)
|| permit2_ == address(0) || feeSink_ == address(0)
) revert BadParams();
npm = INPMCreate(npm_);
posm = IV4PositionManager(posm_);
poolManager = IV4PoolManager(poolManager_);
permit2 = IPermit2(permit2_);
feeSink = feeSink_;
}
receive() external payable {} // native refunds from POSM sweeps land here
// --------------------------------------------------------------- v3 ----
function createAndSeedV3(SeedV3Params calldata p)
external
nonReentrant
returns (uint256 tokenId, uint128 liquidity)
{
if (p.token0 >= p.token1 || p.token0 == address(0)) revert BadParams();
if (p.amount0 == 0 && p.amount1 == 0) revert BadParams();
if (p.tickLower >= p.tickUpper) revert BadParams();
if (p.amount0 > 0) {
OZIERC20(p.token0).safeTransferFrom(msg.sender, address(this), p.amount0);
}
if (p.amount1 > 0) {
OZIERC20(p.token1).safeTransferFrom(msg.sender, address(this), p.amount1);
}
uint256 net0 = _skimErc20(p.token0, p.amount0);
uint256 net1 = _skimErc20(p.token1, p.amount1);
// create + initialize the pool at the creator's price. If the pool
// ALREADY exists this is a no-op that keeps the existing price — a
// front-runner could initialize it at a manipulated price first, then
// our mint would seed at THAT price and be arb'd. So require the live
// price to equal the creator's: fresh pool sets it exactly; a mismatch
// (pre-existing or front-run) reverts, funds returned. (v4 needs no such
// guard: PoolManager.initialize reverts outright if already initialized.)
address pool = npm.createAndInitializePoolIfNecessary(p.token0, p.token1, p.fee, p.sqrtPriceX96);
(uint160 livePrice,,,,,,) = IV3Pool(pool).slot0();
if (livePrice != p.sqrtPriceX96) revert PriceMismatch();
if (net0 > 0) OZIERC20(p.token0).forceApprove(address(npm), net0);
if (net1 > 0) OZIERC20(p.token1).forceApprove(address(npm), net1);
(tokenId, liquidity,,) = npm.mint(
INPMCreate.MintParams({
token0: p.token0,
token1: p.token1,
fee: p.fee,
tickLower: p.tickLower,
tickUpper: p.tickUpper,
amount0Desired: net0,
amount1Desired: net1,
amount0Min: p.amount0Min,
amount1Min: p.amount1Min,
recipient: msg.sender, // NFT straight to the creator
deadline: p.deadline
})
);
_refundErc20(p.token0, msg.sender);
_refundErc20(p.token1, msg.sender);
emit PoolSeededV3(msg.sender, p.token0, p.token1, p.fee, tokenId, liquidity);
}
// --------------------------------------------------------------- v4 ----
function createAndSeedV4(SeedV4Params calldata p)
external
payable
nonReentrant
returns (uint256 tokenId, uint128 liquidity)
{
address c0 = p.poolKey.currency0;
address c1 = p.poolKey.currency1;
if (p.poolKey.hooks != NATIVE) revert HooksNotAllowed();
if (c0 >= c1) revert BadParams(); // native (0) sorts first, so c0==0 is valid
if (p.amount0 == 0 && p.amount1 == 0) revert BadParams();
if (p.tickLower >= p.tickUpper) revert BadParams();
if (p.amount0 > type(uint128).max || p.amount1 > type(uint128).max) revert BadParams();
bool native0 = c0 == NATIVE;
if (native0) {
if (msg.value != p.amount0) revert NativeMismatch();
} else {
if (msg.value != 0) revert NativeMismatch();
if (p.amount0 > 0) OZIERC20(c0).safeTransferFrom(msg.sender, address(this), p.amount0);
}
if (p.amount1 > 0) OZIERC20(c1).safeTransferFrom(msg.sender, address(this), p.amount1);
uint256 net0 = _skimAny(c0, p.amount0);
uint256 net1 = _skimAny(c1, p.amount1);
// initialize the pool at the creator's price (reverts if it already
// exists; the frontend routes existing pools to the add flow)
poolManager.initialize(p.poolKey, p.sqrtPriceX96);
uint160 sqrtL = TickMath.getSqrtRatioAtTick(p.tickLower);
uint160 sqrtU = TickMath.getSqrtRatioAtTick(p.tickUpper);
liquidity = LiquidityAmounts.getLiquidityForAmounts(p.sqrtPriceX96, sqrtL, sqrtU, net0, net1);
if (liquidity == 0) revert SlippageOrDust();
(uint256 mintAmt0, uint256 mintAmt1) =
LiquidityAmounts.getAmountsForLiquidity(p.sqrtPriceX96, sqrtL, sqrtU, liquidity);
if (mintAmt0 < p.amount0Min || mintAmt1 < p.amount1Min) revert SlippageOrDust();
if (!native0 && net0 > 0) _permitPosm(c0, net0);
if (net1 > 0) _permitPosm(c1, net1);
bytes memory actions = native0
? abi.encodePacked(MINT_POSITION, SETTLE_PAIR, SWEEP)
: abi.encodePacked(MINT_POSITION, SETTLE_PAIR);
bytes[] memory params = new bytes[](native0 ? 3 : 2);
params[0] = abi.encode(
p.poolKey,
p.tickLower,
p.tickUpper,
uint256(liquidity),
uint128(net0),
uint128(net1),
msg.sender, // NFT straight to the creator
bytes("")
);
params[1] = abi.encode(c0, c1);
if (native0) params[2] = abi.encode(NATIVE, address(this));
uint256 idBefore = IV4Next(address(posm)).nextTokenId();
posm.modifyLiquidities{value: native0 ? net0 : 0}(abi.encode(actions, params), p.deadline);
tokenId = idBefore;
_refundAny(c0, msg.sender);
_refundAny(c1, msg.sender);
emit PoolSeededV4(msg.sender, c0, c1, p.poolKey.fee, tokenId, liquidity);
}
// ----------------------------------------------------------- helpers ---
function _skimErc20(address token, uint256 amount) internal returns (uint256) {
if (amount == 0) return 0;
uint256 fee = (amount * PLATFORM_FEE_BPS) / BPS;
if (fee > 0) OZIERC20(token).safeTransfer(feeSink, fee);
return amount - fee;
}
function _skimAny(address token, uint256 amount) internal returns (uint256) {
if (amount == 0) return 0;
uint256 fee = (amount * PLATFORM_FEE_BPS) / BPS;
if (fee > 0) {
if (token == NATIVE) {
(bool ok,) = feeSink.call{value: fee}("");
if (!ok) revert BadParams();
} else {
OZIERC20(token).safeTransfer(feeSink, fee);
}
}
return amount - fee;
}
function _permitPosm(address token, uint256 amount) internal {
if (amount == 0) return;
// MAX, not the exact amount: Solady-style tokens fix their Permit2
// allowance at infinity and revert any finite approval (v1 reverted
// here when seeding such a token). Max is accepted by both those and
// standard ERC20s; this contract never holds funds between
// transactions, and Permit2 still scopes the PositionManager to this
// seed's amount below.
OZIERC20(token).forceApprove(address(permit2), type(uint256).max);
uint160 amt = amount > MAX_UINT160 ? MAX_UINT160 : uint160(amount);
permit2.approve(token, address(posm), amt, MAX_UINT48);
}
function _refundErc20(address token, address to) internal {
uint256 bal = OZIERC20(token).balanceOf(address(this));
if (bal > 0) OZIERC20(token).safeTransfer(to, bal);
}
function _refundAny(address token, address to) internal {
if (token == NATIVE) {
uint256 bal = address(this).balance;
if (bal > 0) {
(bool ok,) = to.call{value: bal}("");
if (!ok) revert BadParams();
}
} else {
_refundErc20(token, to);
}
}
}
Chain explorer4979msChain node87ms