Bonding
0x7d7f3a6e19eeb1e9cb53eb95c950f02393f40ddc
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
22,788 bytes
ABI entries
66
19 read · 14 write
License
none
Contract information
- Address
- 0x7d7f3a6e19eeb1e9cb53eb95c950f02393f40ddc
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0xeCC4faa529…8BC500d958
- Creation tx
- 0x73749f2652…15052fc3d3
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (19)
CURVE_SUPPLY() → uint256
LP_RESERVE() → uint256
TOTAL_SUPPLY() → uint256
VIRTUAL_LIQUIDITY_USD_WAD() → uint256
exposureEnabled(address, uint256) → bool
getTokenInfo(address) → tuple
graduatedPair(address) → address
graduationThresholdUsdWad() → uint256
launchDelaySeconds() → uint256
lpLock() → address
owner() → address
pendingGraduation(address) → tuple
pendingOwner() → address
poolManager() → address
spotConfig(address) → tuple
spotPriceWad(address) → uint256
usdg() → address
v2Factory() → address
zap() → address
Events (13)
CreatorTransferredCurveBuyCurveSellExposureEnabledLaunchDelaySetLaunchedLaunchedSpotOwnershipTransferStartedOwnershipTransferredSpotAssetSetTokenGraduatedTokenGraduatingZapSet
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "v2Factory_",
"type": "address"
},
{
"internalType": "address",
"name": "lpLock_",
"type": "address"
},
{
"internalType": "address",
"name": "usdg_",
"type": "address"
},
{
"internalType": "address",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "uint256",
"name": "graduationThresholdUsdWad_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "BadParam",
"type": "error"
},
{
"inputs": [],
"name": "BadSpotPool",
"type": "error"
},
{
"inputs": [],
"name": "EmptySpotPool",
"type": "error"
},
{
"inputs": [],
"name": "EmptySpotPool",
"type": "error"
},
{
"inputs": [],
"name": "ExposureNotEnabled",
"type": "error"
},
{
"inputs": [],
"name": "NotCreator",
"type": "error"
},
{
"inputs": [],
"name": "NotGraduating",
"type": "error"
},
{
"inputs": [],
"name": "NotReadyToGraduate",
"type": "error"
},
{
"inputs": [],
"name": "OnlyZap",
"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": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "SeriesUnusable",
"type": "error"
},
{
"inputs": [],
"name": "SpotAssetNotEnabled",
"type": "error"
},
{
"inputs": [],
"name": "TokenNotOnCurve",
"type": "error"
},
{
"inputs": [],
"name": "TradingNotOpen",
"type": "error"
},
{
"inputs": [],
"name": "UnknownToken",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "newCreator",
"type": "address"
}
],
"name": "CreatorTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "buyer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "assetInUsed",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
}
],
"name": "CurveBuy",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "seller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensIn",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "assetOut",
"type": "uint256"
}
],
"name": "CurveSell",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "vault",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "seriesId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "ExposureEnabled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "secs",
"type": "uint256"
}
],
"name": "LaunchDelaySet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "pair",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "vault",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "seriesId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "metadataURI",
"type": "string"
}
],
"name": "Launched",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "pair",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes32",
"name": "poolId",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "string",
"name": "name",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"indexed": false,
"internalType": "string",
"name": "metadataURI",
"type": "string"
}
],
"name": "LaunchedSpot",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"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": true,
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"components": [
{
"internalType": "address",
"name": "currency0",
"type": "address"
},
{
"internalType": "address",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "address",
"name": "hooks",
"type": "address"
}
],
"indexed": false,
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"indexed": false,
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "SpotAssetSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "v2Pair",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "lpAmount",
"type": "uint256"
}
],
"name": "TokenGraduated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensForLP",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "assetForLP",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "lpReserveBurned",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "unsoldBurned",
"type": "uint256"
}
],
"name": "TokenGraduating",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "zap",
"type": "address"
}
],
"name": "ZapSet",
"type": "event"
},
{
"inputs": [],
"name": "CURVE_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "LP_RESERVE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TOTAL_SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VIRTUAL_LIQUIDITY_USD_WAD",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"internalType": "uint256",
"name": "assetIn",
"type": "uint256"
},
{
"internalType": "address",
"name": "buyer",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "buy",
"outputs": [
{
"internalType": "uint256",
"name": "tokensOut",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "assetInUsed",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "vault",
"type": "address"
},
{
"internalType": "uint256",
"name": "seriesId",
"type": "uint256"
}
],
"name": "exposureEnabled",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
}
],
"name": "finalizeGraduation",
"outputs": [
{
"internalType": "address",
"name": "v2PairAddr",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
}
],
"name": "getTokenInfo",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "pair",
"type": "address"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "address",
"name": "vault",
"type": "address"
},
{
"internalType": "uint256",
"name": "seriesId",
"type": "uint256"
},
{
"internalType": "address",
"name": "xToken",
"type": "address"
},
{
"internalType": "uint64",
"name": "launchTs",
"type": "uint64"
},
{
"internalType": "enum Bonding.Lifecycle",
"name": "lifecycle",
"type": "uint8"
},
{
"internalType": "bool",
"name": "isSpot",
"type": "bool"
}
],
"internalType": "struct Bonding.TokenInfo",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "graduatedPair",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduationThresholdUsdWad",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "address",
"name": "vault",
"type": "address"
},
{
"internalType": "uint256",
"name": "seriesId",
"type": "uint256"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
}
],
"name": "launch",
"outputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"internalType": "address",
"name": "pair",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "launchDelaySeconds",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "string",
"name": "metadataURI",
"type": "string"
},
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
}
],
"name": "launchSpot",
"outputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"internalType": "address",
"name": "pair",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "lpLock",
"outputs": [
{
"internalType": "contract LPLock",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
}
],
"name": "pendingGraduation",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "tokensForLP",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "assetForLP",
"type": "uint256"
}
],
"internalType": "struct Bonding.PendingGraduation",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"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": [
{
"internalType": "address",
"name": "vault",
"type": "address"
},
{
"internalType": "uint256",
"name": "seriesId",
"type": "uint256"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "registerExposure",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"components": [
{
"internalType": "address",
"name": "currency0",
"type": "address"
},
{
"internalType": "address",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "address",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "registerSpotAsset",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokensIn",
"type": "uint256"
},
{
"internalType": "address",
"name": "seller",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "sell",
"outputs": [
{
"internalType": "uint256",
"name": "assetOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "secs",
"type": "uint256"
}
],
"name": "setLaunchDelaySeconds",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "zap_",
"type": "address"
}
],
"name": "setZap",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
}
],
"name": "spotConfig",
"outputs": [
{
"components": [
{
"components": [
{
"internalType": "address",
"name": "currency0",
"type": "address"
},
{
"internalType": "address",
"name": "currency1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "int24",
"name": "tickSpacing",
"type": "int24"
},
{
"internalType": "address",
"name": "hooks",
"type": "address"
}
],
"internalType": "struct PoolKey",
"name": "key",
"type": "tuple"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
},
{
"internalType": "bool",
"name": "registered",
"type": "bool"
}
],
"internalType": "struct Bonding.SpotConfig",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
}
],
"name": "spotPriceWad",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
},
{
"internalType": "address",
"name": "newCreator",
"type": "address"
}
],
"name": "transferCreator",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "coin",
"type": "address"
}
],
"name": "triggerGraduation",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "usdg",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "v2Factory",
"outputs": [
{
"internalType": "contract IUniswapV2Factory",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "zap",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {LaunchToken} from "./LaunchToken.sol";
import {CurvePair} from "./CurvePair.sol";
import {LPLock} from "./LPLock.sol";
import {IExposureVault} from "../interfaces/IExposureVault.sol";
import {IUniswapV2Factory, IUniswapV2Pair} from "../interfaces/IUniswapV2.sol";
import {UniV2Lib} from "./UniV2Lib.sol";
import {IPoolManager, PoolKey} from "../interfaces/IUniswapV4.sol";
import {V4Lib} from "./V4Lib.sol";
/// @notice Launchpad orchestrator: deploys coins + curve pairs, gates trading
/// (anti-snipe, lifecycle), and runs the two-phase graduation. Only the
/// Zap may launch/trade; graduation finalization is permissionless.
contract Bonding is ReentrancyGuard, Ownable2Step {
using SafeERC20 for IERC20;
using UniV2Lib for IUniswapV2Pair;
error OnlyZap();
error ZeroAddress();
error ExposureNotEnabled();
error UnknownToken();
error TokenNotOnCurve();
error TradingNotOpen();
error NotReadyToGraduate();
error NotGraduating();
error NotCreator();
error SeriesUnusable();
error BadSpotPool();
error EmptySpotPool();
error SpotAssetNotEnabled();
error BadParam();
event ZapSet(address zap);
event LaunchDelaySet(uint256 secs);
event ExposureEnabled(address indexed vault, uint256 indexed seriesId, bool enabled);
event SpotAssetSet(address indexed asset, PoolKey key, bool enabled);
event Launched(
address indexed coin,
address pair,
address indexed creator,
address indexed vault,
uint256 seriesId,
string name,
string symbol,
string metadataURI
);
event LaunchedSpot(
address indexed coin,
address pair,
address indexed creator,
address indexed asset,
bytes32 poolId,
string name,
string symbol,
string metadataURI
);
event CurveBuy(address indexed coin, address indexed buyer, uint256 assetInUsed, uint256 tokensOut);
event CurveSell(address indexed coin, address indexed seller, uint256 tokensIn, uint256 assetOut);
event CreatorTransferred(address indexed coin, address newCreator);
event TokenGraduating(address indexed coin, uint256 tokensForLP, uint256 assetForLP, uint256 lpReserveBurned, uint256 unsoldBurned);
event TokenGraduated(address indexed coin, address v2Pair, uint256 lpAmount);
enum Lifecycle {
None,
Curve,
Graduating,
Graduated
}
struct TokenInfo {
address pair;
address creator;
address vault;
uint256 seriesId;
address xToken;
uint64 launchTs;
Lifecycle lifecycle;
bool isSpot; // false => levier (vault-backed); true => spot (asset priced/traded on its registered V4 pool)
}
struct PendingGraduation {
uint256 tokensForLP;
uint256 assetForLP;
}
struct SpotConfig {
PoolKey key;
bool enabled;
bool registered;
}
uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18;
uint256 public constant CURVE_SUPPLY = 750_000_000e18; // 75%
uint256 public constant LP_RESERVE = 250_000_000e18; // 25%
uint256 public constant VIRTUAL_LIQUIDITY_USD_WAD = 3000e18;
uint256 internal constant WAD = 1e18;
uint256 internal constant MAX_LAUNCH_DELAY = 1 days;
address public zap;
uint256 public immutable graduationThresholdUsdWad;
uint256 public launchDelaySeconds = 0;
IUniswapV2Factory public immutable v2Factory;
LPLock public immutable lpLock;
IERC20 public immutable usdg;
IPoolManager public immutable poolManager;
/// @dev 10**(18 - usdgDecimals): converts USDG raw -> wad in spot price math
uint256 internal immutable usdgScale;
mapping(address => TokenInfo) internal _tokenInfo;
mapping(address => PendingGraduation) internal _pending;
mapping(bytes32 => bool) internal _exposureEnabled;
mapping(address => address) public graduatedPair;
mapping(address => SpotConfig) internal _spotConfig;
modifier onlyZap() {
if (msg.sender != zap) revert OnlyZap();
_;
}
constructor(
address v2Factory_,
address lpLock_,
address usdg_,
address poolManager_,
uint256 graduationThresholdUsdWad_
) Ownable(msg.sender) {
if (v2Factory_ == address(0) || lpLock_ == address(0) || usdg_ == address(0) || poolManager_ == address(0)) {
revert ZeroAddress();
}
v2Factory = IUniswapV2Factory(v2Factory_);
lpLock = LPLock(lpLock_);
usdg = IERC20(usdg_);
poolManager = IPoolManager(poolManager_);
usdgScale = 10 ** (18 - IERC20Metadata(usdg_).decimals());
graduationThresholdUsdWad = graduationThresholdUsdWad_;
}
// ------------------------------------------------------------------ admin
function setZap(address zap_) external onlyOwner {
if (zap_ == address(0)) revert ZeroAddress();
zap = zap_;
emit ZapSet(zap_);
}
function setLaunchDelaySeconds(uint256 secs) external onlyOwner {
if (secs > MAX_LAUNCH_DELAY) revert BadParam();
launchDelaySeconds = secs;
emit LaunchDelaySet(secs);
}
function registerExposure(address vault, uint256 seriesId, bool enabled) external onlyOwner {
if (vault == address(0)) revert ZeroAddress();
_exposureEnabled[keccak256(abi.encodePacked(vault, seriesId))] = enabled;
emit ExposureEnabled(vault, seriesId, enabled);
}
/// @notice Enables an asset for spot launches. `key` must be the hookless V4
/// USDG/asset pool used for pricing AND trade routing.
function registerSpotAsset(address asset, PoolKey calldata key, bool enabled) external onlyOwner {
if (asset == address(0)) revert ZeroAddress();
(address c0, address c1) = V4Lib.sortCurrencies(asset, address(usdg));
bool wellFormed = key.currency0 == c0 && key.currency1 == c1 && key.hooks == address(0)
&& key.fee <= 1_000_000 && key.tickSpacing > 0;
if (!wellFormed) revert BadSpotPool();
if (V4Lib.getSqrtPriceX96(poolManager, V4Lib.toId(key)) == 0) revert BadSpotPool(); // must exist & be initialized
_spotConfig[asset] = SpotConfig({key: key, enabled: enabled, registered: true});
emit SpotAssetSet(asset, key, enabled);
}
function transferCreator(address coin, address newCreator) external {
TokenInfo storage info = _tokenInfo[coin];
if (info.lifecycle == Lifecycle.None) revert UnknownToken();
if (msg.sender != info.creator) revert NotCreator();
if (newCreator == address(0)) revert ZeroAddress();
info.creator = newCreator;
emit CreatorTransferred(coin, newCreator);
}
// ------------------------------------------------------------------ views
function exposureEnabled(address vault, uint256 seriesId) public view returns (bool) {
return _exposureEnabled[keccak256(abi.encodePacked(vault, seriesId))];
}
function getTokenInfo(address coin) external view returns (TokenInfo memory) {
return _tokenInfo[coin];
}
function pendingGraduation(address coin) external view returns (PendingGraduation memory) {
return _pending[coin];
}
function spotConfig(address asset) external view returns (SpotConfig memory) {
return _spotConfig[asset];
}
/// @notice USD-wad value of 1e18 raw units of `asset`, read from its registered
/// V4 pool's slot0 price. Same unit convention as `IExposureVault.navWad`.
/// Works for registered-but-disabled assets (live coins must keep pricing
/// after an owner disables new launches).
function spotPriceWad(address asset) public view returns (uint256) {
SpotConfig storage cfg = _spotConfig[asset];
if (!cfg.registered) revert SpotAssetNotEnabled();
uint160 sqrtP = V4Lib.getSqrtPriceX96(poolManager, V4Lib.toId(cfg.key));
if (sqrtP == 0) revert EmptySpotPool();
return V4Lib.spotPriceWadFromSqrt(sqrtP, cfg.key.currency0 == asset, usdgScale);
}
// ----------------------------------------------------------------- launch
function launch(
string calldata name,
string calldata symbol,
string calldata metadataURI,
address vault,
uint256 seriesId,
address creator
) external onlyZap nonReentrant returns (address coin, address pair) {
if (!exposureEnabled(vault, seriesId)) revert ExposureNotEnabled();
if (creator == address(0)) revert ZeroAddress();
(address xToken,,,,) = IExposureVault(vault).getSeries(seriesId);
LaunchToken t = new LaunchToken(name, symbol, address(this));
coin = address(t);
CurvePair p = new CurvePair(address(this), coin, xToken);
pair = address(p);
IERC20(coin).safeTransfer(pair, CURVE_SUPPLY);
IExposureVault(vault).poke();
uint256 nav = IExposureVault(vault).navWad(seriesId);
if (nav == 0) revert SeriesUnusable(); // wiped series is unusable
uint256 virtualAssetReserve = (VIRTUAL_LIQUIDITY_USD_WAD * WAD) / nav;
p.init(TOTAL_SUPPLY, virtualAssetReserve);
IERC20(xToken).forceApprove(pair, type(uint256).max);
IERC20(coin).forceApprove(pair, type(uint256).max);
_tokenInfo[coin] = TokenInfo({
pair: pair,
creator: creator,
vault: vault,
seriesId: seriesId,
xToken: xToken,
launchTs: uint64(block.timestamp),
lifecycle: Lifecycle.Curve,
isSpot: false
});
emit Launched(coin, pair, creator, vault, seriesId, name, symbol, metadataURI);
}
/// @notice Spot variant of `launch`: the curve's exposure asset is a real ERC-20
/// priced by (and traded against, in the Zap) its external USDG/asset pool.
function launchSpot(
string calldata name,
string calldata symbol,
string calldata metadataURI,
address asset,
address creator
) external onlyZap nonReentrant returns (address coin, address pair) {
SpotConfig memory cfg = _spotConfig[asset];
if (!cfg.enabled) revert SpotAssetNotEnabled();
if (creator == address(0)) revert ZeroAddress();
LaunchToken t = new LaunchToken(name, symbol, address(this));
coin = address(t);
CurvePair p = new CurvePair(address(this), coin, asset);
pair = address(p);
IERC20(coin).safeTransfer(pair, CURVE_SUPPLY);
uint256 price = spotPriceWad(asset); // reverts EmptySpotPool if drained
uint256 virtualAssetReserve = (VIRTUAL_LIQUIDITY_USD_WAD * WAD) / price;
p.init(TOTAL_SUPPLY, virtualAssetReserve);
IERC20(asset).forceApprove(pair, type(uint256).max);
IERC20(coin).forceApprove(pair, type(uint256).max);
_tokenInfo[coin] = TokenInfo({
pair: pair,
creator: creator,
vault: address(0),
seriesId: 0,
xToken: asset,
launchTs: uint64(block.timestamp),
lifecycle: Lifecycle.Curve,
isSpot: true
});
emit LaunchedSpot(coin, pair, creator, asset, V4Lib.toId(cfg.key), name, symbol, metadataURI);
}
// ---------------------------------------------------------------- trading
/// @dev USD-wad price of 1e18 raw exposure-asset units. Levier: vault NAV
/// (fresh — the Zap's mint pokes in the same tx). Spot: live pool reserves.
function _priceWad(TokenInfo storage info) internal view returns (uint256 price) {
if (!info.isSpot) {
price = IExposureVault(info.vault).navWad(info.seriesId);
if (price == 0) revert SeriesUnusable(); // wiped series: pricing would panic
} else {
price = spotPriceWad(info.xToken);
}
}
function buy(address coin, uint256 assetIn, address buyer, address to)
external
onlyZap
nonReentrant
returns (uint256 tokensOut, uint256 assetInUsed)
{
TokenInfo storage info = _tokenInfo[coin];
_checkTradable(info, buyer);
CurvePair pair = CurvePair(info.pair);
// nav freshness: the Zap always calls vault.mint (which pokes) in the same tx before buy; any future non-Zap caller must poke first.
uint256 nav = _priceWad(info);
uint256 raisedUsd = (pair.realAssetRaised() * nav) / WAD;
if (raisedUsd >= graduationThresholdUsdWad) {
// NAV drift already crossed the threshold: graduate, consume nothing.
_enterGraduating(coin, info);
return (0, 0);
}
// Cap the crossing buy exactly at the threshold (ceil: raised lands >= threshold).
uint256 maxAssetIn = Math.ceilDiv((graduationThresholdUsdWad - raisedUsd) * WAD, nav);
uint256 effAssetIn = assetIn < maxAssetIn ? assetIn : maxAssetIn;
IERC20(info.xToken).safeTransferFrom(msg.sender, address(this), effAssetIn);
(tokensOut, assetInUsed) = pair.buy(effAssetIn, to);
if (assetInUsed < effAssetIn) {
IERC20(info.xToken).safeTransfer(msg.sender, effAssetIn - assetInUsed);
}
emit CurveBuy(coin, buyer, assetInUsed, tokensOut);
if (pair.tokenBalance() == 0 || (pair.realAssetRaised() * nav) / WAD >= graduationThresholdUsdWad) {
_enterGraduating(coin, info);
}
}
function sell(address coin, uint256 tokensIn, address seller, address to)
external
onlyZap
nonReentrant
returns (uint256 assetOut)
{
TokenInfo storage info = _tokenInfo[coin];
if (info.lifecycle == Lifecycle.None) revert UnknownToken();
if (info.lifecycle != Lifecycle.Curve) revert TokenNotOnCurve();
IERC20(coin).safeTransferFrom(msg.sender, address(this), tokensIn);
assetOut = CurvePair(info.pair).sell(tokensIn, to);
if (assetOut == 0) {
// dust sell: CurvePair's guard didn't pull the coin, so refund what
// we already pulled instead of stranding it on this contract.
IERC20(coin).safeTransfer(msg.sender, tokensIn);
return 0;
}
emit CurveSell(coin, seller, tokensIn, assetOut);
}
// ------------------------------------------------------------- graduation
/// @notice Permissionless: anyone can start graduation once the threshold is met
/// (e.g. the raised reserve appreciated past it with zero buys).
function triggerGraduation(address coin) external nonReentrant {
TokenInfo storage info = _tokenInfo[coin];
if (info.lifecycle == Lifecycle.None) revert UnknownToken();
if (info.lifecycle != Lifecycle.Curve) revert TokenNotOnCurve();
if (!info.isSpot) IExposureVault(info.vault).poke();
CurvePair pair = CurvePair(info.pair);
bool soldOut = pair.tokenBalance() == 0;
uint256 nav = soldOut ? 0 : _priceWad(info); // sold-out graduates regardless of price
if (!soldOut && (pair.realAssetRaised() * nav) / WAD < graduationThresholdUsdWad) {
revert NotReadyToGraduate();
}
_enterGraduating(coin, info);
}
/// @dev Phase 1: freeze the curve, burn unsold tokens, pull the real raised asset
/// out of the pair, size the LP allocation at the curve-close marginal price,
/// burn whatever part of the 250M LP reserve is not needed.
function _enterGraduating(address coin, TokenInfo storage info) internal {
info.lifecycle = Lifecycle.Graduating;
CurvePair pair = CurvePair(info.pair);
(uint256 tokenReserve, uint256 assetReserve) = pair.getReserves();
uint256 unsold = pair.tokenBalance();
if (unsold > 0) LaunchToken(coin).burn(info.pair, unsold);
uint256 assetForLP = pair.realAssetRaised();
if (assetForLP > 0) pair.extractForGraduation(assetForLP, address(this));
// price-matched: tokensForLP/assetForLP == tokenReserve/assetReserve (curve-close price)
uint256 tokensForLP = assetReserve == 0 ? 0 : (assetForLP * tokenReserve) / assetReserve;
if (tokensForLP > LP_RESERVE) tokensForLP = LP_RESERVE;
uint256 lpBurned = LP_RESERVE - tokensForLP;
if (lpBurned > 0) LaunchToken(coin).burn(address(this), lpBurned);
_pending[coin] = PendingGraduation({tokensForLP: tokensForLP, assetForLP: assetForLP});
emit TokenGraduating(coin, tokensForLP, assetForLP, lpBurned, unsold);
}
/// @notice Phase 2 — permissionless. Creates (or adopts) the v2 pair, seeds it at the
/// curve-close price, and locks the LP forever. Defends three regimes:
/// (1) pristine pair, (2) donation-only pre-seed (skim), (3) hostile pre-minted
/// LP (swap-rebalance toward the curve-close price, then add at pool ratio).
function finalizeGraduation(address coin) external nonReentrant returns (address v2PairAddr) {
TokenInfo storage info = _tokenInfo[coin];
if (info.lifecycle != Lifecycle.Graduating) revert NotGraduating();
PendingGraduation memory p = _pending[coin];
address xToken = info.xToken;
v2PairAddr = v2Factory.getPair(coin, xToken);
if (v2PairAddr == address(0)) v2PairAddr = v2Factory.createPair(coin, xToken);
IUniswapV2Pair v2Pair = IUniswapV2Pair(v2PairAddr);
// Regime 2: donations without LP — skim to self, burn coin part, sweep asset part.
if (v2Pair.totalSupply() == 0) {
uint256 coinBal = IERC20(coin).balanceOf(v2PairAddr);
uint256 assetBal = IERC20(xToken).balanceOf(v2PairAddr);
if (coinBal > 0 || assetBal > 0) {
uint256 coinBefore = IERC20(coin).balanceOf(address(this));
uint256 assetBefore = IERC20(xToken).balanceOf(address(this));
v2Pair.skim(address(this));
uint256 coinGot = IERC20(coin).balanceOf(address(this)) - coinBefore;
uint256 assetGot = IERC20(xToken).balanceOf(address(this)) - assetBefore;
if (coinGot > 0) LaunchToken(coin).burn(address(this), coinGot);
if (assetGot > 0) IERC20(xToken).safeTransfer(owner(), assetGot);
}
}
uint256 lpAmount;
if (v2Pair.totalSupply() == 0) {
// Regime 1: pristine — direct two-sided mint at exactly the curve-close price.
IERC20(coin).safeTransfer(v2PairAddr, p.tokensForLP);
IERC20(xToken).safeTransfer(v2PairAddr, p.assetForLP);
lpAmount = v2Pair.mint(address(lpLock));
} else {
// Regime 3: hostile pre-minted LP.
lpAmount = _hostileAdd(coin, xToken, v2Pair, p);
}
lpLock.recordLock(coin, v2PairAddr, lpAmount);
graduatedPair[coin] = v2PairAddr;
info.lifecycle = Lifecycle.Graduated;
delete _pending[coin];
emit TokenGraduated(coin, v2PairAddr, lpAmount);
}
/// @dev Swap-rebalances the hostile pool toward the curve-close price
/// (s = sqrt(rIn*rOut*targetN/targetD) - rIn, capped at 99% of our budget,
/// fee ignored as an approximation), then adds liquidity at the pool's ratio.
/// Leftover coins are burned; leftover asset is swept to the owner.
function _hostileAdd(address coin, address xToken, IUniswapV2Pair v2Pair, PendingGraduation memory p)
internal
returns (uint256 lpAmount)
{
uint256 tokensLeft = p.tokensForLP;
uint256 assetLeft = p.assetForLP;
(uint256 rCoin, uint256 rAsset) = v2Pair.orientedReserves(coin);
if (rCoin * p.assetForLP > rAsset * p.tokensForLP) {
// pool coin-heavy vs target => coin too cheap => buy coins with asset
uint256 s = Math.sqrt(Math.mulDiv(rAsset * rCoin, p.assetForLP, p.tokensForLP));
s = s > rAsset ? s - rAsset : 0;
uint256 maxSpend = (assetLeft * 99) / 100;
if (s > maxSpend) s = maxSpend;
if (s > 0) {
uint256 out = UniV2Lib.getAmountOut(s, rAsset, rCoin);
if (out > 0) {
IERC20(xToken).safeTransfer(address(v2Pair), s);
v2Pair.swapOriented(coin, out, true, address(this));
assetLeft -= s;
tokensLeft += out;
}
}
} else if (rCoin * p.assetForLP < rAsset * p.tokensForLP) {
// pool asset-heavy vs target => coin too expensive => sell coins in
uint256 s = Math.sqrt(Math.mulDiv(rCoin * rAsset, p.tokensForLP, p.assetForLP));
s = s > rCoin ? s - rCoin : 0;
uint256 maxSpend = (tokensLeft * 99) / 100;
if (s > maxSpend) s = maxSpend;
if (s > 0) {
uint256 out = UniV2Lib.getAmountOut(s, rCoin, rAsset);
if (out > 0) {
IERC20(coin).safeTransfer(address(v2Pair), s);
v2Pair.swapOriented(coin, out, false, address(this));
tokensLeft -= s;
assetLeft += out;
}
}
}
// add at the pool's (rebalanced) ratio
(rCoin, rAsset) = v2Pair.orientedReserves(coin);
uint256 addAsset = assetLeft;
uint256 addCoins = Math.mulDiv(addAsset, rCoin, rAsset);
if (addCoins > tokensLeft) {
addCoins = tokensLeft;
addAsset = Math.mulDiv(addCoins, rAsset, rCoin);
}
IERC20(coin).safeTransfer(address(v2Pair), addCoins);
IERC20(xToken).safeTransfer(address(v2Pair), addAsset);
lpAmount = v2Pair.mint(address(lpLock));
uint256 coinLeftover = tokensLeft - addCoins;
if (coinLeftover > 0) LaunchToken(coin).burn(address(this), coinLeftover);
uint256 assetLeftover = assetLeft - addAsset;
if (assetLeftover > 0) IERC20(xToken).safeTransfer(owner(), assetLeftover);
}
function _checkTradable(TokenInfo storage info, address buyer) internal view {
if (info.lifecycle == Lifecycle.None) revert UnknownToken();
if (info.lifecycle != Lifecycle.Curve) revert TokenNotOnCurve();
if (block.timestamp < uint256(info.launchTs) + launchDelaySeconds && buyer != info.creator) {
revert TradingNotOpen();
}
}
}
Chain explorer3228msChain node78ms