UniV2Graduator

0xdf8fc2d9e75f0e150f86584a94f6886ccb858cc1

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
2,636 bytes
ABI entries
15
4 read · 1 write
License
none

Contract information

Address
0xdf8fc2d9e75f0e150f86584a94f6886ccb858cc1
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x5e72554950…6da610ec03

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (4)

factory()address
launchpad()address
router()address
weth()address

Events (1)

Graduated

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IUniswapV2Router02",
        "name": "router_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "launchpad_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "NonNativeQuote",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotFeeExempt",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotLaunchpad",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PairAlreadySeeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Reentrancy",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroLiquidity",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pair",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenSeeded",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "quoteSeeded",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "liquidity",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "factory",
    "outputs": [
      {
        "internalType": "contract IUniswapV2Factory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "quote",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "quoteAmount",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "creator",
        "type": "address"
      }
    ],
    "name": "graduate",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchpad",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "router",
    "outputs": [
      {
        "internalType": "contract IUniswapV2Router02",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

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

import {IGraduator} from "./interfaces/IGraduator.sol";
import {IUniswapV2Factory, IUniswapV2Router02, IUniswapV2Pair, IWETH} from "./interfaces/IUniV2.sol";
import {TaxToken} from "./TaxToken.sol";
import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";

/// @title UniV2Graduator
/// @notice Canonical-Uniswap-**v2** graduation adapter: migrates a graduated the launchpad curve launch into a
///         fresh `TOKEN/WETH` Uniswap v2 pair, seeded at the bonding curve's CONTINUITY PRICE, with the
///         LP **permanently burned** (dead-address). Implements `IGraduator` so the launchpad stays
///         chain/venue-agnostic (checklist §2) — it is a drop-in sibling of `UniV4Graduator`, selectable
///         per-launch via the launchpad's graduator registry.
///
/// @dev    WHY v2 NEEDS THE CURVE'S ETH (and can't seed token-only like v4): a Uniswap v2 pair's first
///         `mint` computes `liquidity = sqrt(amount0*amount1) - MINIMUM_LIQUIDITY`, which reverts if
///         EITHER side is zero (there are no concentrated-liquidity ranges in v2). So a v2 pool MUST be
///         seeded two-sided. The bonding curve already collected real quote (ETH) from buyers and reserves
///         an LP token allocation — the launchpad hands BOTH to this adapter, so the two-sided mint is
///         satisfied with ZERO creator capital. This is exactly why "no-seed v2" is only possible via the
///         curve, not via a standalone instant-v2 launch.
///
/// @dev    CONTINUITY (checklist §3): the pool opens at `ethSeed / tokenSeed`, i.e. the exact ratio of the
///         amounts deposited — which the launchpad sized (`CurveParams.continuitySeed`) to the last curve
///         buyer's price. No arb gap, no migration-block sandwich. v2 has no `sqrtPriceX96`; the opening
///         price simply IS the seeded reserve ratio.
///
/// @dev    PERMANENT LOCK (four.meme / Flaunch pattern, checklist §2): v2 LP is a fungible ERC-20; sending
///         it to `0x…dEaD` is an irreversible burn with no withdraw path anywhere — the lock is the
///         *absence* of a recover path, not a timelock. The 0.3% v2 LP fee therefore accrues into locked
///         reserves and is unrecoverable by anyone (accepted — the enforceable per-swap fee lives in the
///         `TaxToken`'s transfer tax, not the LP fee).
///
/// @dev    TAX TOKEN AWARE: the graduated token is a `TaxToken` (fee-on-transfer). This adapter is
///         registered as the token's `graduator` at launch, which (a) makes it fee-exempt so the seeding
///         `addLiquidity` transfer is NOT taxed (a taxed LP add corrupts the pair's reserve accounting and
///         reverts) and (b) authorizes it to call `setPair(pair)` — the switch that turns the transfer tax
///         ON for pair-directed swaps post-graduation. Until `setPair`, the token is effectively clean.
///
/// @dev    SECURITY: `graduate` is gated to the launchpad; reentrancy-guarded; the adapter only ever holds
///         funds transiently within a single `graduate` call and never exposes a withdraw path. NOT yet
///         audited — new liquidity handling + tax-token interaction (the highest-risk class). Audit before
///         mainnet.
contract UniV2Graduator is IGraduator, ReentrancyGuard {
    using SafeTransferLib for address;

    /// @dev Burn sink for permanent LP lock. v2 LP sent here can never be redeemed.
    address internal constant DEAD = 0x000000000000000000000000000000000000dEaD;

    IUniswapV2Router02 public immutable router;
    IUniswapV2Factory public immutable factory;
    /// @dev WETH the pairs are quoted against (router.WETH()). Native-quote launches pair against this.
    address public immutable weth;
    /// @notice The launchpad permitted to trigger graduation (it pre-funds this adapter).
    address public immutable launchpad;

    error NotLaunchpad();
    error ZeroAmount();
    error NonNativeQuote();
    error ZeroLiquidity();
    error PairAlreadySeeded();
    error NotFeeExempt();

    event Graduated(
        address indexed token,
        address indexed quote,
        address indexed pair,
        uint256 tokenSeeded,
        uint256 quoteSeeded,
        uint256 liquidity
    );

    constructor(IUniswapV2Router02 router_, address launchpad_) {
        if (address(router_) == address(0) || launchpad_ == address(0)) revert ZeroAmount();
        router = router_;
        factory = IUniswapV2Factory(router_.factory());
        weth = router_.WETH();
        launchpad = launchpad_;
    }

    modifier onlyLaunchpad() {
        if (msg.sender != launchpad) revert NotLaunchpad();
        _;
    }

    /// @inheritdoc IGraduator
    /// @dev The launchpad has already transferred `tokenAmount` of `token` (ERC-20) and forwarded
    ///      `quoteAmount` of native ETH (as `msg.value`) to this adapter before this call. NATIVE-QUOTE
    ///      ONLY at v1: the v2 pair is always `TOKEN/WETH`. An ERC-20-quoted v2 graduation is a future
    ///      extension (would seed via `addLiquidity` with an ERC-20 pull instead of `addLiquidityETH`).
    function graduate(address token, address quote, uint256 tokenAmount, uint256 quoteAmount, address creator)
        external
        payable
        onlyLaunchpad
        nonReentrant
    {
        // `creator` is unused here (v2 has no hook to route a post-grad share to — the transfer tax on the
        // TaxToken already routes creator+platform fees). Silence the unused-param warning.
        creator;

        if (tokenAmount == 0 || quoteAmount == 0) revert ZeroAmount();
        if (quote != address(0)) revert NonNativeQuote(); // v1: native/WETH-quoted only
        if (msg.value != quoteAmount) revert ZeroAmount(); // launchpad must forward the ETH as msg.value

        // SECURITY — pair-seeding defense. The token address is deterministic + public for the whole curve
        // phase and `factory.createPair` is permissionless, so an attacker can pre-create OR pre-seed the
        // TOKEN/WETH pair before graduation. Two distinct threats:
        //   (a) pre-CREATE an EMPTY pair (free) — harmless: our first-mint below sets the price at OUR ratio.
        //   (b) pre-SEED the pair with real LP at an adversarial ratio — this is the dangerous one. If we
        //       minted into a pre-seeded pair, `pair.mint` credits only `min(l0,l1)` LP and DONATES our
        //       excess side into reserves the ATTACKER co-owns, letting them withdraw most of the raise +
        //       token float (or force a `mint` revert). So we REQUIRE the pair be empty of LP before seeding.
        // A pre-seeded pair therefore reverts here — but the launchpad catches this (see Launchpad's
        // graduation try/catch + rescue) so it NEVER permanently bricks the launch or strands the raise.
        address pair = factory.getPair(token, weth);
        if (pair == address(0)) pair = factory.createPair(token, weth);
        // Empty of LP → a clean first-mint at our exact continuity ratio. Non-empty → attacker-poisoned.
        if (IUniswapV2Pair(pair).totalSupply() != 0) revert PairAlreadySeeded();

        // Defense-in-depth: assert we're fee-exempt so the token leg into the pair is NOT taxed (a misconfig
        // would short the reserves + misprice the pool). Cheap check; fails loud rather than silently.
        if (!TaxToken(payable(token)).isFeeExempt(address(this))) revert NotFeeExempt();

        // Seed two-sided at the continuity ratio: transfer both legs to the (empty) pair, then mint. Wrap
        // the ETH to WETH first (the pair holds WETH). LP → this adapter.
        IWETH(weth).deposit{value: quoteAmount}();
        token.safeTransfer(pair, tokenAmount);
        IWETH(weth).transfer(pair, quoteAmount);
        uint256 liquidity = IUniswapV2Pair(pair).mint(address(this));
        if (liquidity == 0) revert ZeroLiquidity();

        // Turn the transfer tax ON for the now-live pair, then permanently lock the LP by burning it.
        TaxToken(payable(token)).setPair(pair);
        pair.safeTransfer(DEAD, liquidity);

        emit Graduated(token, quote, pair, tokenAmount, quoteAmount, liquidity);
    }

    /// @dev Accept native ETH forwarded by the launchpad at graduation (and router refunds).
    receive() external payable {}
}
Chain explorer5031msChain node69ms