BBCTokenLaunchV4

0x1ea80084b98ebb2c26080911f5da88bd67500ef6

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
16,765 bytes
ABI entries
86
38 read · 13 write
License
none

Contract information

Address
0x1ea80084b98ebb2c26080911f5da88bd67500ef6
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x4ee3bf2cb3…ec5fc0a40e

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (38)

BPS()uint256
CURVE_ALLOCATION()uint256
GRADUATION_ETH_THRESHOLD()uint256
INITIAL_CURVE_TOKENS()uint256
LP_FEE()uint24
PRESALE_DURATION()uint256
PRESALE_ETH_CAP_PER_NFT()uint256
TEAM_ALLOCATION()uint256
TICK_SPACING()int24
TOTAL_SUPPLY()uint256
TRADE_FEE_BPS()uint256
VIRTUAL_ETH_RESERVE()uint256
accumulatedFees()uint256
bbcNft()address
currentPrice()uint256
estimateBuy(uint256)uint256
estimateSell(uint256)uint256
ethReserve()uint256
feeHook()address
feeRecipient()address
getReserves()uint256, uint256
graduationProgress()uint256
livePhase()uint8
owner()address
phase()uint8
poolInitialized()bool
poolKey()address, address, uint24, int24, address
poolManager()address
presaleCapRemaining(address)uint256
presaleEndTime()uint256
presaleSpent(address)uint256
presaleStartTime()uint256
previewFees()uint256
teamVesting()address
thresholdReached()bool
token()address
tokenReserve()uint256
tradingPaused()bool

Events (10)

FeeRecipientUpdatedFeesClaimedGraduatedInitializedOwnershipTransferredPresaleOpenedPublicOpenedTokensPurchasedTokensSoldTradingPausedSet

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "bbcNft_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "poolManager_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeHook_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "feeRecipient_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      }
    ],
    "name": "AddressEmptyCode",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "AddressInsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyGraduated",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "AlreadyInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CurveClosed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CurveOverflow",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EthTransferFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedInnerCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "GraduationNotReached",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MathOverflowedMulDiv",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotGraduated",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotInitialized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotNFTHolder",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PresaleCapExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SlippageExceeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TradingIsPaused",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "WrongPhase",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "FeeRecipientUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "FeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethSeeded",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensSeeded",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "liquidity",
        "type": "uint128"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "timestamp",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "teamVesting",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "totalSupply",
        "type": "uint256"
      }
    ],
    "name": "Initialized",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "startTime",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "endTime",
        "type": "uint256"
      }
    ],
    "name": "PresaleOpened",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "timestamp",
        "type": "uint256"
      }
    ],
    "name": "PublicOpened",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newPrice",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethReserve",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenReserve",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "timestamp",
        "type": "uint256"
      }
    ],
    "name": "TokensPurchased",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "seller",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newPrice",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethReserve",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenReserve",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "timestamp",
        "type": "uint256"
      }
    ],
    "name": "TokensSold",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "name": "TradingPausedSet",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "CURVE_ALLOCATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRADUATION_ETH_THRESHOLD",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "INITIAL_CURVE_TOKENS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LP_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PRESALE_DURATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "PRESALE_ETH_CAP_PER_NFT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TEAM_ALLOCATION",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TRADE_FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "VIRTUAL_ETH_RESERVE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "accumulatedFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "bbcNft",
    "outputs": [
      {
        "internalType": "contract IERC721Balance",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "buy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "currentPrice",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      }
    ],
    "name": "estimateBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "estimateSell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ethReserve",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeHook",
    "outputs": [
      {
        "internalType": "contract IHooks",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "feeRecipient",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getReserves",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethRes",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "tokenRes",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "graduate",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "graduationProgress",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "bps",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name_",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol_",
        "type": "string"
      },
      {
        "internalType": "address",
        "name": "teamBeneficiary_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "overrideSignerB_",
        "type": "address"
      }
    ],
    "name": "initialize",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "livePhase",
    "outputs": [
      {
        "internalType": "enum BBCTokenLaunchV4.Phase",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "openPresale",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "openPublic",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "phase",
    "outputs": [
      {
        "internalType": "enum BBCTokenLaunchV4.Phase",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolInitialized",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolKey",
    "outputs": [
      {
        "internalType": "Currency",
        "name": "currency0",
        "type": "address"
      },
      {
        "internalType": "Currency",
        "name": "currency1",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      },
      {
        "internalType": "contract IHooks",
        "name": "hooks",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "presaleCapRemaining",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "presaleEndTime",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "presaleSpent",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "presaleStartTime",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "previewFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      }
    ],
    "name": "sell",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient_",
        "type": "address"
      }
    ],
    "name": "setFeeRecipient",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "paused_",
        "type": "bool"
      }
    ],
    "name": "setTradingPaused",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "sweepDust",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "teamVesting",
    "outputs": [
      {
        "internalType": "contract TeamVesting",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "thresholdReached",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "token",
    "outputs": [
      {
        "internalType": "contract BBCToken",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenReserve",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tradingPaused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

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

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

import {BBCToken} from "./BBCToken.sol";
import {TeamVesting} from "./TeamVesting.sol";

import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {LiquidityAmounts} from "v4-periphery/src/libraries/LiquidityAmounts.sol";

interface IERC721Balance {
    function balanceOf(address owner) external view returns (uint256);
}

/// @title BBCTokenLaunchV4 - bonding curve launchpad graduating to Uniswap V4
/// @notice Constant-product (x*y=k) bonding curve with a virtual ETH reserve.
///         NFT-holder presale window, then public. Model C graduation: when the
///         curve raises GRADUATION_ETH_THRESHOLD, ALL remaining curve tokens +
///         ALL raised ETH seed a native-ETH Uniswap V4 pool with the BBCFeeHook
///         attached. Nothing is burned; the liquidity position is owned by this
///         contract, which has no removal path, so it is permanently locked.
///         The dev fee comes from the hook (per-swap), independent of the LP.
///
///         1% curve trade fee -> single settable recipient (dev wallet).
contract BBCTokenLaunchV4 is Ownable, ReentrancyGuard, IUnlockCallback {
    using SafeERC20 for IERC20;

    // ----- Supply (1 billion, 18 decimals) -----
    uint256 public constant TOTAL_SUPPLY = 1_000_000_000 ether;
    uint256 public constant CURVE_ALLOCATION = 900_000_000 ether; // 90% curve (presale + public + pool seed)
    uint256 public constant TEAM_ALLOCATION = 100_000_000 ether; // 10% team -> 60d linear vest
    uint256 public constant INITIAL_CURVE_TOKENS = CURVE_ALLOCATION;

    // ----- Curve -----
    uint256 public constant VIRTUAL_ETH_RESERVE = 1.2 ether;
    uint256 public constant GRADUATION_ETH_THRESHOLD = 10.69 ether;

    // ----- Fees -----
    uint256 public constant BPS = 10_000;
    uint256 public constant TRADE_FEE_BPS = 100; // 1% of ETH side, all to feeRecipient

    // ----- Presale -----
    uint256 public constant PRESALE_DURATION = 24 hours;
    uint256 public constant PRESALE_ETH_CAP_PER_NFT = 0.04 ether;

    // ----- Uniswap V4 pool config -----
    uint24 public constant LP_FEE = 3000; // 0.30% standard tier (accrues to the locked LP)
    int24 public constant TICK_SPACING = 60;

    // ----- External -----
    IERC721Balance public immutable bbcNft;
    IPoolManager public immutable poolManager;
    IHooks public immutable feeHook;

    // ----- State -----
    BBCToken public token;
    TeamVesting public teamVesting;

    enum Phase {
        UNINITIALIZED,
        INITIALIZED,
        PRESALE,
        PUBLIC,
        GRADUATED
    }

    Phase public phase;
    uint256 public presaleStartTime;
    uint256 public presaleEndTime;

    uint256 public ethReserve; // real ETH in the curve (fees excluded)
    uint256 public tokenReserve; // tokens remaining on the curve

    uint256 public accumulatedFees; // 1% curve fees awaiting claim
    address public feeRecipient; // dev wallet

    mapping(address => uint256) public presaleSpent;

    bool public tradingPaused;
    bool public thresholdReached; // latched once the curve fills to the graduation threshold
    PoolKey public poolKey;
    bool public poolInitialized;

    // ----- Events -----
    event Initialized(address indexed token, address indexed teamVesting, uint256 totalSupply);
    event PresaleOpened(uint256 startTime, uint256 endTime);
    event PublicOpened(uint256 timestamp);
    event TokensPurchased(
        address indexed buyer, uint256 ethIn, uint256 tokensOut, uint256 newPrice, uint256 ethReserve, uint256 tokenReserve, uint256 timestamp
    );
    event TokensSold(
        address indexed seller, uint256 tokensIn, uint256 ethOut, uint256 newPrice, uint256 ethReserve, uint256 tokenReserve, uint256 timestamp
    );
    event FeesClaimed(uint256 amount, address indexed to);
    event FeeRecipientUpdated(address recipient);
    event Graduated(bytes32 indexed poolId, uint256 ethSeeded, uint256 tokensSeeded, uint128 liquidity, uint256 timestamp);
    event TradingPausedSet(bool paused);

    // ----- Errors -----
    error AlreadyInitialized();
    error NotInitialized();
    error WrongPhase();
    error NotNFTHolder();
    error PresaleCapExceeded();
    error SlippageExceeded();
    error ZeroAmount();
    error CurveOverflow();
    error GraduationNotReached();
    error AlreadyGraduated();
    error TradingIsPaused();
    error EthTransferFailed();
    error ZeroAddress();
    error NotPoolManager();
    error CurveClosed();
    error NotGraduated();

    constructor(address bbcNft_, address poolManager_, address feeHook_, address feeRecipient_, address owner_)
        Ownable(owner_)
    {
        if (bbcNft_ == address(0) || poolManager_ == address(0) || feeHook_ == address(0) || feeRecipient_ == address(0))
        {
            revert ZeroAddress();
        }
        bbcNft = IERC721Balance(bbcNft_);
        poolManager = IPoolManager(poolManager_);
        feeHook = IHooks(feeHook_);
        feeRecipient = feeRecipient_;
    }

    // =========================================================== lifecycle

    /// @notice Deploy the token + vesting wall and fund allocations. Curve keeps
    ///         the full 900M; team 100M goes to the 60-day linear vesting wall.
    function initialize(string memory name_, string memory symbol_, address teamBeneficiary_, address overrideSignerB_)
        external
        onlyOwner
    {
        if (phase != Phase.UNINITIALIZED) revert AlreadyInitialized();
        if (teamBeneficiary_ == address(0)) revert ZeroAddress();

        token = new BBCToken(name_, symbol_, TOTAL_SUPPLY, address(this));
        teamVesting = new TeamVesting(address(token), teamBeneficiary_, owner(), overrideSignerB_);
        IERC20(address(token)).safeTransfer(address(teamVesting), TEAM_ALLOCATION);

        tokenReserve = INITIAL_CURVE_TOKENS;
        phase = Phase.INITIALIZED;
        emit Initialized(address(token), address(teamVesting), TOTAL_SUPPLY);
    }

    function openPresale() external onlyOwner {
        if (phase != Phase.INITIALIZED) revert WrongPhase();
        phase = Phase.PRESALE;
        presaleStartTime = block.timestamp;
        presaleEndTime = block.timestamp + PRESALE_DURATION;
        emit PresaleOpened(presaleStartTime, presaleEndTime);
    }

    function openPublic() external onlyOwner {
        if (livePhase() != Phase.PRESALE) revert WrongPhase();
        phase = Phase.PUBLIC;
        emit PublicOpened(block.timestamp);
    }

    /// @notice Phase with the 24h presale lapse applied lazily.
    function livePhase() public view returns (Phase) {
        Phase p = phase;
        if (p == Phase.PRESALE && block.timestamp >= presaleEndTime) return Phase.PUBLIC;
        return p;
    }

    // ============================================================= trading

    function buy(uint256 minTokensOut) external payable nonReentrant returns (uint256 tokensOut) {
        if (tradingPaused) revert TradingIsPaused();
        if (msg.value == 0) revert ZeroAmount();

        Phase p = livePhase();
        if (p == Phase.PRESALE) {
            if (bbcNft.balanceOf(msg.sender) == 0) revert NotNFTHolder();
        } else if (p != Phase.PUBLIC) {
            if (p == Phase.GRADUATED) revert AlreadyGraduated();
            revert WrongPhase();
        }

        uint256 netIn = (msg.value * (BPS - TRADE_FEE_BPS)) / BPS;
        uint256 fee;
        uint256 refund;
        uint256 capacity = GRADUATION_ETH_THRESHOLD - ethReserve;
        if (netIn >= capacity) {
            netIn = capacity;
            uint256 grossUsed = Math.ceilDiv(netIn * BPS, BPS - TRADE_FEE_BPS);
            fee = grossUsed - netIn;
            refund = msg.value - grossUsed;
        } else {
            fee = msg.value - netIn;
        }
        if (netIn == 0) revert ZeroAmount();

        if (p == Phase.PRESALE) {
            uint256 spent = presaleSpent[msg.sender] + (msg.value - refund);
            if (spent > PRESALE_ETH_CAP_PER_NFT * bbcNft.balanceOf(msg.sender)) revert PresaleCapExceeded();
            presaleSpent[msg.sender] = spent;
        }

        tokensOut = (tokenReserve * netIn) / (ethReserve + VIRTUAL_ETH_RESERVE + netIn);
        if (tokensOut == 0) revert ZeroAmount();
        if (tokensOut < minTokensOut) revert SlippageExceeded();

        ethReserve += netIn;
        tokenReserve -= tokensOut;
        accumulatedFees += fee;
        if (ethReserve >= GRADUATION_ETH_THRESHOLD) thresholdReached = true; // latch: curve is full

        emit TokensPurchased(msg.sender, msg.value - refund, tokensOut, currentPrice(), ethReserve, tokenReserve, block.timestamp);

        IERC20(address(token)).safeTransfer(msg.sender, tokensOut);
        if (refund > 0) _sendEth(msg.sender, refund);
    }

    /// @notice Sell tokens back to the curve. NOT gated by tradingPaused - exit
    ///         is always available so a paused owner can never trap user ETH
    ///         (pause only blocks buys). Blocked once the curve has filled to the
    ///         graduation threshold (curve closed, awaiting the permissionless
    ///         graduate()), so a dust sell cannot grief graduation.
    function sell(uint256 tokenAmount, uint256 minEthOut) external nonReentrant returns (uint256 ethOut) {
        if (thresholdReached) revert CurveClosed();
        if (tokenAmount == 0) revert ZeroAmount();

        Phase p = livePhase();
        if (p != Phase.PRESALE && p != Phase.PUBLIC) {
            if (p == Phase.GRADUATED) revert AlreadyGraduated();
            revert WrongPhase();
        }
        if (tokenReserve + tokenAmount > INITIAL_CURVE_TOKENS) revert CurveOverflow();

        uint256 ethGross = ((ethReserve + VIRTUAL_ETH_RESERVE) * tokenAmount) / (tokenReserve + tokenAmount);
        if (ethGross > ethReserve) ethGross = ethReserve;
        uint256 fee = (ethGross * TRADE_FEE_BPS) / BPS;
        ethOut = ethGross - fee;
        if (ethOut == 0) revert ZeroAmount();
        if (ethOut < minEthOut) revert SlippageExceeded();

        ethReserve -= ethGross;
        tokenReserve += tokenAmount;
        accumulatedFees += fee;

        emit TokensSold(msg.sender, tokenAmount, ethOut, currentPrice(), ethReserve, tokenReserve, block.timestamp);

        IERC20(address(token)).safeTransferFrom(msg.sender, address(this), tokenAmount);
        _sendEth(msg.sender, ethOut);
    }

    // =========================================================== graduation

    /// @notice Model C graduation. Permissionless once the threshold is met.
    ///         Seeds a native-ETH V4 pool with ALL remaining curve tokens + ALL
    ///         raised ETH at the implied price, full range, hook attached. The
    ///         liquidity position is owned by this contract (no removal path) =>
    ///         permanently locked. Nothing is burned.
    function graduate() external nonReentrant {
        Phase p = phase;
        if (p == Phase.GRADUATED) revert AlreadyGraduated();
        if (p != Phase.PRESALE && p != Phase.PUBLIC) revert NotInitialized();
        if (!thresholdReached) revert GraduationNotReached();

        uint256 ethLP = ethReserve;
        uint256 tokensLP = tokenReserve;
        phase = Phase.GRADUATED;
        ethReserve = 0;
        tokenReserve = 0;

        // native ETH is currency0 (address(0) < token), BBC is currency1
        PoolKey memory key = PoolKey({
            currency0: Currency.wrap(address(0)),
            currency1: Currency.wrap(address(token)),
            fee: LP_FEE,
            tickSpacing: TICK_SPACING,
            hooks: feeHook
        });
        poolKey = key;

        // pool price P = token1/token0 = tokensLP per ETH; sqrtPriceX96 = sqrt(P)*2^96
        uint160 sqrtPriceX96 = uint160(Math.sqrt(Math.mulDiv(tokensLP, 1 << 192, ethLP)));
        poolManager.initialize(key, sqrtPriceX96);
        poolInitialized = true;

        int24 tickLower = TickMath.minUsableTick(TICK_SPACING);
        int24 tickUpper = TickMath.maxUsableTick(TICK_SPACING);
        uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts(
            sqrtPriceX96, TickMath.getSqrtPriceAtTick(tickLower), TickMath.getSqrtPriceAtTick(tickUpper), ethLP, tokensLP
        );

        poolManager.unlock(abi.encode(key, tickLower, tickUpper, liquidity, ethLP));

        emit Graduated(_poolId(key), ethLP, tokensLP, liquidity, block.timestamp);
    }

    /// @notice V4 unlock callback: add the liquidity and settle what we owe.
    function unlockCallback(bytes calldata data) external override returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        (PoolKey memory key, int24 tickLower, int24 tickUpper, uint128 liquidity, uint256 ethLP) =
            abi.decode(data, (PoolKey, int24, int24, uint128, uint256));

        (BalanceDelta delta,) = poolManager.modifyLiquidity(
            key,
            IPoolManager.ModifyLiquidityParams({
                tickLower: tickLower,
                tickUpper: tickUpper,
                liquidityDelta: int256(uint256(liquidity)),
                salt: bytes32(0)
            }),
            ""
        );

        // delta is what WE owe (negative amounts). Settle both currencies.
        int128 owed0 = delta.amount0(); // ETH
        int128 owed1 = delta.amount1(); // BBC token
        if (owed0 < 0) {
            uint256 amt0 = uint256(uint128(-owed0));
            // guard against a bad price/rounding that would exceed the raised ETH
            if (amt0 > ethLP) revert ZeroAmount();
            poolManager.settle{value: amt0}();
        }
        if (owed1 < 0) {
            uint256 amt1 = uint256(uint128(-owed1));
            poolManager.sync(Currency.wrap(address(token)));
            IERC20(address(token)).safeTransfer(address(poolManager), amt1);
            poolManager.settle();
        }
        return "";
    }

    // ================================================================ fees

    /// @notice Send accumulated 1% curve fees to the dev wallet. Permissionless.
    function claimFees() external nonReentrant {
        uint256 amount = accumulatedFees;
        if (amount == 0) revert ZeroAmount();
        accumulatedFees = 0;
        address to = feeRecipient;
        emit FeesClaimed(amount, to);
        _sendEth(to, amount);
    }

    function previewFees() external view returns (uint256) {
        return accumulatedFees;
    }

    /// @notice Sweep post-graduation dust (rounding leftovers from LP seeding)
    ///         to the fee recipient. Only after graduation; never touches curve
    ///         reserves (they're zero post-graduation) or unclaimed fees (ETH
    ///         beyond accumulatedFees only). Permissionless.
    function sweepDust() external nonReentrant {
        if (phase != Phase.GRADUATED) revert NotGraduated();
        address to = feeRecipient;
        uint256 tokenDust = IERC20(address(token)).balanceOf(address(this));
        if (tokenDust > 0) IERC20(address(token)).safeTransfer(to, tokenDust);
        uint256 ethDust = address(this).balance;
        if (ethDust > accumulatedFees) _sendEth(to, ethDust - accumulatedFees);
    }

    // =============================================================== admin

    function setFeeRecipient(address recipient_) external onlyOwner {
        if (recipient_ == address(0)) revert ZeroAddress();
        feeRecipient = recipient_;
        emit FeeRecipientUpdated(recipient_);
    }

    function setTradingPaused(bool paused_) external onlyOwner {
        tradingPaused = paused_;
        emit TradingPausedSet(paused_);
    }

    // =============================================================== views

    function currentPrice() public view returns (uint256) {
        if (tokenReserve == 0) return 0;
        return ((ethReserve + VIRTUAL_ETH_RESERVE) * 1e18) / tokenReserve;
    }

    function estimateBuy(uint256 ethIn) external view returns (uint256 tokensOut) {
        if (ethIn == 0 || phase == Phase.GRADUATED) return 0;
        uint256 netIn = (ethIn * (BPS - TRADE_FEE_BPS)) / BPS;
        uint256 capacity = GRADUATION_ETH_THRESHOLD - ethReserve;
        if (netIn > capacity) netIn = capacity;
        return (tokenReserve * netIn) / (ethReserve + VIRTUAL_ETH_RESERVE + netIn);
    }

    function estimateSell(uint256 tokenAmount) external view returns (uint256 ethOut) {
        if (tokenAmount == 0 || phase == Phase.GRADUATED) return 0;
        if (tokenReserve + tokenAmount > INITIAL_CURVE_TOKENS) return 0;
        uint256 ethGross = ((ethReserve + VIRTUAL_ETH_RESERVE) * tokenAmount) / (tokenReserve + tokenAmount);
        if (ethGross > ethReserve) ethGross = ethReserve;
        return ethGross - (ethGross * TRADE_FEE_BPS) / BPS;
    }

    function graduationProgress() external view returns (uint256 bps) {
        if (phase == Phase.GRADUATED) return BPS;
        uint256 pr = (ethReserve * BPS) / GRADUATION_ETH_THRESHOLD;
        return pr > BPS ? BPS : pr;
    }

    function getReserves() external view returns (uint256 ethRes, uint256 tokenRes) {
        return (ethReserve, tokenReserve);
    }

    function presaleCapRemaining(address user) external view returns (uint256) {
        if (livePhase() != Phase.PRESALE) return 0;
        uint256 cap = PRESALE_ETH_CAP_PER_NFT * bbcNft.balanceOf(user);
        uint256 spent = presaleSpent[user];
        return cap > spent ? cap - spent : 0;
    }

    // ============================================================ internal

    function _sendEth(address to, uint256 amount) internal {
        (bool ok,) = to.call{value: amount}("");
        if (!ok) revert EthTransferFailed();
    }

    function _poolId(PoolKey memory key) internal pure returns (bytes32) {
        return keccak256(abi.encode(key));
    }
}
Chain explorer696msChain node97ms