Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
525 changes: 524 additions & 1 deletion .openzeppelin/bsc-testnet.json

Large diffs are not rendered by default.

158 changes: 151 additions & 7 deletions contracts/launchpadv2/BondingV5.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,26 @@ contract BondingV5 is
/// @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).
/// @dev Appended in upgrade v2 (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;

/// @dev Appended in upgrade v3 — decoded from `extParams_` at preLaunch (append-only layout).
/// Robotics flag (bit 2 of the flags word). See extParams encoding notes above `preLaunch`.
mapping(address => bool) public isRobotics;

/// @notice Fee delegation type per token: 0 = none, 1 = address, 2 = twitter
/// (decoded from bits 3-4 of the `extParams_` flags word).
mapping(address => uint8) public feeDelegationType;

/// @notice Fee delegation recipient from the `extParams_` trailing word, stored as a fixed
/// `bytes32` right-aligned integer: the recipient address as `uint160` for type 1, or
/// the twitter account id as `uint64` for type 2 (the vault is only created just before
/// `launch`). `bytes32(0)` when no delegation. Fixed-size to save gas vs dynamic bytes.
/// @dev MUST remain the last state variable (append-only layout).
mapping(address => bytes32) public feeDelegationRecipient;

event PreLaunched(
address indexed token,
address indexed pair,
Expand All @@ -144,6 +159,36 @@ contract BondingV5 is

event FeeDelegationUpdated(address indexed token, bool isFeeDelegation);

/// @notice Emitted at preLaunch with the extended launch settings decoded from `extParams_`.
event PreLaunchExtParams(
address indexed token,
bool isFeeDelegation,
bool isRobotics,
uint8 feeDelegationType,
bytes32 feeDelegationRecipient
);

event TokenCreated(
uint256 virtualId,
address indexed creator,
address indexed token,
string name,
string symbol,
uint256 maxSupply,
uint256 saleAmount,
uint256 graduationThreshold,
uint256 initialVirtualLiquidity,
uint256 initialPrice,
uint256 initialPurchase,
address quoteAsset,
address pair,
string description,
string image,
BondingConfig.LaunchParams launchParams,
uint256 startTime,
uint256 startTimeDelay
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TokenCreated omits key indexing fields

High Severity

TokenCreated is missing applicationId and targetRaiseAmount even though the emit comment describes the net quote target, applicationId is already available in preLaunch, and indexingEvents.js asserts both fields. Indexers still need RPC for those values, and the new test cannot pass against this ABI.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a3511cc. Configure here.


error InvalidTokenStatus();
error InvalidInput();
error SlippageTooHigh();
Expand All @@ -157,12 +202,38 @@ contract BondingV5 is
_disableInitializers();
}

/// @dev `extParams` first 32-byte word — flags in the LSB (same byte as `abi.encode(bool)`):
/// bit 0: `isFeeDelegation`
/// bit 1: skip `" by Virtuals"` suffix (unset ⇒ append; V1-compatible)
/// Empty `extParams` or length < 32 ⇒ both flags false ⇒ fee delegation off, suffix on.
/// @dev `extParams` encoding — append-only and length-tolerant so it stays backward
/// compatible and can be freely extended:
///
/// Word 0 (`extParams[0:32]`) is a flags bitfield (same LSB byte as `abi.encode(bool)`):
/// bit 0: `isFeeDelegation`
/// bit 1: skip `" by Virtuals"` suffix (unset ⇒ append; V1-compatible)
/// bit 2: `isRobotics`
/// bits 3-4: `feeDelegationType` (0 = none, 1 = address, 2 = twitter)
/// remaining bits: reserved = 0, available for future flags/enums.
///
/// Trailing word (`extParams[32:64]`, present only when needed) carries the
/// `feeDelegationRecipient` as a fixed `bytes32`, ABI-encoded as
/// `abi.encode(uint256 flags, bytes32 recipient)` with the recipient a right-aligned integer:
/// - type 1 (address): the recipient address as `uint160`
/// - type 2 (twitter): the twitter account id as `uint64`
/// The recipient is opaque to this contract (stored/emitted as-is); off-chain decodes by type.
/// Future fields are appended after this word and decoded defensively by length.
///
/// Rules that MUST hold for every future change:
/// - Never repurpose or move an existing bit/offset (bit 0 / bit 1 semantics are frozen).
/// - Decoders tolerate shorter and longer payloads: read a field only when the length
/// covers it, otherwise use the default; ignore trailing bytes / higher bits.
///
/// Empty `extParams` or length < 32 ⇒ all flags false ⇒ fee delegation off, suffix on,
/// robotics off, no delegation type/recipient (identical to the pre-v3 behaviour).
uint256 private constant EXT_PARAMS_FLAG_FEE_DELEGATION = 1;
uint256 private constant EXT_PARAMS_FLAG_SKIP_SUFFIX = 2;
uint256 private constant EXT_PARAMS_FLAG_ROBOTICS = 4; // bit 2
// `feeDelegationType` occupies bits 3-4 of the flags word: shift right by 3 to bring
// those bits down, then mask with 0x3 (2 bits) to isolate them (0 = none, 1 = address, 2 = twitter).
uint256 private constant EXT_PARAMS_FEE_DELEGATION_TYPE_SHIFT = 3;
uint256 private constant EXT_PARAMS_FEE_DELEGATION_TYPE_MASK = 0x3;

function _loadExtParamsWord(
bytes calldata extParams
Expand All @@ -187,6 +258,38 @@ contract BondingV5 is
return (_loadExtParamsWord(extParams) & EXT_PARAMS_FLAG_SKIP_SUFFIX) == 0;
}

function _decodeIsRobotics(
bytes calldata extParams
) internal pure returns (bool) {
return (_loadExtParamsWord(extParams) & EXT_PARAMS_FLAG_ROBOTICS) != 0;
}

function _decodeFeeDelegationType(
bytes calldata extParams
) internal pure returns (uint8) {
return
uint8(
(_loadExtParamsWord(extParams) >>
EXT_PARAMS_FEE_DELEGATION_TYPE_SHIFT) &
EXT_PARAMS_FEE_DELEGATION_TYPE_MASK
);
}

/// @dev Reads the trailing `feeDelegationRecipient` word (2nd word of
/// `abi.encode(uint256 flags, bytes32 recipient)`) when present. Length-gated so legacy
/// flags-only payloads (length < 64) return `bytes32(0)`, and future longer payloads
/// (extra words appended after the recipient) still decode this word safely.
function _decodeFeeDelegationRecipient(
bytes calldata extParams
) internal pure returns (bytes32 recipient) {
if (extParams.length < 64) {
return bytes32(0);
}
assembly ("memory-safe") {
recipient := calldataload(add(extParams.offset, 32))
}
}

function initialize(
address factory_,
address router_,
Expand All @@ -202,8 +305,11 @@ contract BondingV5 is
bondingConfig = BondingConfig(bondingConfig_);
}

/// @param extParams_ Optional extension payload (first 32 bytes). V1: `abi.encode(bool isFeeDelegation)`.
/// V2: set bit 1 of the word to skip `" by Virtuals"` (bit 1 unset ⇒ append). Use `""` for defaults.
/// @param extParams_ Optional extension payload. See the extParams encoding notes above for the
/// full (append-only, backward-compatible) layout. V1: `abi.encode(bool isFeeDelegation)`.
/// V2: set bit 1 of the flags word to skip `" by Virtuals"`. V3: bit 2 = `isRobotics`,
/// bits 3-4 = `feeDelegationType`, with the optional `feeDelegationRecipient` appended as
/// `abi.encode(uint256 flags, bytes recipient)`. Use `""` for defaults.
function preLaunch(
string memory name_,
string memory ticker_,
Expand Down Expand Up @@ -403,6 +509,14 @@ contract BondingV5 is
isFeeDelegation[token] = isFeeDelegation_;
tokenPreLaunchExtParams[token] = extParams_;

// Decode and store the extended launch settings so on-chain is the source of truth.
bool isRobotics_ = _decodeIsRobotics(extParams_);
uint8 feeDelegationType_ = _decodeFeeDelegationType(extParams_);
bytes32 feeDelegationRecipient_ = _decodeFeeDelegationRecipient(extParams_);
isRobotics[token] = isRobotics_;
feeDelegationType[token] = feeDelegationType_;
feeDelegationRecipient[token] = feeDelegationRecipient_;

// Set Data struct fields
newToken.data.token = token;
newToken.data.name = tokenName;
Expand Down Expand Up @@ -435,6 +549,36 @@ contract BondingV5 is
tokenLaunchParams[token]
);

emit PreLaunchExtParams(
token,
isFeeDelegation_,
isRobotics_,
feeDelegationType_,
feeDelegationRecipient_
);

// Net quote target at the graduation threshold (excludes buy tax / anti-sniper fees).
emit TokenCreated(
tokenInfo[token].virtualId,
msg.sender,
token,
name_,
ticker_,
configInitialSupply * (10 ** IAgentTokenV4(token).decimals()),
bondingCurveSupply,
gradThreshold,
liquidity * 2,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Virtual liquidity value doubled

Medium Severity

TokenCreated.initialVirtualLiquidity emits liquidity * 2, but the curve’s fake initial virtual liquidity stored in tokenFakeInitialVirtualLiq is liquidity. Indexers treating this field as y0 for constant-product math will reconstruct the wrong curve.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a3511cc. Configure here.

price,
initialPurchase,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent price units across events

Medium Severity

TokenCreated sets initialPrice as bondingCurveSupply / liquidity (token-per-quote, unscaled), while TradeExecuted sets lastPrice as (reserveAsset * 1 ether) / reserveToken (quote-per-token, 1e18-scaled). A log-only indexer treating both as the same price series will show a discontinuous or inverted curve at the first trade.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d8a8674. Configure here.

assetToken,
pair,
desc_,
img_,
tokenLaunchParams[token],
actualStartTime,
actualStartTimeDelay
);

return (token, pair, tokenInfo[token].virtualId, initialPurchase);
}

Expand Down
80 changes: 80 additions & 0 deletions contracts/launchpadv2/FRouterV3.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ contract FRouterV3 is
IBondingV5ForRouter public bondingV5;
IBondingConfigForRouter public bondingConfig;

struct TradeEventData {
address token;
address trader;
address pair;
bool isBuy;
uint256 amountIn;
uint256 amountOut;
uint256 amount;
uint256 taxFee;
uint256 antiSniperFee;
}

event PrivatePoolDrained(
address indexed token,
address indexed recipient,
Expand All @@ -67,6 +79,22 @@ contract FRouterV3 is
uint256 veTokenAmount
);

event TradeExecuted(
address indexed token,
address indexed trader,
address indexed pair,
bool isBuy,
address quoteAsset,
uint256 amountIn,
uint256 amountOut,
uint256 amount,
uint256 taxFee,
uint256 antiSniperFee,
uint256 reserveTokenAfter,
uint256 reserveAssetAfter,
uint256 lastPrice
);

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
Expand Down Expand Up @@ -184,6 +212,20 @@ contract FRouterV3 is

pair.swap(amountIn, 0, 0, amountOut);

_emitTradeExecuted(
TradeEventData({
token: tokenAddress,
trader: to,
pair: pairAddress,
isBuy: false,
amountIn: amountIn,
amountOut: amountOut,
amount: amount,
taxFee: normalTxFee,
antiSniperFee: antiSniperTxFee
})
);

return (amountIn, amountOut);
}

Expand Down Expand Up @@ -240,6 +282,20 @@ contract FRouterV3 is

IFPairV2(pair).swap(0, amountOut, amount, 0);

_emitTradeExecuted(
TradeEventData({
token: tokenAddress,
trader: to,
pair: pair,
isBuy: true,
amountIn: amountIn,
amountOut: amountOut,
amount: amount,
taxFee: normalTxFee,
antiSniperFee: antiSniperTxFee
})
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Launch buy mislabels indexed trader

Medium Severity

TradeExecuted sets the indexed trader to the router to address (token recipient). On launch(), the creator’s initial purchase calls _buy with address(this) as to, so the event attributes that buy to the bonding contract instead of the creator, while quote and token amounts still reflect a real trade.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7c34f35. Configure here.


return (amount, amountOut);
}

Expand Down Expand Up @@ -372,6 +428,30 @@ contract FRouterV3 is
return finalTaxStartTime;
}

function _emitTradeExecuted(TradeEventData memory trade) private {
IFPairV2 pair = IFPairV2(trade.pair);
(uint256 remainingForSale, uint256 totalRaised) = pair.getReserves();
uint256 lastPrice = remainingForSale == 0
? 0
: ((totalRaised * 1 ether) / remainingForSale); // last price multiplied by 10 * 10^18

emit TradeExecuted(
trade.token,
trade.trader,
trade.pair,
trade.isBuy,
assetToken,
trade.amountIn,
trade.amountOut,
trade.amount,
trade.taxFee,
trade.antiSniperFee,
remainingForSale,
totalRaised,
lastPrice
);
}

function hasAntiSniperTax(address pairAddress) public view returns (bool) {
return
_calculateAntiSniperBuyTax(pairAddress) > 0 ||
Expand Down
Loading