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
- Creator
- 0xD78EF40500…898fEe2fB2
- 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