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
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