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
- Creator
- 0x1E5A3c8b01…57975ad492
- 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