InstantPoolLauncher

0x9eab33527fbfb6bffb918f7e18e797fe303a77d9

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
11,212 bytes
ABI entries
28
11 read · 3 write
License
none

Contract information

Address
0x9eab33527fbfb6bffb918f7e18e797fe303a77d9
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x2fa5e0ff9c…4e473622e7

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (11)

DEFAULT_CREATOR_SHARE_BPS()uint16
MAX_CREATOR_SHARE_BPS()uint16
getLaunch(uint256)tuple
hasPosition(address)bool
initCodeHash(string, string, address)bytes32
launchCount()uint256
launches(uint256)address, address, address, string, uint256, uint256
positions(uint256)address, address, uint16
protocolTreasury()address
tokenPosition(address)uint256
tokenToLaunchId(address)uint256

Events (2)

FeesCollectedInstantLaunched

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "protocolTreasury_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadName",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadShare",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadSymbol",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadTicks",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DevBuyUnderpaid",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotToken0",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolExists",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "RefundFailed",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethToCreator",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethToProtocol",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenToCreator",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenToProtocol",
        "type": "uint256"
      }
    ],
    "name": "FeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "launchId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "InstantLaunched",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "DEFAULT_CREATOR_SHARE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_CREATOR_SHARE_BPS",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "collect",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount1",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "collectForToken",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount0",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount1",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "id",
        "type": "uint256"
      }
    ],
    "name": "getLaunch",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "token",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "pool",
            "type": "address"
          },
          {
            "internalType": "address",
            "name": "creator",
            "type": "address"
          },
          {
            "internalType": "string",
            "name": "metadataUri",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "tokenId",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "createdAt",
            "type": "uint256"
          }
        ],
        "internalType": "struct InstantPoolLauncher.LaunchInfo",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "hasPosition",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name_",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol_",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "creator_",
        "type": "address"
      }
    ],
    "name": "initCodeHash",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "string",
            "name": "name",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "symbol",
            "type": "string"
          },
          {
            "internalType": "string",
            "name": "metadataUri",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "tokenSupply",
            "type": "uint256"
          },
          {
            "internalType": "bytes32",
            "name": "salt",
            "type": "bytes32"
          },
          {
            "internalType": "uint160",
            "name": "sqrtPriceX96",
            "type": "uint160"
          },
          {
            "internalType": "int24",
            "name": "tickLower",
            "type": "int24"
          },
          {
            "internalType": "int24",
            "name": "tickUpper",
            "type": "int24"
          },
          {
            "internalType": "uint16",
            "name": "creatorShareBps",
            "type": "uint16"
          },
          {
            "internalType": "uint256",
            "name": "developerBuyWei",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "feeRecipient",
            "type": "address"
          }
        ],
        "internalType": "struct InstantPoolLauncher.InstantConfig",
        "name": "cfg",
        "type": "tuple"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "tokenAddr",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "launches",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "string",
        "name": "metadataUri",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "createdAt",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "positions",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRecipient",
        "type": "address"
      },
      {
        "internalType": "uint16",
        "name": "creatorShareBps",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolTreasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokenPosition",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "tokenToLaunchId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import {LaunchedToken} from "./LaunchedToken.sol";
import {RobinhoodAddresses} from "./RobinhoodAddresses.sol";
import {IUniswapV3Factory, IUniswapV3Pool, INonfungiblePositionManager} from "./interfaces/Uniswap.sol";
import {IWETH, ISwapRouter02} from "./interfaces/External.sol";

/// @title InstantPoolLauncher — launches a token straight into a Uniswap V3 pool.
/// @notice Instead of a bonding curve, the full supply is deposited as single-sided
/// concentrated liquidity (all token, zero ETH) from a low starting price up to a
/// target. The token is instantly tradeable on any Uniswap V3 aggregator (GMGN,
/// Dexscreener, wallets) from block one. The LP NFT is held here forever — there is
/// no withdraw/decreaseLiquidity path, so liquidity is unruggable — but its swap fees
/// are permissionlessly collectible, split between the creator and the protocol.
///
/// Price math is computed off-chain and passed in (sqrtPriceX96, tickLower, tickUpper).
/// To keep that math unambiguous the token is CREATE2-deployed with a salt mined so its
/// address sorts below WETH (token == token0), so price is always WETH-per-token.
contract InstantPoolLauncher is ReentrancyGuard {
    using SafeERC20 for IERC20;

    address public immutable protocolTreasury;
    uint16 public constant MAX_CREATOR_SHARE_BPS = 9_000;
    uint16 public constant DEFAULT_CREATOR_SHARE_BPS = 5_000;

    struct InstantConfig {
        string name;
        string symbol;
        string metadataUri;
        uint256 tokenSupply; // 0 => 1e9 * 1e18
        bytes32 salt; // CREATE2 salt, mined so token address < WETH
        uint160 sqrtPriceX96; // initial pool price (= sqrt ratio at tickLower)
        int24 tickLower;
        int24 tickUpper;
        uint16 creatorShareBps; // creator share of LP fees; 0 => default
        uint256 developerBuyWei; // optional immediate buy through the pool
        address feeRecipient; // creator wallet for LP fees; 0 => msg.sender
    }

    struct LaunchInfo {
        address token;
        address pool;
        address creator;
        string metadataUri;
        uint256 tokenId;
        uint256 createdAt;
    }

    struct Position {
        address token;
        address feeRecipient;
        uint16 creatorShareBps;
    }

    LaunchInfo[] public launches;
    mapping(address => uint256) public tokenToLaunchId;
    mapping(uint256 => Position) public positions;
    mapping(address => uint256) public tokenPosition;
    mapping(address => bool) public hasPosition;

    event InstantLaunched(
        uint256 indexed launchId,
        address indexed token,
        address indexed pool,
        address creator,
        uint256 tokenId
    );
    event FeesCollected(
        address indexed token,
        uint256 indexed tokenId,
        uint256 ethToCreator,
        uint256 ethToProtocol,
        uint256 tokenToCreator,
        uint256 tokenToProtocol
    );

    error BadName();
    error BadSymbol();
    error BadShare();
    error BadTicks();
    error NotToken0();
    error PoolExists();
    error DevBuyUnderpaid();
    error RefundFailed();

    constructor(address protocolTreasury_) {
        require(protocolTreasury_ != address(0), "treasury");
        protocolTreasury = protocolTreasury_;
    }

    /// @notice Init-code hash for CREATE2 address prediction / salt mining off-chain.
    function initCodeHash(string calldata name_, string calldata symbol_, address creator_)
        external
        view
        returns (bytes32)
    {
        return keccak256(
            abi.encodePacked(type(LaunchedToken).creationCode, abi.encode(name_, symbol_, creator_, address(this)))
        );
    }

    function launch(InstantConfig calldata cfg)
        external
        payable
        nonReentrant
        returns (address tokenAddr, address pool, uint256 tokenId)
    {
        if (bytes(cfg.name).length == 0 || bytes(cfg.name).length > 30) revert BadName();
        if (bytes(cfg.symbol).length == 0 || bytes(cfg.symbol).length > 10) revert BadSymbol();
        uint16 share = cfg.creatorShareBps == 0 ? DEFAULT_CREATOR_SHARE_BPS : cfg.creatorShareBps;
        if (share > MAX_CREATOR_SHARE_BPS) revert BadShare();
        if (cfg.tickLower >= cfg.tickUpper) revert BadTicks();

        uint256 supply = cfg.tokenSupply == 0 ? 1_000_000_000 ether : cfg.tokenSupply;
        address weth = RobinhoodAddresses.WETH;

        LaunchedToken token = new LaunchedToken{salt: cfg.salt}(cfg.name, cfg.symbol, msg.sender, address(this));
        tokenAddr = address(token);
        // Single-sided math assumes token == token0; caller mines a salt so this holds.
        if (tokenAddr >= weth) revert NotToken0();

        token.setCurve(address(this));
        token.mintToCurve(supply); // mints the full supply to this launcher

        IUniswapV3Factory f = IUniswapV3Factory(RobinhoodAddresses.V3_FACTORY);
        if (f.getPool(tokenAddr, weth, RobinhoodAddresses.FEE_TIER) != address(0)) revert PoolExists();
        pool = f.createPool(tokenAddr, weth, RobinhoodAddresses.FEE_TIER);
        IUniswapV3Pool(pool).initialize(cfg.sqrtPriceX96);

        tokenId = _mintSingleSided(tokenAddr, weth, supply, cfg.tickLower, cfg.tickUpper);

        address feeRecipient = cfg.feeRecipient == address(0) ? msg.sender : cfg.feeRecipient;
        positions[tokenId] = Position({token: tokenAddr, feeRecipient: feeRecipient, creatorShareBps: share});
        tokenPosition[tokenAddr] = tokenId;
        hasPosition[tokenAddr] = true;

        uint256 launchId = launches.length;
        launches.push(
            LaunchInfo({
                token: tokenAddr,
                pool: pool,
                creator: msg.sender,
                metadataUri: cfg.metadataUri,
                tokenId: tokenId,
                createdAt: block.timestamp
            })
        );
        tokenToLaunchId[tokenAddr] = launchId;

        emit InstantLaunched(launchId, tokenAddr, pool, msg.sender, tokenId);

        if (cfg.developerBuyWei > 0) {
            if (msg.value < cfg.developerBuyWei) revert DevBuyUnderpaid();
            IWETH(weth).deposit{value: cfg.developerBuyWei}();
            IERC20(weth).forceApprove(RobinhoodAddresses.SWAP_ROUTER, cfg.developerBuyWei);
            ISwapRouter02(RobinhoodAddresses.SWAP_ROUTER).exactInputSingle(
                ISwapRouter02.ExactInputSingleParams({
                    tokenIn: weth,
                    tokenOut: tokenAddr,
                    fee: RobinhoodAddresses.FEE_TIER,
                    recipient: msg.sender,
                    amountIn: cfg.developerBuyWei,
                    amountOutMinimum: 0,
                    sqrtPriceLimitX96: 0
                })
            );
        }

        if (msg.value > cfg.developerBuyWei) {
            (bool ok,) = msg.sender.call{value: msg.value - cfg.developerBuyWei}("");
            if (!ok) revert RefundFailed();
        }
    }

    function _mintSingleSided(address token, address weth, uint256 supply, int24 tickLower, int24 tickUpper)
        internal
        returns (uint256 tokenId)
    {
        IERC20(token).forceApprove(RobinhoodAddresses.POSITION_MANAGER, supply);
        (tokenId,,,) = INonfungiblePositionManager(RobinhoodAddresses.POSITION_MANAGER).mint(
            INonfungiblePositionManager.MintParams({
                token0: token,
                token1: weth,
                fee: RobinhoodAddresses.FEE_TIER,
                tickLower: tickLower,
                tickUpper: tickUpper,
                amount0Desired: supply,
                amount1Desired: 0,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this),
                deadline: block.timestamp
            })
        );
    }

    function collectForToken(address token) external returns (uint256 amount0, uint256 amount1) {
        require(hasPosition[token], "no position");
        return _collect(tokenPosition[token]);
    }

    function collect(uint256 tokenId) external returns (uint256 amount0, uint256 amount1) {
        return _collect(tokenId);
    }

    function _collect(uint256 tokenId) internal nonReentrant returns (uint256 amount0, uint256 amount1) {
        Position memory pos = positions[tokenId];
        require(pos.token != address(0), "unknown position");

        (amount0, amount1) = INonfungiblePositionManager(RobinhoodAddresses.POSITION_MANAGER).collect(
            INonfungiblePositionManager.CollectParams({
                tokenId: tokenId,
                recipient: address(this),
                amount0Max: type(uint128).max,
                amount1Max: type(uint128).max
            })
        );

        // token is token0 (enforced at launch), WETH is token1.
        uint256 tokenAmt = amount0;
        uint256 wethAmt = amount1;
        address creator = pos.feeRecipient;
        address protocol = protocolTreasury;
        uint16 share = pos.creatorShareBps;

        uint256 ethToCreator;
        uint256 ethToProtocol;
        uint256 tokenToCreator;
        uint256 tokenToProtocol;

        if (wethAmt > 0) {
            IWETH(RobinhoodAddresses.WETH).withdraw(wethAmt);
            ethToCreator = (wethAmt * share) / 10_000;
            ethToProtocol = wethAmt - ethToCreator;
            if (ethToCreator > 0) {
                (bool ok,) = creator.call{value: ethToCreator}("");
                require(ok, "eth creator");
            }
            if (ethToProtocol > 0) {
                (bool ok,) = protocol.call{value: ethToProtocol}("");
                require(ok, "eth protocol");
            }
        }

        if (tokenAmt > 0) {
            tokenToCreator = (tokenAmt * share) / 10_000;
            tokenToProtocol = tokenAmt - tokenToCreator;
            if (tokenToCreator > 0) IERC20(pos.token).safeTransfer(creator, tokenToCreator);
            if (tokenToProtocol > 0) IERC20(pos.token).safeTransfer(protocol, tokenToProtocol);
        }

        emit FeesCollected(pos.token, tokenId, ethToCreator, ethToProtocol, tokenToCreator, tokenToProtocol);
    }

    function launchCount() external view returns (uint256) {
        return launches.length;
    }

    function getLaunch(uint256 id) external view returns (LaunchInfo memory) {
        return launches[id];
    }

    receive() external payable {}
}
Chain explorer2973msChain node82ms