FulcrumBondingCurveV2
0x8d5a90d30d83d0a03f0163b941c2a25ea40e3811
Verification
Verified
v0.8.35+commit.47b9dedd
Type
Contract
9,017 bytes
ABI entries
75
44 read · 9 write
License
none
Contract information
- Address
- 0x8d5a90d30d83d0a03f0163b941c2a25ea40e3811
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.35+commit.47b9dedd
- Optimization
- Enabled
- Creator
- 0xBb480C359a…c59163D94C
- Creation tx
- 0x7265dd1cff…ca301330c9
Token
- Name
- Sofia Vega
- Symbol
- SVGA
- Decimals
- 18
- Holders
- 3
- Market
- View token page →
Read contract (44)
BPS() → uint16
BUY_CREATOR_BPS() → uint16
BUY_PLATFORM_BPS() → uint16
CURVE_ALLOCATION() → uint256
DEAD() → address
GRADUATION_FEE_BPS() → uint16
NPM() → address
POOL_FEE() → uint24
RESERVE_LP2() → uint256
SELL_BURN_BPS() → uint16
SELL_CREATOR_BPS() → uint16
SELL_PLATFORM_BPS() → uint16
TICK_HIGH() → int24
TICK_LOW() → int24
TOTAL_SUPPLY() → uint256
V3_FACTORY() → address
VIRTUAL_ETH() → uint256
VIRTUAL_TOKEN_RESERVE() → uint256
WETH() → address
allowance(address, address) → uint256
balanceOf(address) → uint256
creator() → address
creatorRewardsPot() → uint256
curveState() → uint256, uint256, uint256, uint256, bool, bool
decimals() → uint8
devLockedUntilGraduation() → bool
factory() → address
graduated() → bool
graduationTarget() → uint256
lastPokeDay() → uint256
metadataHash() → bytes32
name() → string
platformWallet() → address
realEthReserve() → uint256
reserveLpDeployed() → bool
symbol() → string
tokenReserve() → uint256
tokensSold() → uint256
totalBurned() → uint256
totalSupply() → uint256
v3Pool() → address
volumeStreak() → uint8
volumeStreakDays() → uint8
volumeThresholdWei() → uint256
Events (14)
ApprovalBuyCreatorRewardsClaimedDevLockEnabledFeeBurnGraduatedGraduationTargetChangedReserveBurnedReserveLpDeployedSellTransferVirtualEthChangedVolumePokedVolumeThresholdChanged
ABI
[
{
"inputs": [
{
"components": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "bool",
"name": "lockDev",
"type": "bool"
},
{
"internalType": "address",
"name": "factory",
"type": "address"
},
{
"internalType": "uint256",
"name": "totalSupply",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "curveAllocation",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "virtualEth",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "graduationTarget",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "reserveLp2",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "volumeThresholdWei",
"type": "uint256"
},
{
"internalType": "uint8",
"name": "volumeStreakDays",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "metadataHash",
"type": "bytes32"
}
],
"internalType": "struct FulcrumBondingCurveV2.LaunchArgs",
"name": "a",
"type": "tuple"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "allowance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "ERC20InsufficientAllowance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "ERC20InsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "approver",
"type": "address"
}
],
"name": "ERC20InvalidApprover",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "receiver",
"type": "address"
}
],
"name": "ERC20InvalidReceiver",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "ERC20InvalidSender",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "ERC20InvalidSpender",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"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": "newPriceX96",
"type": "uint256"
}
],
"name": "Buy",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "burned",
"type": "uint256"
}
],
"name": "CreatorRewardsClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "DevLockEnabled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "string",
"name": "reason",
"type": "string"
}
],
"name": "FeeBurn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethSeeded",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "lpTokens",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "overhangBurned",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "platformFee",
"type": "uint256"
}
],
"name": "Graduated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newTarget",
"type": "uint256"
}
],
"name": "GraduationTargetChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "ReserveBurned",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "eth",
"type": "uint256"
}
],
"name": "ReserveLpDeployed",
"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": "newPriceX96",
"type": "uint256"
}
],
"name": "Sell",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newVE",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newVirtualTokenReserve",
"type": "uint256"
}
],
"name": "VirtualEthChanged",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "dailyVolume",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint8",
"name": "streak",
"type": "uint8"
}
],
"name": "VolumePoked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "wei_",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint8",
"name": "streak",
"type": "uint8"
}
],
"name": "VolumeThresholdChanged",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "BUY_CREATOR_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "BUY_PLATFORM_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "CURVE_ALLOCATION",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "DEAD",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "GRADUATION_FEE_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "NPM",
"outputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "RESERVE_LP2",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SELL_BURN_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SELL_CREATOR_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SELL_PLATFORM_BPS",
"outputs": [
{
"internalType": "uint16",
"name": "",
"type": "uint16"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_HIGH",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_LOW",
"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": "V3_FACTORY",
"outputs": [
{
"internalType": "contract IUniswapV3Factory",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VIRTUAL_ETH",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VIRTUAL_TOKEN_RESERVE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "WETH",
"outputs": [
{
"internalType": "contract IWETH9",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "buyFor",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "claimCreatorRewards",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "creator",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "creatorRewardsPot",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "curveState",
"outputs": [
{
"internalType": "uint256",
"name": "_realEth",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_tokenReserve",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_virtualEth",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_graduationTarget",
"type": "uint256"
},
{
"internalType": "bool",
"name": "_graduated",
"type": "bool"
},
{
"internalType": "bool",
"name": "_devLocked",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "devLockedUntilGraduation",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "graduated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "graduationTarget",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lastPokeDay",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lockDevUntilGraduation",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "metadataHash",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "platformWallet",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "realEthReserve",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "reserveLpDeployed",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokensIn",
"type": "uint256"
}
],
"name": "sell",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newVE",
"type": "uint256"
}
],
"name": "setVirtualEth",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenReserve",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokensSold",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalBurned",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "v3Pool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "volumeStreak",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "volumeStreakDays",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "volumeThresholdWei",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
// ---------------------------------------------------------------------------
// Uniswap V3 minimal interfaces (Robinhood Chain)
// ---------------------------------------------------------------------------
interface IWETH9 {
function deposit() external payable;
function approve(address spender, uint256 amount) external returns (bool);
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address) external view returns (uint256);
}
interface IUniswapV3Factory {
function createPool(address a, address b, uint24 fee) external returns (address);
function getPool(address a, address b, uint24 fee) external view returns (address);
}
interface IUniswapV3Pool {
function initialize(uint160 sqrtPriceX96) external;
function slot0() external view returns (uint160 sqrtPriceX96, int24, uint16, uint16, uint16, uint8, bool);
}
interface INonfungiblePositionManager {
struct MintParams {
address token0; address token1; uint24 fee;
int24 tickLower; int24 tickUpper;
uint256 amount0Desired; uint256 amount1Desired;
uint256 amount0Min; uint256 amount1Min;
address recipient; uint256 deadline;
}
function mint(MintParams calldata p) external payable
returns (uint256, uint128, uint256, uint256);
}
interface IFulcrumFactoryV2 {
function platformWallet() external view returns (address);
function owner() external view returns (address);
}
// ---------------------------------------------------------------------------
// FulcrumFactoryV2 — UUPS-upgradeable deployer.
// V3 semantics (in-place rewrite of the same contract name for ABI
// continuity):
// * Virtual token reserve model: curve prices with a synthetic token
// reserve so the last real token sells for the exact ETH target.
// * NEVER burns real tokens. Graduation trigger is "all curve tokens
// sold" (tokensSold == curveAllocation), not an ETH threshold.
// * Fixed LP allocation of `lpTokens` (defaults to 206.9M) + ALL raised
// ETH seeded into ONE Uniswap V3 pool at grad. No second LP, no
// volume oracle.
// * Sell fee reduced to 0.5% (0.2% creator + 0.3% platform) — no
// curve-backing burn.
//
// MAINNET BUILD — the graduation LP NFT is minted to the DEAD address
// so liquidity is permanently locked. Nobody can ever pull LP. There
// is no emergency-withdraw hatch: raised ETH only leaves the curve via
// user sells, or by being paired into the burned LP at graduation.
// ---------------------------------------------------------------------------
contract FulcrumFactoryV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
address public platformWallet;
uint256 public launchFee;
// Defaults for newly launched tokens
uint256 public defaultTotalSupply; // e.g. 1_000_000_000 ether
uint256 public defaultCurveAllocation; // e.g. 793_100_000 ether (must be < totalSupply)
uint256 public defaultVirtualEth; // e.g. 1.4 ether
uint256 public defaultGraduationTarget; // e.g. 3.92 ether — display target; also derives virtual token reserve
uint256 public defaultReserveLp2; // LP token allocation (e.g. 206.9M) — added to ALL raised ETH at grad
uint256 public defaultVolumeThresholdWei; // (unused in V3 — retained for storage layout compat)
uint8 public defaultVolumeStreakDays; // (unused in V3 — retained for storage layout compat)
event TokenLaunched(address indexed tokenAddress, address indexed creator);
event InitialBuy(address indexed tokenAddress, address indexed creator, uint256 ethIn, uint256 tokensOut);
event PlatformWalletChanged(address indexed w);
event LaunchFeeChanged(uint256 f);
event DefaultsChanged();
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() { _disableInitializers(); }
function initialize(
address _owner,
address _platform,
uint256 _launchFee,
uint256 _totalSupply,
uint256 _curveAllocation,
uint256 _virtualEth,
uint256 _graduationTarget,
uint256 _reserveLp2,
uint256 _volumeThresholdWei,
uint8 _volumeStreakDays
) external initializer {
__Ownable_init(_owner);
platformWallet = _platform;
launchFee = _launchFee;
defaultTotalSupply = _totalSupply;
defaultCurveAllocation = _curveAllocation;
defaultVirtualEth = _virtualEth;
defaultGraduationTarget = _graduationTarget;
defaultReserveLp2 = _reserveLp2;
defaultVolumeThresholdWei = _volumeThresholdWei;
defaultVolumeStreakDays = _volumeStreakDays;
}
function _authorizeUpgrade(address) internal override onlyOwner {}
function setPlatformWallet(address w) external onlyOwner {
require(w != address(0), "zero"); platformWallet = w; emit PlatformWalletChanged(w);
}
function setLaunchFee(uint256 f) external onlyOwner { launchFee = f; emit LaunchFeeChanged(f); }
/// Update defaults for FUTURE launches. Pass 0 on any field to leave it unchanged.
function setDefaults(
uint256 _totalSupply,
uint256 _curveAllocation,
uint256 _virtualEth,
uint256 _graduationTarget,
uint256 _reserveLp2,
uint256 _volumeThresholdWei,
uint8 _volumeStreakDays
) external onlyOwner {
if (_totalSupply != 0) defaultTotalSupply = _totalSupply;
if (_curveAllocation != 0) defaultCurveAllocation = _curveAllocation;
if (_virtualEth != 0) defaultVirtualEth = _virtualEth;
if (_graduationTarget != 0) defaultGraduationTarget = _graduationTarget;
if (_reserveLp2 != 0) defaultReserveLp2 = _reserveLp2;
if (_volumeThresholdWei != 0) defaultVolumeThresholdWei = _volumeThresholdWei;
if (_volumeStreakDays != 0) defaultVolumeStreakDays = _volumeStreakDays;
emit DefaultsChanged();
}
// Per-token owner-only overrides (proxy calls)
function setTokenGraduationTarget(address, uint256) external view onlyOwner { revert("disabled in v3"); }
function setTokenVolumeThreshold(address, uint256, uint8) external view onlyOwner { revert("disabled in v3"); }
function pokeVolume(address, uint256) external view onlyOwner { revert("disabled in v3"); }
function forceDeployReserveLP(address) external payable onlyOwner { revert("disabled in v3"); }
function burnReserveLP(address) external view onlyOwner { revert("disabled in v3"); }
function launchToken(string memory name, string memory symbol, bool lockDev, bytes32 salt, bytes32 metadataHash)
external payable returns (address) { return _launch(name, symbol, lockDev, salt, metadataHash); }
function launchTokenAndBuy(string memory name, string memory symbol, bool lockDev, bytes32 salt, bytes32 metadataHash)
external payable returns (address token)
{
token = _launch(name, symbol, lockDev, salt, metadataHash);
uint256 buyAmount = msg.value - launchFee;
if (buyAmount > 0) {
uint256 out = FulcrumBondingCurveV2(payable(token)).buyFor{value: buyAmount}(msg.sender);
emit InitialBuy(token, msg.sender, buyAmount, out);
}
}
function _launch(string memory name, string memory symbol, bool lockDev, bytes32 salt, bytes32 metadataHash) internal returns (address) {
require(msg.value >= launchFee, "Pay launch fee");
require(defaultGraduationTarget > 0, "target unset");
(bool ok, ) = payable(platformWallet).call{value: launchFee}("");
require(ok, "fee xfer");
FulcrumBondingCurveV2.LaunchArgs memory a = FulcrumBondingCurveV2.LaunchArgs({
name: name, symbol: symbol, creator: msg.sender, lockDev: lockDev, factory: address(this),
totalSupply: defaultTotalSupply,
curveAllocation: defaultCurveAllocation,
virtualEth: defaultVirtualEth,
graduationTarget: defaultGraduationTarget,
reserveLp2: defaultReserveLp2,
volumeThresholdWei: defaultVolumeThresholdWei,
volumeStreakDays: defaultVolumeStreakDays,
metadataHash: metadataHash
});
FulcrumBondingCurveV2 t = new FulcrumBondingCurveV2{salt: salt}(a);
emit TokenLaunched(address(t), msg.sender);
return address(t);
}
function predictTokenAddress(
string memory name, string memory symbol, address creator, bool lockDev, bytes32 salt, bytes32 metadataHash
) external view returns (address) {
FulcrumBondingCurveV2.LaunchArgs memory a = FulcrumBondingCurveV2.LaunchArgs({
name: name, symbol: symbol, creator: creator, lockDev: lockDev, factory: address(this),
totalSupply: defaultTotalSupply,
curveAllocation: defaultCurveAllocation,
virtualEth: defaultVirtualEth,
graduationTarget: defaultGraduationTarget,
reserveLp2: defaultReserveLp2,
volumeThresholdWei: defaultVolumeThresholdWei,
volumeStreakDays: defaultVolumeStreakDays,
metadataHash: metadataHash
});
bytes memory bytecode = abi.encodePacked(
type(FulcrumBondingCurveV2).creationCode,
abi.encode(a)
);
bytes32 h = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, keccak256(bytecode)));
return address(uint160(uint256(h)));
}
}
// ---------------------------------------------------------------------------
// FulcrumBondingCurveV2 — V3 semantics
//
// Pricing uses a *virtual* token reserve derived from the pair
// (curveAllocation, virtualEth, graduationTarget) so that:
// VIRTUAL_TOKEN_RESERVE = curveAllocation * (virtualEth + graduationTarget)
// / graduationTarget
//
// At tokensSold == curveAllocation the real ETH raised equals
// graduationTarget exactly, and the final curve price matches the price
// implied by (lpTokens, ETH raised) → LP opens without any handoff dump.
//
// - No burns. Real supply is fixed at TOTAL_SUPPLY (1B) forever.
// - Graduation triggers when the FULL curveAllocation has been sold
// (not on an ETH threshold), so the curve always ends with 0 tokens
// left for sale on the curve.
// - At graduation, all raised ETH (minus 0.5% platform fee) is paired
// with the fixed `LP_TOKENS` allocation into a single Uniswap V3 pool.
// The LP NFT is minted to platformWallet() (test build).
// - Sell fee: 0.5% total = 0.2% creator + 0.3% platform. No curve-backing burn.
// ---------------------------------------------------------------------------
contract FulcrumBondingCurveV2 is ERC20 {
struct LaunchArgs {
string name; string symbol; address creator; bool lockDev; address factory;
uint256 totalSupply;
uint256 curveAllocation;
uint256 virtualEth;
uint256 graduationTarget;
uint256 reserveLp2;
uint256 volumeThresholdWei;
uint8 volumeStreakDays;
bytes32 metadataHash;
}
// -------- Constants (fees, addresses) --------
uint16 public constant BUY_CREATOR_BPS = 10; // 0.1%
uint16 public constant BUY_PLATFORM_BPS = 20; // 0.2%
uint16 public constant SELL_CREATOR_BPS = 20; // 0.2%
uint16 public constant SELL_PLATFORM_BPS = 30; // 0.3%
uint16 public constant SELL_BURN_BPS = 0; // removed in V3
uint16 public constant GRADUATION_FEE_BPS= 50; // 0.5% ETH
uint16 public constant BPS = 10_000;
IWETH9 public constant WETH = IWETH9(0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73);
IUniswapV3Factory public constant V3_FACTORY =
IUniswapV3Factory(0x1f7d7550B1b028f7571E69A784071F0205FD2EfA);
INonfungiblePositionManager public constant NPM =
INonfungiblePositionManager(0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3);
uint24 public constant POOL_FEE = 100; // 0.01% tier, tickSpacing 1
int24 public constant TICK_LOW = -887272;
int24 public constant TICK_HIGH = 887272;
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
// -------- Per-token immutables --------
address public immutable creator;
address public immutable factory;
bytes32 public immutable metadataHash;
uint256 public immutable TOTAL_SUPPLY;
uint256 public immutable CURVE_ALLOCATION;
/// Mutable so factory owner can retune pricing BEFORE any buy happens
/// (e.g. ETH price swings between deploy and first trade). Locked after
/// the first buy — see setVirtualEth().
uint256 public VIRTUAL_ETH;
/// Fixed LP token allocation seeded at graduation (paired with all raised ETH).
uint256 public immutable RESERVE_LP2;
/// Virtual token reserve used only for AMM pricing. Never minted; never burned.
/// Recomputed alongside VIRTUAL_ETH via setVirtualEth().
uint256 public VIRTUAL_TOKEN_RESERVE;
// -------- Mutable state --------
/// Kept for ABI/UI compat. Represents the ETH raised when the FULL
/// curveAllocation has been sold (i.e. the intrinsic ETH the curve
/// yields at grad). In V3 the actual trigger is tokensSold == curveAllocation.
uint256 public graduationTarget;
bool public devLockedUntilGraduation;
bool public graduated;
address public v3Pool;
uint256 public realEthReserve;
/// Virtual token side of the AMM invariant (starts at VIRTUAL_TOKEN_RESERVE
/// and decreases with buys). Exposed under the legacy name for UI compat.
uint256 public tokenReserve;
/// Real tokens actually transferred out of the curve to buyers.
/// Capped at CURVE_ALLOCATION. Graduation triggers when it hits the cap.
uint256 public tokensSold;
uint256 public creatorRewardsPot;
uint256 public totalBurned; // stays 0 in V3, kept for ABI compat
// Legacy fields — unused in V3, retained for storage-layout / ABI compat
uint256 public volumeThresholdWei;
uint8 public volumeStreakDays;
uint8 public volumeStreak;
uint256 public lastPokeDay;
bool public reserveLpDeployed;
event Buy(address indexed buyer, uint256 ethIn, uint256 tokensOut, uint256 newPriceX96);
event Sell(address indexed seller, uint256 tokensIn, uint256 ethOut, uint256 newPriceX96);
event CreatorRewardsClaimed(address indexed creator, uint256 amount, uint256 burned);
event FeeBurn(uint256 amount, string reason);
event Graduated(address indexed pool, uint256 ethSeeded, uint256 lpTokens, uint256 overhangBurned, uint256 platformFee);
event DevLockEnabled();
event GraduationTargetChanged(uint256 newTarget);
event VolumeThresholdChanged(uint256 wei_, uint8 streak);
event VolumePoked(uint256 dailyVolume, uint8 streak);
event ReserveLpDeployed(uint256 tokens, uint256 eth);
event ReserveBurned(uint256 amount);
event VirtualEthChanged(uint256 newVE, uint256 newVirtualTokenReserve);
constructor(LaunchArgs memory a) ERC20(a.name, a.symbol) {
require(a.graduationTarget > 0, "target=0");
require(a.virtualEth > 0, "virtual=0");
require(a.totalSupply > 0, "supply=0");
require(a.curveAllocation > 0 && a.reserveLp2 > 0
&& a.curveAllocation + a.reserveLp2 <= a.totalSupply, "alloc");
creator = a.creator;
factory = a.factory;
devLockedUntilGraduation = a.lockDev;
metadataHash = a.metadataHash;
TOTAL_SUPPLY = a.totalSupply;
CURVE_ALLOCATION = a.curveAllocation;
VIRTUAL_ETH = a.virtualEth;
RESERVE_LP2 = a.reserveLp2;
graduationTarget = a.graduationTarget;
volumeThresholdWei = a.volumeThresholdWei;
volumeStreakDays = a.volumeStreakDays;
// VIRTUAL_TOKEN_RESERVE = curveAllocation * (virtualEth + graduationTarget) / graduationTarget
// With this, at tokensSold == curveAllocation, realEth raised == graduationTarget exactly.
VIRTUAL_TOKEN_RESERVE = (a.curveAllocation * (a.virtualEth + a.graduationTarget)) / a.graduationTarget;
tokenReserve = VIRTUAL_TOKEN_RESERVE;
_mint(address(this), a.totalSupply);
}
function platformWallet() public view returns (address) {
return IFulcrumFactoryV2(factory).platformWallet();
}
/// Factory owner can retune the virtual ETH reserve BEFORE any buy has
/// happened. This exists so pricing can be adjusted if ETH's USD price
/// swings materially between deploy and first trade. Locked forever
/// after the first buy so live traders are never repriced.
function setVirtualEth(uint256 newVE) external {
require(msg.sender == IFulcrumFactoryV2(factory).owner(), "not owner");
require(!graduated, "graduated");
require(tokensSold == 0, "trading started");
require(newVE > 0, "zero");
VIRTUAL_ETH = newVE;
VIRTUAL_TOKEN_RESERVE = (CURVE_ALLOCATION * (newVE + graduationTarget)) / graduationTarget;
tokenReserve = VIRTUAL_TOKEN_RESERVE;
emit VirtualEthChanged(newVE, VIRTUAL_TOKEN_RESERVE);
}
// ============ BUY ============
receive() external payable { _buy(msg.sender); }
function buyFor(address to) external payable returns (uint256) { return _buy(to); }
function _buy(address to) internal returns (uint256 tokensOut) {
require(!graduated, "Graduated");
require(msg.value > 0, "No ETH");
uint256 creatorFee = (msg.value * BUY_CREATOR_BPS) / BPS;
uint256 platformFee = (msg.value * BUY_PLATFORM_BPS) / BPS;
uint256 ethAfterFee = msg.value - creatorFee - platformFee;
uint256 xVirt = VIRTUAL_ETH + realEthReserve;
uint256 k = xVirt * tokenReserve;
uint256 newX = xVirt + ethAfterFee;
uint256 newY = k / newX;
tokensOut = tokenReserve - newY;
require(tokensOut > 0, "Zero out");
// Cap at remaining real curve allocation; refund excess ETH.
uint256 remaining = CURVE_ALLOCATION - tokensSold;
require(remaining > 0, "Curve full");
uint256 refund = 0;
if (tokensOut > remaining) {
// Solve for exact ETH needed for `remaining` tokens: newY_target = tokenReserve - remaining.
uint256 targetY = tokenReserve - remaining;
uint256 targetX = k / targetY;
uint256 ethNeeded = targetX - xVirt;
// Re-derive fees from ethNeeded so the invariant holds exactly.
uint256 grossNeeded = (ethNeeded * BPS) / (BPS - BUY_CREATOR_BPS - BUY_PLATFORM_BPS) + 1;
if (grossNeeded > msg.value) grossNeeded = msg.value;
creatorFee = (grossNeeded * BUY_CREATOR_BPS) / BPS;
platformFee = (grossNeeded * BUY_PLATFORM_BPS) / BPS;
ethAfterFee = grossNeeded - creatorFee - platformFee;
refund = msg.value - grossNeeded;
tokensOut = remaining;
}
realEthReserve += ethAfterFee;
tokenReserve -= tokensOut;
tokensSold += tokensOut;
creatorRewardsPot += creatorFee;
(bool okP, ) = payable(platformWallet()).call{value: platformFee}("");
require(okP, "plat xfer");
if (refund > 0) {
(bool okR, ) = payable(to).call{value: refund}("");
require(okR, "refund xfer");
}
_transfer(address(this), to, tokensOut);
emit Buy(to, msg.value, tokensOut, _priceX96());
if (!graduated && tokensSold >= CURVE_ALLOCATION) {
try this.graduate() { } catch { }
}
}
// ============ SELL ============
function sell(uint256 tokensIn) external returns (uint256 ethOut) {
require(!graduated, "Graduated");
require(tokensIn > 0, "Zero");
if (msg.sender == creator && devLockedUntilGraduation) revert("Dev locked until graduation");
require(balanceOf(msg.sender) >= tokensIn, "Bal");
require(tokensIn <= tokensSold, "Exceeds sold");
uint256 xVirt = VIRTUAL_ETH + realEthReserve;
uint256 k = xVirt * tokenReserve;
uint256 newY = tokenReserve + tokensIn;
uint256 newX = k / newY;
uint256 ethOutGross = xVirt - newX;
require(ethOutGross > 0 && realEthReserve > 1, "Curve");
if (ethOutGross >= realEthReserve) ethOutGross = realEthReserve - 1;
uint256 creatorFee = (ethOutGross * SELL_CREATOR_BPS) / BPS;
uint256 platformFee = (ethOutGross * SELL_PLATFORM_BPS) / BPS;
ethOut = ethOutGross - creatorFee - platformFee;
tokenReserve += tokensIn;
tokensSold -= tokensIn;
realEthReserve -= (ethOut + creatorFee + platformFee);
_transfer(msg.sender, address(this), tokensIn);
creatorRewardsPot += creatorFee;
(bool okP, ) = payable(platformWallet()).call{value: platformFee}("");
require(okP, "plat xfer");
(bool s, ) = payable(msg.sender).call{value: ethOut}("");
require(s, "eth xfer");
emit Sell(msg.sender, tokensIn, ethOut, _priceX96());
}
// ============ CREATOR CLAIM ============
function claimCreatorRewards() external {
require(msg.sender == creator, "Not creator");
uint256 amt = creatorRewardsPot;
require(amt > 0, "Nothing");
creatorRewardsPot = 0;
(bool ok, ) = payable(msg.sender).call{value: amt}("");
require(ok, "claim xfer");
emit CreatorRewardsClaimed(msg.sender, amt, 0);
}
function _update(address from, address to, uint256 value) internal override {
if (from == creator && devLockedUntilGraduation && !graduated
&& from != address(0) && to != address(this) && to != address(0)) {
revert("Dev tokens locked until graduation");
}
super._update(from, to, value);
}
function lockDevUntilGraduation() external {
require(msg.sender == creator, "Not creator");
require(!graduated, "Graduated");
devLockedUntilGraduation = true;
emit DevLockEnabled();
}
// ============ GRADUATION (V3 — fixed LP alloc, no burns) ============
function graduate() external {
require(!graduated, "Already");
require(tokensSold >= CURVE_ALLOCATION, "Curve not sold out");
graduated = true;
uint256 ethPot = realEthReserve;
realEthReserve = 0;
tokenReserve = 0;
// 0.5% grad fee in ETH
uint256 platformFee = (ethPot * GRADUATION_FEE_BPS) / BPS;
(bool okFee, ) = payable(platformWallet()).call{value: platformFee}("");
require(okFee, "plat xfer");
uint256 ethForLP = ethPot - platformFee;
// Fixed LP allocation (206.9M). Contract still holds it — never touched during the curve.
uint256 lpTokens = RESERVE_LP2;
require(balanceOf(address(this)) >= lpTokens, "lp bal");
WETH.deposit{value: ethForLP}();
address tokenA = address(this);
address tokenB = address(WETH);
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
(uint256 amount0, uint256 amount1) = tokenA < tokenB
? (lpTokens, ethForLP)
: (ethForLP, lpTokens);
address pool = V3_FACTORY.getPool(token0, token1, POOL_FEE);
if (pool == address(0)) pool = V3_FACTORY.createPool(token0, token1, POOL_FEE);
v3Pool = pool;
IUniswapV3Pool(pool).initialize(_sqrtPriceX96(amount0, amount1));
_approve(address(this), address(NPM), lpTokens);
IWETH9(address(WETH)).approve(address(NPM), ethForLP);
INonfungiblePositionManager.MintParams memory p = INonfungiblePositionManager.MintParams({
token0: token0, token1: token1, fee: POOL_FEE,
tickLower: TICK_LOW, tickUpper: TICK_HIGH,
amount0Desired: amount0, amount1Desired: amount1,
amount0Min: 0, amount1Min: 0,
recipient: DEAD, deadline: block.timestamp + 300
});
NPM.mint(p);
emit Graduated(pool, ethForLP, lpTokens, 0, platformFee);
}
// ============ VIEWS ============
function _priceX96() internal view returns (uint256) {
if (tokenReserve == 0) return 0;
uint256 ratio = ((VIRTUAL_ETH + realEthReserve) * 1e18) / tokenReserve;
return _sqrt(ratio) * (1 << 48);
}
function _sqrtPriceX96(uint256 amount0, uint256 amount1) internal pure returns (uint160) {
uint256 num = amount1 * (1 << 96);
uint256 ratio = num / amount0;
return uint160(_sqrt(ratio) * (1 << 48));
}
function _sqrt(uint256 x) internal pure returns (uint256 y) {
if (x == 0) return 0;
uint256 z = (x + 1) / 2; y = x;
while (z < y) { y = z; z = (x / z + z) / 2; }
}
function curveState() external view returns (
uint256 _realEth, uint256 _tokenReserve, uint256 _virtualEth,
uint256 _graduationTarget, bool _graduated, bool _devLocked
) {
return (realEthReserve, tokenReserve, VIRTUAL_ETH, graduationTarget, graduated, devLockedUntilGraduation);
}
}Chain explorer1319msChain node79ms