Contract detail
BondingV5
BondingV5 is a verified Robinhood Chain smart contract at 0xffb470f3…6acb6265 with 0 indexed transactions.
- Address type
- Smart contract
- Transactions
- 0
- History coverage
- No indexed activity found
- Emitted events
- 1
- Verification
- Verified
- Compiler
- v0.8.26+commit.8a97fa7a
- Verification match
- full
- Creation transaction
- 0x2665dff2…376c9434
- Created at block
- #191784
- Language
- solidity
- EVM version
- paris
- License
- none
- Compiler optimization
- Enabled · 200 runs
Verified contract
Contract source code
// SPDX-License-Identifier: MIT
// Modified from https://github.com/sourlodine/Pump.fun-Smart-Contract/blob/main/contracts/PumpFun.sol
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
import "./BondingConfig.sol";
import "./IFPairV2.sol";
import "../virtualPersona/IAgentTokenV4.sol";
// Minimal interfaces to reduce contract size
interface IFFactoryV2Minimal {
function createPair(
address tokenA,
address tokenB,
uint256 startTime,
uint256 startTimeDelay
) external returns (address pair);
function getPair(
address tokenA,
address tokenB
) external view returns (address pair);
function taxVault() external view returns (address);
}
interface IFRouterV3Minimal {
function assetToken() external view returns (address);
function addInitialLiquidity(
address token,
uint256 amountToken,
uint256 amountAsset
) external;
function buy(
uint256 amountIn,
address tokenAddress,
address to,
bool isInitialPurchase
) external returns (uint256, uint256);
function sell(
uint256 amountIn,
address tokenAddress,
address to
) external returns (uint256, uint256);
function graduate(address tokenAddress) external;
function setTaxStartTime(
address pairAddress,
uint256 taxStartTime
) external;
function hasAntiSniperTax(address pairAddress) external view returns (bool);
}
interface IAgentFactoryV7Minimal {
function createNewAgentTokenAndApplication(
string memory name,
string memory symbol,
bytes memory tokenSupplyParams,
uint8[] memory cores,
bytes32 tbaSalt,
address tbaImplementation,
uint32 daoVotingPeriod,
uint256 daoThreshold,
uint256 applicationThreshold,
address creator
) external returns (address, uint256);
function addBlacklistAddress(address token, address addr) external;
function removeBlacklistAddress(address token, address addr) external;
function updateApplicationThresholdWithApplicationId(
uint256 applicationId,
uint256 applicationThreshold
) external;
function executeBondingCurveApplicationSalt(
uint256 id,
uint256 totalSupply,
uint256 lpSupply,
address vault,
bytes32 salt
) external returns (address);
}
interface IAgentTaxMinimal {
function registerToken(address tokenAddress, address tba, address creator) external;
}
contract BondingV5 is
Initializable,
ReentrancyGuardUpgradeable,
OwnableUpgradeable
{
using SafeERC20 for IERC20;
IFFactoryV2Minimal public factory;
IFRouterV3Minimal public router;
IAgentFactoryV7Minimal public agentFactory;
BondingConfig public bondingConfig; // Configuration contract for multi-chain launch modes
mapping(address => BondingConfig.Token) public tokenInfo;
address[] public tokenInfos;
// this is for BE to separate with old virtualId from bondingV1, but this field is not used yet
uint256 public constant VirtualIdBase = 50_000_000_000;
// Mapping to store configurable launch parameters for each token
mapping(address => BondingConfig.LaunchParams) public tokenLaunchParams;
// Mapping to store graduation threshold for each token (calculated per-token based on airdropBips and needAcf)
mapping(address => uint256) public tokenGradThreshold;
mapping(address => bool) public isFeeDelegation;
/// @notice Raw `extParams_` from `preLaunch` (per agent token). Backend can read as canonical payload; not updated by `setFeeDelegation`.
mapping(address => bytes) public tokenPreLaunchExtParams;
/// @dev Appended in upgrade v2 — must remain last state variable (append-only layout).
/// Fake initial virtual liquidity frozen at preLaunch for migration price continuity.
/// If unset (pre-upgrade token), graduation uses BondingConfig legacyInitialVirtualLiq.
mapping(address => uint256) public tokenFakeInitialVirtualLiq;
event PreLaunched(
address indexed token,
address indexed pair,
uint256 virtualId,
uint256 initialPurchase,
BondingConfig.LaunchParams launchParams
);
event Launched(
address indexed token,
address indexed pair,
uint256 virtualId,
uint256 initialPurchase,
uint256 initialPurchasedAmount,
BondingConfig.LaunchParams launchParams
);
event CancelledLaunch(
address indexed token,
address indexed pair,
uint,
uint256 initialPurchase
);
event Graduated(address indexed token, address agentToken);
event FeeDelegationUpdated(address indexed token, bool isFeeDelegation);
error InvalidTokenStatus();
error InvalidInput();
error SlippageTooHigh();
error UnauthorizedLauncher();
error LaunchModeNotEnabled();
error InvalidAntiSniperType();
error InvalidSpecialLaunchParams();
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
/// @notice Decode `isFeeDelegation` from optional extension calldata.
/// @dev V1 layout: first 32 bytes = `abi.encode(bool isFeeDelegation)` (standard ABI word).
/// Empty `extParams` or length less than 32 ⇒ false. Extra trailing bytes are ignored here so
/// callers can forward-compat append more values after the bool (same encoding rules).
/// Non-canonical bool words (not 0 or 1) ⇒ false.
function _decodeIsFeeDelegation(bytes calldata extParams) internal pure returns (bool) {
if (extParams.length < 32) {
return false;
}
bytes32 word;
assembly ("memory-safe") {
word := calldataload(extParams.offset)
}
uint256 v = uint256(word);
if (v == 1) return true;
if (v == 0) return false;
return false;
}
function initialize(
address factory_,
address router_,
address agentFactory_,
address bondingConfig_
) external initializer {
__Ownable_init(msg.sender);
__ReentrancyGuard_init();
factory = IFFactoryV2Minimal(factory_);
router = IFRouterV3Minimal(router_);
agentFactory = IAgentFactoryV7Minimal(agentFactory_);
bondingConfig = BondingConfig(bondingConfig_);
}
/// @param extParams_ Optional extension payload. V1: `abi.encode(bool isFeeDelegation)`; use
/// `""` (empty) to default `isFeeDelegation` to false. Future versions may use
/// `abi.encode(bool, ...)`; this function only reads the first bool word.
function preLaunch(
string memory name_,
string memory ticker_,
uint8[] memory cores_,
string memory desc_,
string memory img_,
string[4] memory urls_,
uint256 purchaseAmount_,
uint256 startTime_,
uint8 launchMode_,
uint16 airdropBips_,
bool needAcf_,
uint8 antiSniperTaxType_,
bool isProject60days_,
bytes calldata extParams_
) public nonReentrant returns (address, address, uint, uint256) {
bool isFeeDelegation_ = _decodeIsFeeDelegation(extParams_);
// Fail-fast: validate reserve bips and calculate bonding curve supply upfront
// This validates: airdropBips <= maxAirdropBips AND totalReserved < maxTotalReservedBips
uint256 bondingCurveSupplyBase = bondingConfig
.calculateBondingCurveSupply(airdropBips_, needAcf_);
// Validate anti-sniper tax type
if (!bondingConfig.isValidAntiSniperType(antiSniperTaxType_)) {
revert InvalidAntiSniperType();
}
if (cores_.length <= 0) {
revert InvalidInput();
}
// Determine if this is an immediate launch or scheduled launch
// Immediate launch: startTime < now + scheduledLaunchStartTimeDelay
// Scheduled launch: startTime >= now + scheduledLaunchStartTimeDelay
BondingConfig.ScheduledLaunchParams
memory scheduledParams = bondingConfig.getScheduledLaunchParams();
uint256 scheduledThreshold = block.timestamp +
scheduledParams.startTimeDelay;
bool isScheduledLaunch = startTime_ >= scheduledThreshold;
// Validate launch mode, authorization, and mode-specific requirements
_validateLaunchMode(
launchMode_,
antiSniperTaxType_,
airdropBips_,
needAcf_,
isProject60days_,
isScheduledLaunch
);
uint256 actualStartTime;
uint256 actualStartTimeDelay;
if (isScheduledLaunch) {
// Scheduled launch: use provided startTime
actualStartTime = startTime_;
actualStartTimeDelay = scheduledParams.startTimeDelay;
} else {
// Immediate launch: start immediately
actualStartTime = block.timestamp;
actualStartTimeDelay = 0;
}
// Calculate launch fee based on launch type and ACF requirement
// Fee structure:
// - Immediate launch, no ACF: 0
// - Immediate launch, with ACF: acfFee
// - Scheduled launch, no ACF: normalLaunchFee
// - Scheduled launch, with ACF: normalLaunchFee + acfFee
uint256 launchFee = bondingConfig.calculateLaunchFee(
isScheduledLaunch,
needAcf_
);
if (purchaseAmount_ < launchFee) {
revert InvalidInput();
}
address assetToken = router.assetToken();
uint256 initialPurchase = (purchaseAmount_ - launchFee);
if (launchFee > 0) {
IERC20(assetToken).safeTransferFrom(
msg.sender,
bondingConfig.feeTo(),
launchFee
);
}
IERC20(assetToken).safeTransferFrom(
msg.sender,
address(this),
initialPurchase
);
uint256 configInitialSupply = bondingConfig.initialSupply();
BondingConfig.DeployParams memory configDeployParams = bondingConfig
.getDeployParams();
(address token, uint256 applicationId) = agentFactory
.createNewAgentTokenAndApplication(
name_, // without "fun " prefix
ticker_,
abi.encode(
// tokenSupplyParams
configInitialSupply,
0, // lpSupply, will mint to agentTokenAddress
configInitialSupply, // vaultSupply, will mint to vault
configInitialSupply,
configInitialSupply,
0,
address(this) // vault, is the bonding contract itself
),
cores_,
configDeployParams.tbaSalt,
configDeployParams.tbaImplementation,
configDeployParams.daoVotingPeriod,
configDeployParams.daoThreshold,
0, // applicationThreshold_
msg.sender // token creator
);
// this is to prevent transfer to blacklist address before graduation
agentFactory.addBlacklistAddress(
token,
IAgentTokenV4(token).liquidityPools()[0]
);
// Calculate bonding curve supply in wei (base supply was validated at the beginning)
uint256 bondingCurveSupply = bondingCurveSupplyBase *
(10 ** IAgentTokenV4(token).decimals());
// Calculate total reserved supply for transfer
uint256 totalReservedSupply = configInitialSupply -
bondingCurveSupplyBase;
address pair = factory.createPair(
token,
assetToken,
actualStartTime,
actualStartTimeDelay
);
require(_approval(address(router), token, bondingCurveSupply));
uint256 liquidity = bondingConfig.getFakeInitialVirtualLiq(needAcf_);
tokenFakeInitialVirtualLiq[token] = liquidity;
uint256 price = bondingCurveSupply / liquidity;
router.addInitialLiquidity(token, bondingCurveSupply, liquidity);
// Transfer reserved tokens (airdrop + ACF if needed) to teamTokenReservedWallet
if (totalReservedSupply > 0) {
IERC20(token).safeTransfer(
bondingConfig.teamTokenReservedWallet(),
totalReservedSupply * (10 ** IAgentTokenV4(token).decimals())
);
}
// Calculate and store per-token graduation threshold
uint256 gradThreshold = bondingConfig.calculateGradThreshold(
bondingCurveSupply,
needAcf_
);
tokenGradThreshold[token] = gradThreshold;
tokenInfos.push(token);
// Use storage reference to avoid stack overflow
BondingConfig.Token storage newToken = tokenInfo[token];
newToken.creator = msg.sender;
newToken.token = token;
newToken.agentToken = address(0);
newToken.pair = pair;
newToken.description = desc_;
newToken.cores = cores_;
newToken.image = img_;
newToken.twitter = urls_[0];
newToken.telegram = urls_[1];
newToken.youtube = urls_[2];
newToken.website = urls_[3];
newToken.trading = true;
newToken.tradingOnUniswap = false;
newToken.applicationId = applicationId;
newToken.initialPurchase = initialPurchase;
newToken.virtualId = VirtualIdBase + tokenInfos.length;
newToken.launchExecuted = false;
// Store V5 configurable launch parameters
tokenLaunchParams[token] = BondingConfig.LaunchParams({
launchMode: launchMode_,
airdropBips: airdropBips_,
needAcf: needAcf_,
antiSniperTaxType: antiSniperTaxType_,
isProject60days: isProject60days_
});
isFeeDelegation[token] = isFeeDelegation_;
tokenPreLaunchExtParams[token] = extParams_;
// Set Data struct fields
newToken.data.token = token;
newToken.data.name = name_;
newToken.data._name = name_;
newToken.data.ticker = ticker_;
newToken.data.supply = bondingCurveSupply;
newToken.data.price = price;
newToken.data.marketCap = liquidity;
newToken.data.liquidity = liquidity * 2;
newToken.data.volume = 0;
newToken.data.volume24H = 0;
newToken.data.prevPrice = price;
newToken.data.lastUpdated = block.timestamp;
// Register token with AgentTax for on-chain tax attribution
// This must happen BEFORE _buy() is called, as _buy() triggers depositTax()
address taxVault = factory.taxVault();
require(taxVault != address(0), "TaxVault not set");
IAgentTaxMinimal(taxVault).registerToken(
token,
newToken.creator, // Use creator as TBA
newToken.creator
);
emit PreLaunched(
token,
pair,
tokenInfo[token].virtualId,
initialPurchase,
tokenLaunchParams[token]
);
return (token, pair, tokenInfo[token].virtualId, initialPurchase);
}
function cancelLaunch(address tokenAddress_) public nonReentrant() {
BondingConfig.Token storage tokenRef = tokenInfo[tokenAddress_];
// Validate that the token exists and was properly prelaunched
if (tokenRef.token == address(0) || tokenRef.pair == address(0)) {
revert InvalidInput();
}
if (msg.sender != tokenRef.creator) {
revert InvalidInput();
}
// Validate that the token has not been launched (or cancelled)
if (tokenRef.launchExecuted) {
revert InvalidTokenStatus();
}
if (tokenRef.initialPurchase > 0) {
IERC20(router.assetToken()).safeTransfer(
tokenRef.creator,
tokenRef.initialPurchase
);
}
uint256 initialPurchase = tokenRef.initialPurchase; // record real initialPurchase for event
tokenRef.initialPurchase = 0; // prevent duplicate transfer initialPurchase back to the creator
tokenRef.trading = false; // disable trading for cancelled tokens
tokenRef.launchExecuted = true; // pretend it has been launched (cancelled) and prevent duplicate launch
emit CancelledLaunch(
tokenAddress_,
tokenRef.pair,
tokenInfo[tokenAddress_].virtualId,
initialPurchase
);
}
function launch(
address tokenAddress_
) public nonReentrant returns (address, address, uint, uint256) {
BondingConfig.Token storage tokenRef = tokenInfo[tokenAddress_];
// Validate that the token exists and was properly prelaunched
if (tokenRef.token == address(0) || tokenRef.pair == address(0)) {
revert InvalidInput();
}
if (tokenRef.launchExecuted) {
revert InvalidTokenStatus();
}
// If initialPurchase == 0, the function marks the token as launched
// while swaps remain blocked (since enabling depends solely on time),
// resulting in an inconsistent "launched but not tradable" state, also the 550M is go to teamTokenReservedWallet
// so we need to check the start time of the pair
IFPairV2 pairContract = IFPairV2(tokenRef.pair);
if (block.timestamp < pairContract.startTime()) {
revert InvalidInput();
}
// X_LAUNCH, ACP_SKILL, and Project60days: taxRecipient (AgentTax) must be updated by the backend
// before trading starts; only privileged backend wallets may call launch() so that ordering is enforced.
if (isFeeDelegation[tokenAddress_] || isProject60days(tokenAddress_) || isProjectXLaunch(tokenAddress_) || isAcpSkillLaunch(tokenAddress_)) {
if (!bondingConfig.isPrivilegedLauncher(msg.sender)) {
revert UnauthorizedLauncher();
}
}
// Set tax start time to current block timestamp for proper anti-sniper tax calculation
router.setTaxStartTime(tokenRef.pair, block.timestamp);
// Make initial purchase for creator
// bondingContract will transfer initialPurchase $Virtual to pairAddress
// pairAddress will transfer amountsOut $agentToken to bondingContract
// bondingContract then will transfer all the amountsOut $agentToken to teamTokenReservedWallet
// in the BE, teamTokenReservedWallet will split it out for the initialBuy and 550M
uint256 amountOut = 0;
uint256 initialPurchase = tokenRef.initialPurchase;
if (initialPurchase > 0) {
IERC20(router.assetToken()).forceApprove(
address(router),
initialPurchase
);
amountOut = _buy(
address(this),
initialPurchase, // will raise error if initialPurchase <= 0
tokenAddress_,
0,
block.timestamp + 300,
true // isInitialPurchase = true for creator's purchase
);
// creator's initialBoughtToken need to go to teamTokenReservedWallet for locking, not to creator
IERC20(tokenAddress_).safeTransfer(
bondingConfig.teamTokenReservedWallet(),
amountOut
);
// update initialPurchase and launchExecuted to prevent duplicate purchase
tokenRef.initialPurchase = 0;
}
emit Launched(
tokenAddress_,
tokenRef.pair,
tokenInfo[tokenAddress_].virtualId,
initialPurchase,
amountOut,
tokenLaunchParams[tokenAddress_]
);
tokenRef.launchExecuted = true;
return (
tokenAddress_,
tokenRef.pair,
tokenInfo[tokenAddress_].virtualId,
initialPurchase
);
}
function sell(
uint256 amountIn_,
address tokenAddress_,
uint256 amountOutMin_,
uint256 deadline_
) public returns (bool) {
// this alrealy prevented it's a not-exists token
if (!tokenInfo[tokenAddress_].trading) {
revert InvalidTokenStatus();
}
// this is to prevent sell before launch
if (!tokenInfo[tokenAddress_].launchExecuted) {
revert InvalidTokenStatus();
}
if (block.timestamp > deadline_) {
revert InvalidInput();
}
(uint256 amount0In, uint256 amount1Out) = router.sell(
amountIn_,
tokenAddress_,
msg.sender
);
if (amount1Out == 0 || amount1Out < amountOutMin_) {
revert SlippageTooHigh();
}
uint256 duration = block.timestamp -
tokenInfo[tokenAddress_].data.lastUpdated;
if (duration > 86400) {
tokenInfo[tokenAddress_].data.lastUpdated = block.timestamp;
}
return true;
}
function _buy(
address buyer_,
uint256 amountIn_,
address tokenAddress_,
uint256 amountOutMin_,
uint256 deadline_,
bool isInitialPurchase_
) internal returns (uint256) {
if (block.timestamp > deadline_) {
revert InvalidInput();
}
address pairAddress = factory.getPair(
tokenAddress_,
router.assetToken()
);
IFPairV2 pairContract = IFPairV2(pairAddress);
(uint256 reserveA, uint256 reserveB) = pairContract.getReserves();
(uint256 amount1In, uint256 amount0Out) = router.buy(
amountIn_,
tokenAddress_,
buyer_,
isInitialPurchase_
);
// Match BondingV2: reject zero output even when amountOutMin_ is 0 (drained pool / forced grad)
if (amount0Out == 0 || amount0Out < amountOutMin_) {
revert SlippageTooHigh();
}
uint256 newReserveA = reserveA - amount0Out;
uint256 duration = block.timestamp -
tokenInfo[tokenAddress_].data.lastUpdated;
if (duration > 86400) {
tokenInfo[tokenAddress_].data.lastUpdated = block.timestamp;
}
// Get per-token gradThreshold (calculated during preLaunch based on airdropBips and needAcf)
uint256 gradThreshold = tokenGradThreshold[tokenAddress_];
if (
newReserveA <= gradThreshold &&
!router.hasAntiSniperTax(pairAddress) &&
tokenInfo[tokenAddress_].trading
) {
_openTradingOnUniswap(tokenAddress_);
}
return amount0Out;
}
function buy(
uint256 amountIn_,
address tokenAddress_,
uint256 amountOutMin_,
uint256 deadline_
) public payable returns (bool) {
// this alrealy prevented it's a not-exists token
if (!tokenInfo[tokenAddress_].trading) {
revert InvalidTokenStatus();
}
// this is to prevent sell before launch
if (!tokenInfo[tokenAddress_].launchExecuted) {
revert InvalidTokenStatus();
}
_buy(
msg.sender,
amountIn_,
tokenAddress_,
amountOutMin_,
deadline_,
false
);
return true;
}
function _openTradingOnUniswap(address tokenAddress_) private {
BondingConfig.Token storage tokenRef = tokenInfo[tokenAddress_];
if (tokenRef.tradingOnUniswap || !tokenRef.trading) {
revert InvalidTokenStatus();
}
// Transfer asset tokens to bonding contract
address pairAddress = factory.getPair(
tokenAddress_,
router.assetToken()
);
IFPairV2 pairContract = IFPairV2(pairAddress);
uint256 assetBalance = pairContract.assetBalance();
uint256 tokenBalance = pairContract.balance();
// Bonding reserves include fakeInitialVirtualLiq that was never deposited.
// Seed UniV2 with only the token share backed by real asset so FDV is continuous.
uint256 fakeVirtualLiq = tokenFakeInitialVirtualLiq[tokenAddress_];
if (fakeVirtualLiq == 0) {
fakeVirtualLiq = bondingConfig.getLegacyInitialVirtualLiq();
}
uint256 lpTokenAmount = tokenBalance;
if (fakeVirtualLiq > 0 && assetBalance > 0) {
lpTokenAmount =
(tokenBalance * assetBalance) /
(assetBalance + fakeVirtualLiq);
}
uint256 excessTokens = tokenBalance - lpTokenAmount;
router.graduate(tokenAddress_);
// previously initFromBondingCurve has two parts:
// 1. transfer applicationThreshold_ assetToken from bondingContract to agentFactoryV3Contract
// 2. create Application
// now only need to do 1st part and update application.withdrawableAmount to assetBalance
IERC20(router.assetToken()).safeTransfer(
address(agentFactory),
assetBalance
);
agentFactory.updateApplicationThresholdWithApplicationId(
tokenRef.applicationId,
assetBalance
);
// remove blacklist address after graduation, cuz executeBondingCurveApplicationSalt we will transfer all left agentTokens to the uniswapV2Pair
agentFactory.removeBlacklistAddress(
tokenAddress_,
IAgentTokenV4(tokenAddress_).liquidityPools()[0]
);
// previously executeBondingCurveApplicationSalt will create agentToken and do two parts:
// 1. (lpSupply = all left $preToken in prePairAddress) $agentToken mint to agentTokenAddress
// 2. (vaultSupply = 1B - lpSupply) $agentToken mint to prePairAddress
if (excessTokens > 0) {
address burnWallet = bondingConfig.graduationExcessBurnWallet();
IERC20(tokenAddress_).safeTransfer(burnWallet, excessTokens);
}
// Transfer LP-bound tokens to agent token for UniV2 seeding
IERC20(tokenAddress_).safeTransfer(tokenAddress_, lpTokenAmount);
require(tokenRef.applicationId != 0, "ApplicationId not found");
address agentToken = agentFactory.executeBondingCurveApplicationSalt(
tokenRef.applicationId,
tokenRef.data.supply / 1 ether, // totalSupply
lpTokenAmount / 1 ether, // lpSupply
pairAddress, // vault
keccak256(
abi.encodePacked(msg.sender, block.timestamp, tokenAddress_)
)
);
tokenRef.agentToken = agentToken;
// this is not needed, previously need to do this because of
// 1. (vaultSupply = 1B - lpSupply) $agentToken will mint to prePairAddress
// 2. then in unwrapToken, we will transfer burn preToken of each account and transfer same amount of agentToken to them from prePairAddress
// router.approval(
// pairAddress,
// agentToken,
// address(this),
// IERC20(agentToken).balanceOf(pairAddress)
// );
emit Graduated(tokenAddress_, agentToken);
tokenRef.trading = false;
tokenRef.tradingOnUniswap = true;
}
// View functions to check token launch type (affects tax recipient updates and liquidity drain permissions)
function isProject60days(address token_) public view returns (bool) {
return tokenLaunchParams[token_].isProject60days;
}
function isProjectXLaunch(address token_) public view returns (bool) {
return
tokenLaunchParams[token_].launchMode ==
bondingConfig.LAUNCH_MODE_X_LAUNCH();
}
function isAcpSkillLaunch(address token_) public view returns (bool) {
return
tokenLaunchParams[token_].launchMode ==
bondingConfig.LAUNCH_MODE_ACP_SKILL();
}
// View function for FRouterV3 to get anti-sniper tax type
// Reverts if token was not created by BondingV5, allowing FRouterV3 to fallback to legacy logic
function tokenAntiSniperType(address token_) external view returns (uint8) {
if (tokenInfo[token_].creator == address(0)) {
revert InvalidTokenStatus();
}
return tokenLaunchParams[token_].antiSniperTaxType;
}
function _approval(
address spender_,
address token_,
uint256 amount_
) internal returns (bool) {
IERC20(token_).forceApprove(spender_, amount_);
return true;
}
/**
* @notice Validate launch mode is enabled and caller is authorized
* @param launchMode_ The launch mode identifier
*/
function _validateLaunchMode(
uint8 launchMode_,
uint8 antiSniperTaxType_,
uint16 airdropBips_,
bool needAcf_,
bool isProject60days_,
bool isScheduledLaunch_
) internal view {
// Validate launch mode is one of the supported types
if (
launchMode_ != bondingConfig.LAUNCH_MODE_NORMAL() &&
launchMode_ != bondingConfig.LAUNCH_MODE_X_LAUNCH() &&
launchMode_ != bondingConfig.LAUNCH_MODE_ACP_SKILL()
) {
revert LaunchModeNotEnabled();
}
// X_LAUNCH & ACP_SKILL: privileged preLaunch + strict params. Project60days stays NORMAL here;
// launch() still requires privileged wallet for 60d / X / ACP so taxRecipient is backend-orchestrated.
if (
launchMode_ == bondingConfig.LAUNCH_MODE_X_LAUNCH() ||
launchMode_ == bondingConfig.LAUNCH_MODE_ACP_SKILL()
) {
if (!bondingConfig.isPrivilegedLauncher(msg.sender)) {
revert UnauthorizedLauncher();
}
// 1. ANTI_SNIPER_60S
// 2. Immediate launch (startTime within 24h)
// 3. airdropBips = 0
// 4. needAcf = false
// 5. isProject60days_ = false
if (
antiSniperTaxType_ != bondingConfig.ANTI_SNIPER_60S() ||
isScheduledLaunch_ ||
airdropBips_ != 0 ||
needAcf_ ||
isProject60days_
) {
revert InvalidSpecialLaunchParams();
}
}
}
function setBondingConfig(address bondingConfig_) public onlyOwner {
bondingConfig = BondingConfig(bondingConfig_);
}
/// @notice Update `isFeeDelegation` for an existing BondingV5 token (e.g. backend correction after mis-encoded `extParams`). Does not change `tokenPreLaunchExtParams`.
function setFeeDelegation(address token, bool isFeeDelegation_) external onlyOwner {
if (token == address(0) || tokenInfo[token].token == address(0)) {
revert InvalidInput();
}
isFeeDelegation[token] = isFeeDelegation_;
emit FeeDelegationUpdated(token, isFeeDelegation_);
}
}