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
115 changes: 108 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,15 @@ 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
);

error InvalidTokenStatus();
error InvalidInput();
error SlippageTooHigh();
Expand All @@ -157,12 +181,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 +237,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 +284,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 +488,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_;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale fee delegation fields

Medium Severity

preLaunch now persists feeDelegationType and feeDelegationRecipient as on-chain source of truth, but setFeeDelegation still only updates isFeeDelegation. After an owner correction, the bool used for launch authorization can disagree with the stored type and recipient that backends read for vault setup.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3efab8f. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

isFeeDelegation is the top-level gate for fee delegation.


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

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

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

Expand Down
17 changes: 12 additions & 5 deletions test/launchpadv5/bondingV5.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const ACF_FEE = ethers.parseEther("10"); // Extra fee when needAcf = true (10 on
// Bonding curve params
const FAKE_INITIAL_VIRTUAL_LIQ = ethers.parseEther("6300");
const TARGET_REAL_VIRTUAL = ethers.parseEther("42000");
const ACF_FAKE_INITIAL_VIRTUAL_LIQ = ethers.parseEther("14000");
const { setupBondingV5Test } = require("./bondingV5Fixture.js");

describe("BondingV5", function () {
Expand Down Expand Up @@ -823,7 +824,9 @@ describe("BondingV5", function () {
targetRealVirtual: ethers.parseEther("50000"),
};

await bondingConfig.connect(owner).setBondingCurveParams(newParams);
await bondingConfig
.connect(owner)
.setBondingCurveParams(newParams, ACF_FAKE_INITIAL_VIRTUAL_LIQ);

const params = await bondingConfig.bondingCurveParams();
expect(params.fakeInitialVirtualLiq).to.equal(
Expand All @@ -832,10 +835,13 @@ describe("BondingV5", function () {
expect(params.targetRealVirtual).to.equal(newParams.targetRealVirtual);

// Reset to original
await bondingConfig.connect(owner).setBondingCurveParams({
fakeInitialVirtualLiq: FAKE_INITIAL_VIRTUAL_LIQ,
targetRealVirtual: TARGET_REAL_VIRTUAL,
});
await bondingConfig.connect(owner).setBondingCurveParams(
{
fakeInitialVirtualLiq: FAKE_INITIAL_VIRTUAL_LIQ,
targetRealVirtual: TARGET_REAL_VIRTUAL,
},
ACF_FAKE_INITIAL_VIRTUAL_LIQ
);
});

it("Should revert if non-owner tries to update params", async function () {
Expand Down Expand Up @@ -3324,6 +3330,7 @@ describe("BondingV5", function () {
fakeInitialVirtualLiq: FAKE_INITIAL_VIRTUAL_LIQ,
targetRealVirtual: TARGET_REAL_VIRTUAL,
},
ACF_FAKE_INITIAL_VIRTUAL_LIQ,
],
{ initializer: "initialize" }
);
Expand Down
2 changes: 1 addition & 1 deletion test/launchpadv5/bondingV5DrainLiquidity.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ describe("BondingV5 / FRouterV3 — drain liquidity (V5 suite)", function () {
{ initializer: "initialize" }
);
await freshRouter.waitForDeployment();
await freshRouter.grantRole(await freshRouter.EXECUTOR_ROLE(), admin.address);
await freshRouter.grantRole(await freshRouter.BE_OPS_ROLE(), admin.address);

await expect(
freshRouter
Expand Down
Loading