LivoLaunchpad
0xfd550c5dc070ea575a06a40f2e18304d85211663
Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
8,141 bytes
ABI entries
55
15 read · 10 write
License
none
Contract information
- Address
- 0xfd550c5dc070ea575a06a40f2e18304d85211663
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.28+commit.7893614a
- Optimization
- Enabled
- Creator
- 0xBa489180Ea…15F388f181
- Creation tx
- 0x0e2f6c48f6…7287e9efa8
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (15)
BASIS_POINTS() → uint256
VERSION() → string
getMaxEthToSpend(address) → uint256
getTokenConfig(address) → tuple
getTokenState(address) → tuple
owner() → address
pendingOwner() → address
quoteBuyExactTokens(address, uint256) → uint256, uint256, uint256, bool
quoteBuyTokensWithExactEth(address, uint256) → uint256, uint256, uint256, bool
quoteSellExactTokens(address, uint256) → uint256, uint256, uint256
quoteSellTokensForExactEth(address, uint256) → uint256, uint256, uint256
tokenConfigs(address) → address
tokenStates(address) → uint256, uint256, bool
treasury() → address
whitelistedFactories(address) → bool
Events (12)
CommunityTakeOverCreatorTaxesAccruedFactoryBlacklistedFactoryWhitelistedLivoTokenBuyLivoTokenSellLpFeesAccruedOwnershipTransferStartedOwnershipTransferredTokenGraduatedTokenLaunchedTreasuryAddressUpdated
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_treasury",
"type": "address"
},
{
"internalType": "address",
"name": "_owner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyConfigured",
"type": "error"
},
{
"inputs": [],
"name": "AlreadyGraduated",
"type": "error"
},
{
"inputs": [],
"name": "DeadlineExceeded",
"type": "error"
},
{
"inputs": [],
"name": "EthTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientEthReserves",
"type": "error"
},
{
"inputs": [],
"name": "InvalidAddress",
"type": "error"
},
{
"inputs": [],
"name": "InvalidAmount",
"type": "error"
},
{
"inputs": [],
"name": "InvalidLaunchpadFee",
"type": "error"
},
{
"inputs": [],
"name": "InvalidToken",
"type": "error"
},
{
"inputs": [],
"name": "NotEnoughSupply",
"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": "ReceivingZeroAmount",
"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": "UnauthorizedFactory",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "CommunityTakeOver",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "CreatorTaxesAccrued",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "factory",
"type": "address"
}
],
"name": "FactoryBlacklisted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "factory",
"type": "address"
}
],
"name": "FactoryWhitelisted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "buyer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethFee",
"type": "uint256"
}
],
"name": "LivoTokenBuy",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "seller",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethFee",
"type": "uint256"
}
],
"name": "LivoTokenSell",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "creatorShare",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "treasuryShare",
"type": "uint256"
}
],
"name": "LpFeesAccrued",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethCollected",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensForGraduation",
"type": "uint256"
}
],
"name": "TokenGraduated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "graduationThreshold",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "maxExcessOverThreshold",
"type": "uint256"
}
],
"name": "TokenLaunched",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "newTreasury",
"type": "address"
}
],
"name": "TreasuryAddressUpdated",
"type": "event"
},
{
"inputs": [],
"name": "BASIS_POINTS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "VERSION",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "factory",
"type": "address"
}
],
"name": "blacklistFactory",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "minTokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "buyTokensWithExactEth",
"outputs": [
{
"internalType": "uint256",
"name": "receivedTokens",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newTokenOwner",
"type": "address"
}
],
"name": "communityTakeOver",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "getMaxEthToSpend",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "getTokenConfig",
"outputs": [
{
"components": [
{
"internalType": "contract ILivoBondingCurve",
"name": "bondingCurve",
"type": "address"
}
],
"internalType": "struct TokenConfig",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "getTokenState",
"outputs": [
{
"components": [
{
"internalType": "uint256",
"name": "ethCollected",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "releasedSupply",
"type": "uint256"
},
{
"internalType": "bool",
"name": "graduated",
"type": "bool"
}
],
"internalType": "struct TokenState",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "contract ILivoBondingCurve",
"name": "bondingCurve",
"type": "address"
}
],
"name": "launchToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"name": "quoteBuyExactTokens",
"outputs": [
{
"internalType": "uint256",
"name": "ethFee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethForReserves",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "totalEthNeeded",
"type": "uint256"
},
{
"internalType": "bool",
"name": "canGraduate",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "ethValue",
"type": "uint256"
}
],
"name": "quoteBuyTokensWithExactEth",
"outputs": [
{
"internalType": "uint256",
"name": "ethForPurchase",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethFee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokensToReceive",
"type": "uint256"
},
{
"internalType": "bool",
"name": "canGraduate",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
}
],
"name": "quoteSellExactTokens",
"outputs": [
{
"internalType": "uint256",
"name": "ethPulledFromReserves",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethFee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethForSeller",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "ethAmount",
"type": "uint256"
}
],
"name": "quoteSellTokensForExactEth",
"outputs": [
{
"internalType": "uint256",
"name": "ethPulledFromReserves",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "ethFee",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokensRequired",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minEthAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "sellExactTokens",
"outputs": [
{
"internalType": "uint256",
"name": "receivedEth",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "setTreasuryAddress",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenConfigs",
"outputs": [
{
"internalType": "contract ILivoBondingCurve",
"name": "bondingCurve",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenStates",
"outputs": [
{
"internalType": "uint256",
"name": "ethCollected",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "releasedSupply",
"type": "uint256"
},
{
"internalType": "bool",
"name": "graduated",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "factory",
"type": "address"
}
],
"name": "whitelistFactory",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "factory",
"type": "address"
}
],
"name": "whitelistedFactories",
"outputs": [
{
"internalType": "bool",
"name": "authorized",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable, Ownable2Step} from "lib/openzeppelin-contracts/contracts/access/Ownable2Step.sol";
import {ReentrancyGuardTransient} from "lib/openzeppelin-contracts/contracts/utils/ReentrancyGuardTransient.sol";
import {ILivoToken} from "src/interfaces/ILivoToken.sol";
import {ILivoBondingCurve} from "src/interfaces/ILivoBondingCurve.sol";
import {ILivoGraduator} from "src/interfaces/ILivoGraduator.sol";
import {ILivoLaunchpad2} from "src/interfaces/ILivoLaunchpad2.sol";
import {TokenConfig, TokenState, TokenDataLib} from "src/types/tokenData.sol";
contract LivoLaunchpad is ILivoLaunchpad2, Ownable2Step, ReentrancyGuardTransient {
using SafeERC20 for IERC20;
using TokenDataLib for TokenConfig;
using TokenDataLib for TokenState;
/// @notice Version of the Livo stack this contract belongs to
string public constant VERSION = "2.0";
/// @notice Authorized factories
mapping(address factory => bool authorized) public whitelistedFactories;
/// @notice Defensive per-trade cap on the pre-graduation fee (LP fee + tax)
uint256 internal constant MAX_TRADING_FEE_BPS = 2_500; // 25%
/// @notice 100% in basis points
uint256 public constant BASIS_POINTS = 10_000;
/// @notice Livo Treasury, receiver of the treasury share of trading fees and of graduation fees
address public treasury;
/// @notice Mapping of token address to its configuration
mapping(address => TokenConfig) public tokenConfigs;
/// @notice Mapping of token address to its state variables
mapping(address => TokenState) public tokenStates;
///////////////////// Errors /////////////////////
error InvalidAddress();
error InvalidAmount();
error ReceivingZeroAmount();
error InvalidLaunchpadFee();
error InvalidToken();
error NotEnoughSupply();
error AlreadyGraduated();
error InsufficientEthReserves();
error EthTransferFailed();
error DeadlineExceeded();
error SlippageExceeded();
error AlreadyConfigured();
error UnauthorizedFactory();
///////////////////// Events /////////////////////
event TokenLaunched(address indexed token, uint256 graduationThreshold, uint256 maxExcessOverThreshold);
event TokenGraduated(address indexed token, uint256 ethCollected, uint256 tokensForGraduation);
event LivoTokenBuy(
address indexed token, address indexed buyer, uint256 ethAmount, uint256 tokenAmount, uint256 ethFee
);
event LivoTokenSell(
address indexed token, address indexed seller, uint256 tokenAmount, uint256 ethAmount, uint256 ethFee
);
/// @notice LP/trading fee split for a pre-graduation trade — creator share routed via the token's
/// `accrueFees`, treasury share pushed. Mirrors `LivoSwapHook.LpFeesAccrued` so pre- and
/// post-graduation fees aggregate identically.
event LpFeesAccrued(address indexed token, uint256 creatorShare, uint256 treasuryShare);
/// @notice Creator tax taken on a pre-graduation trade (100% to the creator via `accrueFees`).
/// Mirrors `LivoSwapHook.CreatorTaxesAccrued`. Emitted only when the tax is non-zero.
event CreatorTaxesAccrued(address indexed token, uint256 amount);
event TreasuryAddressUpdated(address newTreasury);
event CommunityTakeOver(address indexed token, address newOwner);
event FactoryWhitelisted(address indexed factory);
event FactoryBlacklisted(address indexed factory);
////////////////// MODIFIERS ///////////////////////
modifier onlyWhitelistedFactory() {
_onlyWhitelistedFactory();
_;
}
/////////////////////////////////////////////////
/// @param _treasury Address of the treasury to receive fees
/// @param _owner Address of the contract owner
constructor(address _treasury, address _owner) Ownable(_owner) {
// Set initial values directly (not via onlyOwner setters) to support CREATE2 deployment
require(_treasury != address(0), InvalidAddress());
treasury = _treasury;
emit TreasuryAddressUpdated(_treasury);
}
/// @notice Registers a new token in the launchpad with its bonding curve, callable only by whitelisted factories
function launchToken(address token, ILivoBondingCurve bondingCurve) external onlyWhitelistedFactory {
// this check is important because bondingCurve!=address(0) is used as proxy for valid existing tokens within the Launchpad
// the msg.sender is a trusted factory, which should have a valid bonding curve compliant with IBondingCurve
require(address(bondingCurve) != address(0), InvalidAddress());
// the token config (just the bonding curve) becomes immutable once set here. Pre-graduation
// trading fees are no longer snapshotted here — they are read per-trade from the token itself.
tokenConfigs[token] = TokenConfig({bondingCurve: bondingCurve});
ILivoBondingCurve.GraduationConfig memory gradConfig = bondingCurve.getGraduationConfig();
emit TokenLaunched(token, gradConfig.ethGraduationThreshold, gradConfig.maxExcessOverThreshold);
}
/// @notice Buys tokens with exact ETH amount
/// @dev The user sends ETH via msg.value and receives tokens based on the bonding curve price
/// @dev 1% fee deducted from ETH amount.
/// @dev Slippage control is done with minTokenAmount.
/// @dev Purchases can trigger graduation if threshold reached
/// @dev Cannot buy a token that has already graduated (use uniswap instead)
/// @param token Address of the token to buy
/// @param minTokenAmount Minimum amount of tokens to receive (slippage protection)
/// @param deadline Unix timestamp after which transaction will revert
/// @return receivedTokens Amount of tokens received
function buyTokensWithExactEth(address token, uint256 minTokenAmount, uint256 deadline)
external
payable
nonReentrant
returns (uint256 receivedTokens)
{
TokenState storage tokenState = tokenStates[token];
require(msg.value > 0, InvalidAmount());
require(block.timestamp <= deadline, DeadlineExceeded());
require(tokenConfigs[token].exists(), InvalidToken());
require(tokenState.notGraduated(), AlreadyGraduated());
// pre-graduation fee policy is read from the token itself and capped by the launchpad
(uint16 totalFeeBps, uint16 lpFeeBps, uint16 treasuryShareBps) = _readLaunchpadFees(token, true);
// this call to bonding curve reverts if exceeds graduation margins
// The internal function also reverts with NotEnoughSupply if exceeding this contract token balance
(uint256 ethForReserves, uint256 ethFee, uint256 tokensToReceive, bool canGraduate) =
_quoteBuyTokensWithExactEth(token, msg.value, totalFeeBps);
require(tokensToReceive > 0, ReceivingZeroAmount());
require(tokensToReceive >= minTokenAmount, SlippageExceeded());
tokenState.ethCollected += ethForReserves;
tokenState.releasedSupply += tokensToReceive;
// WARNING: fee-on-transfer / rebasing tokens are NOT supported and must never be launched here. The
// launchpad assumes the buyer receives exactly `tokensToReceive`. A token that skims on transfer would
// silently under-deliver (defeating the minTokenAmount slippage check above) and drift inventory.
IERC20(token).safeTransfer(msg.sender, tokensToReceive);
// split the fee: LP fee between treasury (push) and creator (accrueFees); tax (the remainder) fully to creator
uint256 lpFee = (msg.value * lpFeeBps) / BASIS_POINTS;
// tax = ethFee - lpFee
_settleFee(token, lpFee, ethFee - lpFee, treasuryShareBps);
// Fee breakdown (lpFee/tax/split) is intentionally NOT included here: it is emitted in the same tx
// via LpFeesAccrued/CreatorTaxesAccrued, which mirror LivoSwapHook so the indexer aggregates pre- and
// post-graduation fees through one code path.
emit LivoTokenBuy(token, msg.sender, msg.value, tokensToReceive, ethFee);
// if the graduation criteria is met, graduation happens automatically
if (canGraduate) {
_graduateToken(token, tokenState);
}
return tokensToReceive;
}
/// @notice Sells an exact amount of tokens back and receives ETH based on the bonding curve price.
/// @dev 1% fee deducted from ETH received.
/// @dev Slippage control is done with minEthAmount (min eth willing to receive).
/// @dev Cannot sell tokens that have been graduated (use uniswap instead)
/// @dev Even if minEthAmount==0, receiving 0 eth is not allowed and the transaction reverts
/// @param token Address of the token to sell
/// @param tokenAmount Amount of tokens to sell
/// @param minEthAmount Minimum amount of ETH to receive (slippage protection)
/// @param deadline Unix timestamp after which transaction will revert
/// @return receivedEth Amount of ETH received
function sellExactTokens(address token, uint256 tokenAmount, uint256 minEthAmount, uint256 deadline)
external
nonReentrant
returns (uint256 receivedEth)
{
TokenState storage tokenState = tokenStates[token];
require(tokenAmount > 0, InvalidAmount());
require(block.timestamp <= deadline, DeadlineExceeded());
require(tokenConfigs[token].exists(), InvalidToken());
require(tokenState.notGraduated(), AlreadyGraduated());
// pre-graduation fee policy is read from the token itself and capped by the launchpad
(uint16 totalFeeBps, uint16 lpFeeBps, uint16 treasuryShareBps) = _readLaunchpadFees(token, false);
// reverts with InsufficientEthReserves if seller would receive more than eth reserves allocated to the token
// that scenario should never happen, it is an extra cautious measure
(uint256 ethPulledFromReserves, uint256 ethFee, uint256 ethForSeller) =
_quoteSellExactTokens(token, tokenAmount, totalFeeBps);
require(ethForSeller >= minEthAmount, SlippageExceeded());
// When minEthAmount==0, we assume that the seller accepts any kind of "reasonable" slippage
// However, receiving eth in exchange for a non-zero amount of tokens would be unfair
require(ethForSeller > 0, ReceivingZeroAmount());
tokenState.ethCollected -= ethPulledFromReserves;
tokenState.releasedSupply -= tokenAmount;
// funds transfers
// WARNING: fee-on-transfer / rebasing tokens are NOT supported and must never be launched here. The
// launchpad pays ETH for the full `tokenAmount` but assumes it receives exactly that many tokens back.
// A token that skims on transfer would silently leak launchpad inventory (eventually bricking buys and
// graduation, where the launchpad's token balance no longer matches its ETH-side accounting).
IERC20(token).safeTransferFrom(msg.sender, address(this), tokenAmount);
// split the fee: LP fee between treasury (push) and creator (accrueFees); tax (the remainder) fully to creator
uint256 lpFee = (ethPulledFromReserves * lpFeeBps) / BASIS_POINTS;
// tax = ethFee - lpFee
_settleFee(token, lpFee, ethFee - lpFee, treasuryShareBps);
// Emitted after the fee events (LpFeesAccrued/CreatorTaxesAccrued in _settleFee) so the
// pre-graduation event order matches buys and the post-graduation LivoSwapHook: fee events
// first, trade event last. Fee breakdown is intentionally NOT included here (see buy).
emit LivoTokenSell(token, msg.sender, tokenAmount, ethForSeller, ethFee);
_transferEth(msg.sender, ethForSeller, true);
return ethForSeller;
}
//////////////////////////// view functions //////////////////////////
/// @notice Quotes the result of buying tokens with exact ETH amount
/// @param token Address of the token to quote
/// @param ethValue Amount of ETH to spend
/// @return ethForPurchase Amount of ETH used effectively for purchase (after fees)
/// @return ethFee Fee amount in ETH
/// @return tokensToReceive Amount of tokens that would be received
/// @return canGraduate True if this purchase would trigger graduation
function quoteBuyTokensWithExactEth(address token, uint256 ethValue)
external
view
returns (uint256 ethForPurchase, uint256 ethFee, uint256 tokensToReceive, bool canGraduate)
{
if (!tokenConfigs[token].exists()) revert InvalidToken();
(uint16 totalFeeBps,,) = _readLaunchpadFees(token, true);
// this reverts with NotEnoughSupply if attempting to purchase more than this contract's balance
(ethForPurchase, ethFee, tokensToReceive, canGraduate) =
_quoteBuyTokensWithExactEth(token, ethValue, totalFeeBps);
}
/// @notice Quotes the result of selling exact amount of tokens
/// @param token Address of the token to quote
/// @param tokenAmount Amount of tokens to sell
/// @return ethPulledFromReserves Amount of ETH pulled from the reserves before fees are applied
/// @return ethFee Fee amount in ETH
/// @return ethForSeller Amount of ETH the seller would receive
function quoteSellExactTokens(address token, uint256 tokenAmount)
external
view
returns (uint256 ethPulledFromReserves, uint256 ethFee, uint256 ethForSeller)
{
if (!tokenConfigs[token].exists()) revert InvalidToken();
(uint16 totalFeeBps,,) = _readLaunchpadFees(token, false);
// reverts with InsufficientEthReserves if seller would receive more than eth reserves allocated to the token
// that scenario should never happen, it is an extra cautious measure
(ethPulledFromReserves, ethFee, ethForSeller) = _quoteSellExactTokens(token, tokenAmount, totalFeeBps);
}
/// @notice Quotes how much total ETH is needed to buy an exact amount of tokens
/// @param token Address of the token to quote
/// @param tokenAmount Amount of tokens to buy
/// @return ethFee Fee amount in ETH
/// @return ethForReserves ETH that goes into the reserves
/// @return totalEthNeeded Total ETH to send (including fee)
/// @return canGraduate True if this purchase would trigger graduation
function quoteBuyExactTokens(address token, uint256 tokenAmount)
external
view
returns (uint256 ethFee, uint256 ethForReserves, uint256 totalEthNeeded, bool canGraduate)
{
if (!tokenConfigs[token].exists()) revert InvalidToken();
(uint16 totalFeeBps,,) = _readLaunchpadFees(token, true);
(totalEthNeeded, ethFee, ethForReserves, canGraduate) = _quoteBuyExactTokens(token, tokenAmount, totalFeeBps);
}
/// @notice Quotes how many tokens must be sold to receive an exact amount of ETH
/// @param token Address of the token to quote
/// @param ethAmount Amount of ETH the seller wants to receive
/// @return ethPulledFromReserves Amount of ETH pulled from reserves (before fee)
/// @return ethFee Fee amount in ETH
/// @return tokensRequired Amount of tokens that must be sold
function quoteSellTokensForExactEth(address token, uint256 ethAmount)
external
view
returns (uint256 ethPulledFromReserves, uint256 ethFee, uint256 tokensRequired)
{
if (!tokenConfigs[token].exists()) revert InvalidToken();
(uint16 totalFeeBps,,) = _readLaunchpadFees(token, false);
(ethPulledFromReserves, ethFee, tokensRequired) = _quoteSellTokensForExactEth(token, ethAmount, totalFeeBps);
}
/// @notice Returns the maximum amount of ETH that can be spent on a given token
/// @dev This avoids going above the excess limit above graduation threshold
function getMaxEthToSpend(address token) external view returns (uint256) {
return _maxEthToSpend(token);
}
/// @notice Returns relevant state variables of a token defined in TokenState struct
/// @param token Address of the token
/// @return The TokenState
function getTokenState(address token) external view returns (TokenState memory) {
return tokenStates[token];
}
/// @notice Returns the configuration of a token
/// @param token Address of the token
/// @return The token configuration
function getTokenConfig(address token) external view returns (TokenConfig memory) {
return tokenConfigs[token];
}
//////////////////////////// Admin functions //////////////////////////
/// @notice Whitelist a Factory allowed to launch tokens here
function whitelistFactory(address factory) external onlyOwner {
require(factory != address(0), InvalidAddress());
require(!whitelistedFactories[factory], AlreadyConfigured());
whitelistedFactories[factory] = true;
emit FactoryWhitelisted(factory);
}
/// @notice blacklist a Factory not allowed to launch tokens here anymore
function blacklistFactory(address factory) external onlyOwner {
require(factory != address(0), InvalidAddress());
require(whitelistedFactories[factory], UnauthorizedFactory());
whitelistedFactories[factory] = false;
emit FactoryBlacklisted(factory);
}
/// @notice Updates the treasury address
/// @param recipient The new treasury address
function setTreasuryAddress(address recipient) public onlyOwner {
require(recipient != address(0), InvalidAddress());
treasury = recipient;
emit TreasuryAddressUpdated(recipient);
}
/// @notice If a token is abandoned by original creators and the community wants to step in,
/// the contract admins can propose a new owner without needing to be the current owner.
/// The proposed address must still call acceptTokenOwnership() to complete the transfer.
/// @dev A malicious team can potentially take over any token, but that would undermine the community trust so the incentives are little.
/// @param token The address of the token
/// @param newTokenOwner The address of the proposed new tokenOwner (must not be address(0))
function communityTakeOver(address token, address newTokenOwner) external onlyOwner {
// address(0) is allowed, to cancel an existing proposed owner
ILivoToken(token).proposeNewOwner(newTokenOwner);
emit CommunityTakeOver(token, newTokenOwner);
}
//////////////////////////// Internal functions //////////////////////////
/// @dev This function assumes that the graduation criteria is met
/// @dev It also assumes that the token hasn't been graduated yet
function _graduateToken(address tokenAddress, TokenState storage tokenState) internal {
IERC20 token = IERC20(tokenAddress);
tokenState.graduated = true;
uint256 ethCollected = tokenState.ethCollected;
uint256 tokenBalance = _availableTokens(tokenAddress);
// Update state - all resources transferred to graduator
tokenState.ethCollected = 0;
tokenState.releasedSupply += tokenBalance;
address graduator = ILivoToken(tokenAddress).graduator();
// Transfer ALL tokens to graduator
token.safeTransfer(graduator, tokenBalance);
// Graduator handles: burning, fees, compensation, liquidity
// IMPORTANT: for v2 taxable tokens, transfer should happen before marking the token as graduated, so that the graduator is not taxed at graduation
ILivoGraduator(graduator).graduateToken{value: ethCollected}(tokenAddress, tokenBalance);
emit TokenGraduated(tokenAddress, ethCollected, tokenBalance);
}
/// @notice Transfers ETH to a recipient, optionally reverting on failure
function _transferEth(address recipient, uint256 amount, bool requireSuccess) internal returns (bool) {
if (amount == 0) return true;
// note: this call happens always after all state changes in all callers of this function to protect against re-entrancy
(bool success,) = recipient.call{value: amount}("");
require(!requireSuccess || success, EthTransferFailed());
return success;
}
/// @notice Routes a trading fee for accounting parity with the post-graduation `LivoSwapHook`:
/// the LP fee is split between treasury (pushed) and creator, while the tax goes entirely
/// to the creator. Emits `LpFeesAccrued` (and `CreatorTaxesAccrued` when the tax is non-zero).
/// @dev Must be called after all state changes (CEI). Trade entry points are also nonReentrant,
/// which is what makes the `accrueFees` hop to a creator-controlled receiver safe.
function _settleFee(address token, uint256 lpFee, uint256 tax, uint16 treasuryShareBps) internal {
if (lpFee == 0 && tax == 0) return;
uint256 treasuryShare = (lpFee * treasuryShareBps) / BASIS_POINTS;
uint256 creatorShare = lpFee - treasuryShare;
uint256 creatorTotal = creatorShare + tax;
emit LpFeesAccrued(token, creatorShare, treasuryShare);
if (tax > 0) emit CreatorTaxesAccrued(token, tax);
_transferEth(treasury, treasuryShare, true);
if (creatorTotal > 0) ILivoToken(token).accrueFees{value: creatorTotal}();
}
//////////////////////// INTERNAL VIEW FUNCTIONS //////////////////////////
/// @notice Reads the token's pre-graduation fee policy for a trade and enforces the launchpad caps.
/// @dev The fee rates and the treasury/creator split are owned by the token; the launchpad only
/// bounds them defensively: the total fee (LP fee + tax) must be `<= MAX_TRADING_FEE_BPS` and
/// `treasuryShareBps <= BASIS_POINTS`.
/// @return totalFeeBps Sum of the LP fee and the tax — the amount deducted from the trade.
/// @return lpFeeBps The LP-fee component, split treasury/creator by `treasuryShareBps`.
/// @return treasuryShareBps Share of the LP fee routed to the treasury.
function _readLaunchpadFees(address token, bool isBuy)
internal
view
returns (uint16 totalFeeBps, uint16 lpFeeBps, uint16 treasuryShareBps)
{
ILivoToken.LaunchpadFees memory fees = ILivoToken(token)
.getLaunchpadFees(
ILivoToken.LaunchpadTrade({
isBuy: isBuy,
ethReserves: tokenStates[token].ethCollected,
releasedSupply: tokenStates[token].releasedSupply
})
);
uint256 total = uint256(fees.lpFeeBps) + fees.taxBps;
require(total <= MAX_TRADING_FEE_BPS, InvalidLaunchpadFee());
require(fees.treasuryShareBps <= BASIS_POINTS, InvalidLaunchpadFee());
return (uint16(total), fees.lpFeeBps, fees.treasuryShareBps);
}
/// @notice Returns the maximum ETH a user can spend on a token without exceeding graduation limits
function _maxEthToSpend(address token) internal view returns (uint256 ethBuy) {
(uint16 totalFeeBps,,) = _readLaunchpadFees(token, true);
uint256 remainingReserves = tokenConfigs[token].bondingCurve.maxEthReserves() - tokenStates[token].ethCollected;
// apply inverse fees
ethBuy = (remainingReserves * BASIS_POINTS) / (BASIS_POINTS - totalFeeBps);
}
/// @notice Computes the buy quote: ETH split, fee, tokens received, and graduation eligibility
function _quoteBuyTokensWithExactEth(address token, uint256 ethValue, uint16 buyFeeBps)
internal
view
returns (uint256 ethForPurchase, uint256 ethFee, uint256 tokensToReceive, bool canGraduate)
{
// it is ok that these fees round in favor of the user (1 wei less fee on every purchase)
ethFee = (ethValue * buyFeeBps) / BASIS_POINTS;
ethForPurchase = ethValue - ethFee;
(tokensToReceive, canGraduate) =
tokenConfigs[token].bondingCurve.buyTokensWithExactEth(tokenStates[token].ethCollected, ethForPurchase);
if (tokensToReceive > _availableTokens(token)) revert NotEnoughSupply();
return (ethForPurchase, ethFee, tokensToReceive, canGraduate);
}
/// @notice Computes the sell quote: ETH pulled from reserves, fee, and ETH for the seller
function _quoteSellExactTokens(address token, uint256 tokenAmount, uint16 sellFeeBps)
internal
view
returns (uint256 ethPulledFromReserves, uint256 ethFee, uint256 ethForSeller)
{
ethPulledFromReserves =
tokenConfigs[token].bondingCurve.sellExactTokens(tokenStates[token].ethCollected, tokenAmount);
// it is ok that these fees round in favor of the user (1 wei less fee on every sale)
ethFee = (ethPulledFromReserves * sellFeeBps) / BASIS_POINTS;
ethForSeller = ethPulledFromReserves - ethFee;
if (ethPulledFromReserves > _availableEthFromReserves(token)) revert InsufficientEthReserves();
return (ethPulledFromReserves, ethFee, ethForSeller);
}
/// @notice Computes the inverse buy quote: total ETH needed to buy exact tokens
function _quoteBuyExactTokens(address token, uint256 tokenAmount, uint16 buyFeeBps)
internal
view
returns (uint256 totalEthNeeded, uint256 ethFee, uint256 ethForReserves, bool canGraduate)
{
(ethForReserves, canGraduate) =
tokenConfigs[token].bondingCurve.buyExactTokens(tokenStates[token].ethCollected, tokenAmount);
// Inverse fee: ethForReserves = totalEthNeeded * (BASIS_POINTS - buyFeeBps) / BASIS_POINTS
// So totalEthNeeded = ceil(ethForReserves * BASIS_POINTS / (BASIS_POINTS - buyFeeBps))
uint256 denom = BASIS_POINTS - buyFeeBps;
totalEthNeeded = (ethForReserves * BASIS_POINTS + denom - 1) / denom;
ethFee = totalEthNeeded - ethForReserves;
if (tokenAmount > _availableTokens(token)) revert NotEnoughSupply();
}
/// @notice Computes the inverse sell quote: tokens needed to receive exact ETH
function _quoteSellTokensForExactEth(address token, uint256 ethAmount, uint16 sellFeeBps)
internal
view
returns (uint256 ethPulledFromReserves, uint256 ethFee, uint256 tokensRequired)
{
// ethAmount = ethPulledFromReserves * (BASIS_POINTS - sellFeeBps) / BASIS_POINTS
// So ethPulledFromReserves = ceil(ethAmount * BASIS_POINTS / (BASIS_POINTS - sellFeeBps))
uint256 denom = BASIS_POINTS - sellFeeBps;
ethPulledFromReserves = (ethAmount * BASIS_POINTS + denom - 1) / denom;
ethFee = ethPulledFromReserves - ethAmount;
tokensRequired = tokenConfigs[token].bondingCurve
.sellTokensForExactEth(tokenStates[token].ethCollected, ethPulledFromReserves);
if (ethPulledFromReserves > _availableEthFromReserves(token)) revert InsufficientEthReserves();
}
/// @dev The supply of a token that can be purchased
function _availableTokens(address token) internal view returns (uint256) {
return ILivoToken(token).balanceOf(address(this));
}
/// @notice Returns the ETH reserves currently allocated to a token
function _availableEthFromReserves(address token) internal view returns (uint256) {
return tokenStates[token].ethCollected;
}
function _onlyWhitelistedFactory() internal view {
require(whitelistedFactories[msg.sender], UnauthorizedFactory());
}
}
Chain explorer2216msChain node83ms