MachineFunLauncher

0x2d02252060ad7d6902fa863d24ad9afa2f2a2bbf

Verification
Verified
0.8.26+commit.8a97fa7a
Type
Contract
12,009 bytes
ABI entries
40
12 read · 10 write
License
none

Contract information

Address
0x2d02252060ad7d6902fa863d24ad9afa2f2a2bbf
Chain
Robinhood Chain (4663)
Compiler
0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xc78e0950e2…e36de08d89

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (12)

LP_FEE()uint24
TICK_SPACING()int24
TOKEN_SUPPLY()uint256
allTokens(uint256)address
allTokensLength()uint256
creatorFeeBps()uint16
defaultStartTick()int24
launchFee()uint256
owner()address
poolManager()address
protocolFeeRecipient()address
tokenInfo(address)address, int24, int24, tuple

Events (7)

CreatorFeeBpsSetDefaultStartTickSetFeesCollectedLaunchFeeSetOwnershipTransferredProtocolFeeRecipientSetTokenLaunched

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "protocolFeeRecipient_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FeeShareTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidStartTick",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnexpectedEthOwed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WrongLaunchFee",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "bps",
        "type": "uint16"
      }
    ],
    "name": "CreatorFeeBpsSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "int24",
        "name": "startTick",
        "type": "int24"
      }
    ],
    "name": "DefaultStartTickSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "FeesCollected",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "LaunchFeeSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "ProtocolFeeRecipientSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "startTick",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "liquidity",
        "type": "uint128"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensInPool",
        "type": "uint256"
      }
    ],
    "name": "TokenLaunched",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "LP_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOKEN_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "allTokensLength",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "collectFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creatorFeeBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "defaultStartTick",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "metadataURI",
        "type": "string"
      },
      {
        "internalType": "int24",
        "name": "startTickOverride",
        "type": "int24"
      }
    ],
    "name": "launch",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolFeeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "bps",
        "type": "uint16"
      }
    ],
    "name": "setCreatorFeeBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "int24",
        "name": "startTick",
        "type": "int24"
      }
    ],
    "name": "setDefaultStartTick",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "setLaunchFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "setProtocolFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "tokenInfo",
    "outputs": [
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      },
      {
        "internalType": "int24",
        "name": "tickLower",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "tickUpper",
        "type": "int24"
      },
      {
        "components": [
          {
            "internalType": "Currency",
            "name": "currency0",
            "type": "address"
          },
          {
            "internalType": "Currency",
            "name": "currency1",
            "type": "address"
          },
          {
            "internalType": "uint24",
            "name": "fee",
            "type": "uint24"
          },
          {
            "internalType": "int24",
            "name": "tickSpacing",
            "type": "int24"
          },
          {
            "internalType": "contract IHooks",
            "name": "hooks",
            "type": "address"
          }
        ],
        "internalType": "struct PoolKey",
        "name": "poolKey",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "rawData",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "withdrawLaunchFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

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

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol";
import {FixedPoint96} from "@uniswap/v4-core/src/libraries/FixedPoint96.sol";

import {MachineToken} from "./MachineToken.sol";

/// @title MachineFunLauncher — machine.fun Uniswap v4 launchpad
///
/// @notice One-transaction fair launches on Uniswap v4:
///
///   1. Deploys a fixed-supply ERC-20 (1B tokens, no mint, no owner powers).
///   2. Creates a native-ETH/token v4 pool (1% LP fee, no hook).
///   3. Deposits the ENTIRE supply as a single-sided liquidity position:
///      range [minUsableTick, startTick], pool initialized exactly at
///      startTick, so the position holds only tokens and every buy (ETH in)
///      moves the price down-tick through the range — i.e. up in USD terms.
///
/// Because the pair is native ETH (Currency = address(0), always currency0),
/// the launched token is always currency1 and "single-sided" means the
/// position is 100% token at initialization: the pool needs zero ETH to
/// launch, and the market cap floor is set purely by `startTick`.
///
/// Starting market cap: price/token at startTick is 1.0001^-startTick ETH, so
///   startTick = ln(supply * ethUsd / targetMcapUsd) / ln(1.0001)
/// The default (190600) targets ~$10k FDV on 1B supply with ETH ≈ $1,900.
/// Pass a per-launch override (or have the owner update the default) to track
/// the live ETH price.
///
/// The LP position is owned by this contract and can never be withdrawn —
/// liquidity is locked forever. Only the swap fees it earns are collectable,
/// split between the token's creator and the protocol.
contract MachineFunLauncher is IUnlockCallback, Ownable {
    // ---------------------------------------------------------------- errors
    error NotPoolManager();
    error WrongLaunchFee();
    error InvalidStartTick();
    error UnknownToken();
    error UnexpectedEthOwed();
    error FeeShareTooHigh();
    error ZeroAddress();
    error EthTransferFailed();

    // ---------------------------------------------------------------- events
    event TokenLaunched(
        address indexed token,
        address indexed creator,
        PoolId indexed poolId,
        int24 startTick,
        uint128 liquidity,
        uint256 tokensInPool
    );
    event FeesCollected(address indexed token, uint256 ethAmount, uint256 tokenAmount);
    event DefaultStartTickSet(int24 startTick);
    event CreatorFeeBpsSet(uint16 bps);
    event ProtocolFeeRecipientSet(address recipient);
    event LaunchFeeSet(uint256 fee);

    // ------------------------------------------------------------- constants
    /// @notice Fixed supply of every launched token (1 billion, 18 decimals).
    uint256 public constant TOKEN_SUPPLY = 1_000_000_000e18;

    /// @notice LP fee of every pool: 1% (units of hundredths of a bip).
    uint24 public constant LP_FEE = 10_000;

    /// @notice Tick spacing of every pool.
    int24 public constant TICK_SPACING = 200;

    /// @notice Uniswap v4 PoolManager (canonical singleton on each chain).
    IPoolManager public immutable poolManager;

    // ---------------------------------------------------------------- config
    /// @notice Default upper tick of new positions = starting price.
    /// @dev 190600 ≈ 1.893e8 tokens/ETH ≈ $10k FDV at ETH $1,900.
    int24 public defaultStartTick = 190_600;

    /// @notice Share of collected LP fees paid to the token creator (bps).
    uint16 public creatorFeeBps = 5_000; // 50%

    /// @notice Receives the protocol's share of LP fees and launch fees.
    address public protocolFeeRecipient;

    /// @notice Optional flat ETH fee charged per launch (0 by default).
    uint256 public launchFee;

    // ----------------------------------------------------------------- state
    struct TokenInfo {
        address creator;
        int24 tickLower;
        int24 tickUpper;
        PoolKey poolKey;
    }

    /// @notice Launch info for every token deployed through this contract.
    mapping(address token => TokenInfo) internal _tokenInfo;

    /// @notice All tokens ever launched (for indexers/frontends).
    address[] public allTokens;

    // -------------------------------------------------------------- callback
    enum Action {
        ADD_LIQUIDITY,
        COLLECT_FEES
    }

    struct CallbackData {
        Action action;
        address token;
        PoolKey key;
        int24 tickLower;
        int24 tickUpper;
        uint128 liquidity;
    }

    constructor(IPoolManager poolManager_, address protocolFeeRecipient_) Ownable(msg.sender) {
        if (address(poolManager_) == address(0) || protocolFeeRecipient_ == address(0)) revert ZeroAddress();
        poolManager = poolManager_;
        protocolFeeRecipient = protocolFeeRecipient_;
    }

    // ---------------------------------------------------------------- launch
    /// @notice Deploy a token and launch it on Uniswap v4 in one transaction.
    /// @param name Token name.
    /// @param symbol Token symbol.
    /// @param metadataURI Off-chain metadata URI (image/description), may be "".
    /// @param startTickOverride Custom starting tick (multiple of TICK_SPACING),
    ///        or 0 to use `defaultStartTick`. Frontends should compute this
    ///        from the live ETH price to hit the $10k target exactly.
    /// @return token The launched token address.
    /// @return poolId The Uniswap v4 pool id.
    function launch(
        string calldata name,
        string calldata symbol,
        string calldata metadataURI,
        int24 startTickOverride
    ) external payable returns (address token, PoolId poolId) {
        if (msg.value != launchFee) revert WrongLaunchFee();

        int24 startTick = startTickOverride == 0 ? defaultStartTick : startTickOverride;
        int24 tickLower = TickMath.minUsableTick(TICK_SPACING);
        if (
            startTick % TICK_SPACING != 0 || startTick <= tickLower
                || startTick > TickMath.maxUsableTick(TICK_SPACING)
        ) revert InvalidStartTick();

        token = address(new MachineToken(name, symbol, metadataURI, TOKEN_SUPPLY));

        PoolKey memory key = PoolKey({
            currency0: Currency.wrap(address(0)), // native ETH — always currency0
            currency1: Currency.wrap(token),
            fee: LP_FEE,
            tickSpacing: TICK_SPACING,
            hooks: IHooks(address(0))
        });
        poolId = key.toId();

        // Initialize exactly at the top of the range: position is 100% token.
        poolManager.initialize(key, TickMath.getSqrtPriceAtTick(startTick));

        // Liquidity that a [tickLower, startTick] position needs to hold the
        // full supply as token1: L = amount1 * Q96 / (sqrtUpper - sqrtLower).
        uint160 sqrtLower = TickMath.getSqrtPriceAtTick(tickLower);
        uint160 sqrtUpper = TickMath.getSqrtPriceAtTick(startTick);
        uint128 liquidity =
            uint128(FullMath.mulDiv(TOKEN_SUPPLY, FixedPoint96.Q96, sqrtUpper - sqrtLower));

        poolManager.unlock(
            abi.encode(
                CallbackData({
                    action: Action.ADD_LIQUIDITY,
                    token: token,
                    key: key,
                    tickLower: tickLower,
                    tickUpper: startTick,
                    liquidity: liquidity
                })
            )
        );

        _tokenInfo[token] =
            TokenInfo({creator: msg.sender, tickLower: tickLower, tickUpper: startTick, poolKey: key});
        allTokens.push(token);

        // Rounding leaves at most a few thousand wei of token behind — burn it
        // so this contract never holds a spendable balance of launched tokens.
        uint256 dust = IERC20(token).balanceOf(address(this));
        if (dust > 0) IERC20(token).transfer(address(0xdead), dust);

        uint256 tokensInPool = TOKEN_SUPPLY - dust;
        emit TokenLaunched(token, msg.sender, poolId, startTick, liquidity, tokensInPool);
    }

    // ------------------------------------------------------------------ fees
    /// @notice Collect the LP fees a token's position has earned and pay them
    ///         out (creatorFeeBps to the creator, remainder to the protocol).
    ///         Permissionless: anyone may trigger a collection.
    function collectFees(address token) external {
        TokenInfo memory info = _tokenInfo[token];
        if (info.creator == address(0)) revert UnknownToken();

        poolManager.unlock(
            abi.encode(
                CallbackData({
                    action: Action.COLLECT_FEES,
                    token: token,
                    key: info.poolKey,
                    tickLower: info.tickLower,
                    tickUpper: info.tickUpper,
                    liquidity: 0
                })
            )
        );
    }

    /// @inheritdoc IUnlockCallback
    function unlockCallback(bytes calldata rawData) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        CallbackData memory data = abi.decode(rawData, (CallbackData));

        if (data.action == Action.ADD_LIQUIDITY) {
            (BalanceDelta delta,) = poolManager.modifyLiquidity(
                data.key,
                ModifyLiquidityParams({
                    tickLower: data.tickLower,
                    tickUpper: data.tickUpper,
                    liquidityDelta: int256(uint256(data.liquidity)),
                    salt: bytes32(0)
                }),
                ""
            );
            // Initialized at tickUpper => the position needs no ETH at all.
            if (delta.amount0() != 0) revert UnexpectedEthOwed();

            // Pay the tokens we owe the pool.
            uint256 owed = uint256(uint128(-delta.amount1()));
            poolManager.sync(data.key.currency1);
            IERC20(data.token).transfer(address(poolManager), owed);
            poolManager.settle();
        } else {
            // liquidityDelta == 0 pokes the position; the returned delta is
            // exactly the fees accrued since the last collection.
            (BalanceDelta delta,) = poolManager.modifyLiquidity(
                data.key,
                ModifyLiquidityParams({
                    tickLower: data.tickLower,
                    tickUpper: data.tickUpper,
                    liquidityDelta: 0,
                    salt: bytes32(0)
                }),
                ""
            );

            address creator = _tokenInfo[data.token].creator;
            uint256 eth = uint256(uint128(delta.amount0()));
            uint256 tok = uint256(uint128(delta.amount1()));
            _payOut(data.key.currency0, eth, creator);
            _payOut(data.key.currency1, tok, creator);
            emit FeesCollected(data.token, eth, tok);
        }
        return "";
    }

    function _payOut(Currency currency, uint256 amount, address creator) internal {
        if (amount == 0) return;
        uint256 creatorShare = (amount * creatorFeeBps) / 10_000;
        if (creatorShare > 0) poolManager.take(currency, creator, creatorShare);
        if (amount - creatorShare > 0) {
            poolManager.take(currency, protocolFeeRecipient, amount - creatorShare);
        }
    }

    // ----------------------------------------------------------------- views
    function tokenInfo(address token)
        external
        view
        returns (address creator, int24 tickLower, int24 tickUpper, PoolKey memory poolKey)
    {
        TokenInfo memory info = _tokenInfo[token];
        return (info.creator, info.tickLower, info.tickUpper, info.poolKey);
    }

    function allTokensLength() external view returns (uint256) {
        return allTokens.length;
    }

    // ----------------------------------------------------------------- admin
    function setDefaultStartTick(int24 startTick) external onlyOwner {
        if (
            startTick % TICK_SPACING != 0 || startTick <= TickMath.minUsableTick(TICK_SPACING)
                || startTick > TickMath.maxUsableTick(TICK_SPACING)
        ) revert InvalidStartTick();
        defaultStartTick = startTick;
        emit DefaultStartTickSet(startTick);
    }

    function setCreatorFeeBps(uint16 bps) external onlyOwner {
        if (bps > 10_000) revert FeeShareTooHigh();
        creatorFeeBps = bps;
        emit CreatorFeeBpsSet(bps);
    }

    function setProtocolFeeRecipient(address recipient) external onlyOwner {
        if (recipient == address(0)) revert ZeroAddress();
        protocolFeeRecipient = recipient;
        emit ProtocolFeeRecipientSet(recipient);
    }

    function setLaunchFee(uint256 fee) external onlyOwner {
        launchFee = fee;
        emit LaunchFeeSet(fee);
    }

    /// @notice Sweep accumulated launch fees (ETH) to the protocol recipient.
    function withdrawLaunchFees() external {
        (bool ok,) = protocolFeeRecipient.call{value: address(this).balance}("");
        if (!ok) revert EthTransferFailed();
    }
}
Chain explorer4562msChain node92ms