PumpFactoryV3Seeded

0x6aa203ea119b3bc0a857071056b91a0e07bd66ee

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
9,061 bytes
ABI entries
20
14 read · 1 write
License
none

Contract information

Address
0x6aa203ea119b3bc0a857071056b91a0e07bd66ee
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x64a1994269…dd51856199

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (14)

BPS()uint256
DUST_BPS()uint256
FEE()uint24
MAX_USABLE_TICK()int24
SPACING()int24
SUPPLY()uint256
WETH()address
locker()address
nfpm()address
poolOf(address)address
predictTokenAddress(bytes32, address)address
tokenImplementation()address
usedSalts(bytes32)bool
v3Factory()address

Events (1)

TokenLaunchedSeeded

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_tokenImplementation",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_v3Factory",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_nfpm",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_weth",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_locker",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "FailedDeployment",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "seedingContract",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "posId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "seededTokens",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "floatTokens",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethSeeded",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      }
    ],
    "name": "TokenLaunchedSeeded",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DUST_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_USABLE_TICK",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "bytes32",
        "name": "xpad",
        "type": "bytes32"
      },
      {
        "internalType": "uint256",
        "name": "seededTokens",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "seedingContract",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "launchSeeded",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "posId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "locker",
    "outputs": [
      {
        "internalType": "contract FeeLockerHolderYield",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "nfpm",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "poolOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "xpad",
        "type": "bytes32"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "predictTokenAddress",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenImplementation",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "usedSalts",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "v3Factory",
    "outputs": [
      {
        "internalType": "contract IUniswapV3Factory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {IERC20} from "./interfaces/IERC20.sol";
import {IWETH} from "./interfaces/IWETH.sol";
import {IUniswapV3Factory} from "./interfaces/IUniswapV3Factory.sol";
import {IUniswapV3Pool} from "./interfaces/IUniswapV3Pool.sol";
import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol";
import {TickMath} from "./libraries/TickMath.sol";
import {XpadTokenV3} from "./XpadTokenV3.sol";
import {FeeLockerHolderYield} from "./FeeLockerHolderYield.sol";

/**
 * @title PumpFactoryV3Seeded
 * @notice Two-sided variant of PumpFactoryV3 for the Community Seeder Pool (see SeederPool.sol).
 *         Instead of a single-sided token-only launch, it splits the supply and mints a TWO-SIDED,
 *         forever-locked V3 position from the remaining float PLUS the seeded ETH.
 *
 * launchSeeded (called only by a SeederPool): deploy def1 clone + mint SUPPLY to this factory ->
 * transfer the seeded share (80-90%) to the seeding contract -> wrap the seeded ETH -> compute the
 * pool price ON-CHAIN from (float tokens, seeded WETH) so there is no off-chain price param and no
 * seat-count race -> create+initialize the pool -> full-range two-sided NFPM.mint (float + WETH)
 * recipient = FeeLockerHolderYield -> register -> sweep bounded dust to the creator -> emit poolId.
 *
 * PRICE. The initial marginal price P = amount1/amount0 (raw units, token0/token1 sorted). A
 * full-range position deposits token0:token1 in exactly the ratio 1:P, so setting
 * sqrtPriceX96 = floor(sqrt(P) * 2^96) = floor(sqrt(amount1 * 2^192 / amount0)) makes the mint
 * consume both provided amounts up to sub-unit rounding; the tiny remainder is swept. Everything is
 * derived from the amounts read at launch, so launch() is deterministic. Same front-run tolerance as
 * PumpFactoryV3: a griefer can pre-create/pre-init the (predictable) pool, but the post-init asserts
 * are the sole price guard - a wrong pre-init price reverts, a correct one is harmless.
 *
 * The locker is FeeLockerHolderYield (40% creator / 35% treasury / 25% holderDistributor), so the
 * seeding contract, as an 80-90% holder, receives its share of the holder-yield slice which it then
 * splits equally among seeders. Immutable wiring; no owner, no upgrade.
 */
contract PumpFactoryV3Seeded is ReentrancyGuard {
    // ─── Immutable wiring ─────────────────────────────────────────────
    address public immutable tokenImplementation;
    IUniswapV3Factory public immutable v3Factory;
    INonfungiblePositionManager public immutable nfpm;
    address public immutable WETH;
    FeeLockerHolderYield public immutable locker;

    // ─── Constants ────────────────────────────────────────────────────
    uint24 public constant FEE = 10000; // 1%
    int24 public constant SPACING = 200;
    uint256 public constant SUPPLY = 1_000_000_000 * 1e18; // 1e27
    int24 public constant MAX_USABLE_TICK = 887200; // floor(887272 / 200) * 200 (full-range bound)

    // Per-side dust tolerance for the two-sided mint. Real remainder from the sqrt floor is a few
    // wei; 1% is a loose ceiling that still reverts loudly on a gross imbalance. The NFPM amountMin
    // floor and the residual ceiling use the SAME bound (invariant: a mint that clears amountMin
    // leaves residual <= the ceiling), mirroring PumpFactoryV3's single-sided dust discipline.
    uint256 public constant DUST_BPS = 100; // 1%
    uint256 public constant BPS = 10000;

    mapping(bytes32 => bool) public usedSalts;
    mapping(address => address) public poolOf;

    event TokenLaunchedSeeded(
        address indexed token,
        address indexed pool,
        address indexed creator,
        address seedingContract,
        uint256 posId,
        uint256 seededTokens,
        uint256 floatTokens,
        uint256 ethSeeded,
        uint160 sqrtPriceX96
    );

    constructor(address _tokenImplementation, address _v3Factory, address _nfpm, address _weth, address _locker) {
        require(_tokenImplementation != address(0), "impl zero");
        require(_v3Factory != address(0), "v3Factory zero");
        require(_nfpm != address(0), "nfpm zero");
        require(_weth != address(0), "weth zero");
        require(_locker != address(0), "locker zero");
        tokenImplementation = _tokenImplementation;
        v3Factory = IUniswapV3Factory(_v3Factory);
        nfpm = INonfungiblePositionManager(_nfpm);
        WETH = _weth;
        locker = FeeLockerHolderYield(_locker);
    }

    /**
     * @notice Atomic two-sided seeded launch. `msg.value` is the seeded ETH (all of it goes into the
     *         locked LP). Called by a SeederPool; `seedingContract` receives the seeded share and
     *         `creator` is the fee-recipient/vanity owner.
     */
    function launchSeeded(
        string calldata name,
        string calldata symbol,
        bytes32 xpad,
        uint256 seededTokens,
        address seedingContract,
        address creator
    ) external payable nonReentrant returns (address token, address pool, uint256 posId) {
        require(msg.value > 0, "no seed ETH");
        require(seedingContract != address(0), "seeding zero");
        require(creator != address(0), "creator zero");
        require(seededTokens > 0 && seededTokens < SUPPLY, "bad seeded amount");
        uint256 floatTokens = SUPPLY - seededTokens;

        // (a) Deploy token clone (def1 vanity from the creator) + mint SUPPLY to this factory.
        token = _deployToken(name, symbol, creator, xpad);

        // (b) Attribute the seeded share to the seeding contract (locked there forever).
        require(IERC20(token).transfer(seedingContract, seededTokens), "seed transfer");

        // (c) Wrap the seeded ETH so both LP sides are ERC-20.
        IWETH(WETH).deposit{value: msg.value}();

        // (d) On-chain price -> create/init -> full-range two-sided mint -> register -> sweep dust.
        uint160 sqrtPriceX96;
        (pool, posId, sqrtPriceX96) = _initAndMint(token, creator, floatTokens, msg.value);
        poolOf[token] = pool;

        emit TokenLaunchedSeeded(
            token, pool, creator, seedingContract, posId, seededTokens, floatTokens, msg.value, sqrtPriceX96
        );
    }

    // ─── Steps ────────────────────────────────────────────────────────

    function _deployToken(string calldata name, string calldata symbol, address creator, bytes32 xpad)
        private
        returns (address token)
    {
        if (xpad == bytes32(0)) {
            token = Clones.clone(tokenImplementation);
        } else {
            // Vanity salt scoped to the creator (the dev), not the SeederPool caller, so the def1
            // address is dev-predictable exactly like the single-sided launcher.
            bytes32 eff = keccak256(abi.encodePacked(creator, xpad));
            require(!usedSalts[eff], "Salt already used");
            usedSalts[eff] = true;
            token = Clones.cloneDeterministic(tokenImplementation, eff);
        }
        XpadTokenV3(token).initialize(name, symbol, creator, SUPPLY); // mints SUPPLY to this factory
        require(IERC20(token).balanceOf(address(this)) == SUPPLY, "mint mismatch");
    }

    function _initAndMint(address token, address creator, uint256 floatTokens, uint256 wethAmt)
        private
        returns (address pool, uint256 posId, uint160 sqrtPriceX96)
    {
        bool tokenIsToken0 = token < WETH;
        (address token0, address token1) = tokenIsToken0 ? (token, WETH) : (WETH, token);
        (uint256 amount0, uint256 amount1) = tokenIsToken0 ? (floatTokens, wethAmt) : (wethAmt, floatTokens);

        // sqrtPriceX96 = floor(sqrt(P) * 2^96), P = amount1/amount0 => sqrt(amount1 * 2^192 / amount0).
        // Math.mulDiv gives full 512-bit precision so amount1 * 2^192 never overflows the intermediate.
        uint256 priceX192 = Math.mulDiv(amount1, 1 << 192, amount0);
        uint256 s = Math.sqrt(priceX192);
        require(s >= TickMath.MIN_SQRT_RATIO && s < TickMath.MAX_SQRT_RATIO, "price out of range");
        sqrtPriceX96 = uint160(s);

        // Create + init, tolerating a front-run pre-create/pre-init; the asserts below are the guard.
        pool = v3Factory.getPool(token, WETH, FEE);
        if (pool == address(0)) pool = v3Factory.createPool(token, WETH, FEE);
        require(pool != address(0), "createPool failed");
        (uint160 existing,,,,,,) = IUniswapV3Pool(pool).slot0();
        if (existing == 0) IUniswapV3Pool(pool).initialize(sqrtPriceX96);

        (uint160 gotSqrt, int24 gotTick,,,,,) = IUniswapV3Pool(pool).slot0();
        require(gotSqrt == sqrtPriceX96, "sqrt mismatch");
        require(gotTick == TickMath.getTickAtSqrtRatio(sqrtPriceX96), "tick mismatch");
        require(IUniswapV3Pool(pool).liquidity() == 0, "liquidity nonzero");

        // Full-range two-sided position. The current tick must sit strictly inside for BOTH sides to
        // be pulled (else the mint is single-sided and reverts on amountMin below).
        int24 tickLower = -MAX_USABLE_TICK;
        int24 tickUpper = MAX_USABLE_TICK;
        require(gotTick > tickLower && gotTick < tickUpper, "tick outside range");

        uint256 amt0Min = amount0 - (amount0 * DUST_BPS) / BPS;
        uint256 amt1Min = amount1 - (amount1 * DUST_BPS) / BPS;

        IERC20(token).approve(address(nfpm), floatTokens);
        IWETH(WETH).approve(address(nfpm), wethAmt);
        uint256 used0;
        uint256 used1;
        (posId,, used0, used1) = nfpm.mint(
            INonfungiblePositionManager.MintParams({
                token0: token0,
                token1: token1,
                fee: FEE,
                tickLower: tickLower,
                tickUpper: tickUpper,
                amount0Desired: amount0,
                amount1Desired: amount1,
                amount0Min: amt0Min,
                amount1Min: amt1Min,
                recipient: address(locker),
                deadline: block.timestamp
            })
        );
        IERC20(token).approve(address(nfpm), 0);
        IWETH(WETH).approve(address(nfpm), 0);

        locker.register(posId, token, creator);

        // Sweep only the UNUSED portion of what WE provided, computed from the mint's returned used
        // amounts - NOT balanceOf(this). WETH is a shared, donatable asset on this predictable
        // singleton; a balanceOf-based residual would let a griefer donate >1% WETH to trip the
        // residual assert and brick every launch. Using `desired - used` ignores donations entirely
        // (they stay inert in the factory, the griefer's loss) and the launch proceeds.
        (uint256 usedToken, uint256 usedWeth) = tokenIsToken0 ? (used0, used1) : (used1, used0);
        _sweep(token, creator, floatTokens - usedToken, floatTokens);
        _sweep(WETH, creator, wethAmt - usedWeth, wethAmt);
    }

    /// @dev Send an exact residual to `to`, bounded to DUST_BPS of what was provided (mirrors the
    ///     amountMin floor: a mint that clears amountMin leaves residual <= this ceiling).
    function _sweep(address asset, address to, uint256 residual, uint256 provided) private {
        require(residual <= (provided * DUST_BPS) / BPS, "residual too high");
        if (residual > 0) require(IERC20(asset).transfer(to, residual), "sweep");
    }

    // ─── Views ────────────────────────────────────────────────────────

    function predictTokenAddress(bytes32 xpad, address creator) external view returns (address) {
        bytes32 eff = keccak256(abi.encodePacked(creator, xpad));
        return Clones.predictDeterministicAddress(tokenImplementation, eff, address(this));
    }
}
Chain explorer3263msChain node92ms