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
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