From 7c34f354c1c31bf671bae5d81394f25db1874d7d Mon Sep 17 00:00:00 2001 From: brianna Date: Fri, 3 Jul 2026 17:33:18 +0800 Subject: [PATCH 01/10] feat(launchpadv2): add indexing events (TradeExecuted, TokenCreated, MigrateExecuted) Additive, non-breaking on-chain events to support external indexing of bonding-curve activity. No function signatures or storage layout change. FRouterV3: - TradeExecuted after every buy/sell: token/trader/pair, isBuy, quote asset, amounts, fees (tax + anti-sniper), net trader quote, post-trade reserves, lastPrice. BondingV5: - TokenCreated creation snapshot: virtualId, creator, token, metadata, curve params (saleAmount, graduationThreshold, targetRaiseAmount, initialVirtualLiquidity, initialPrice), quoteAsset, pair, applicationId, launchParams, start time. - MigrateExecuted at graduation (caller, token, pair, agentToken, applicationId, assetAmount, tokenAmount). Verified: compiles; BondingV5 21.95 KiB < 24.576 KiB EIP-170 limit; storage layout unchanged. Co-Authored-By: Claude Opus 4.8 --- contracts/launchpadv2/BondingV5.sol | 78 +++++++++ contracts/launchpadv2/FRouterV3.sol | 85 ++++++++++ test/launchpadv5/bondingV5Fixture.js | 1 + test/launchpadv5/indexingEvents.js | 237 +++++++++++++++++++++++++++ 4 files changed, 401 insertions(+) create mode 100644 test/launchpadv5/indexingEvents.js diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index c7633068..df16e686 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -144,6 +144,43 @@ contract BondingV5 is event FeeDelegationUpdated(address indexed token, bool isFeeDelegation); + event TokenCreated( + uint256 virtualId, + address indexed creator, + address indexed token, + string name, + string symbol, + uint256 maxSupply, + uint256 saleAmount, + uint256 graduationThreshold, + uint256 targetRaiseAmount, + uint256 initialVirtualLiquidity, + uint256 initialPrice, + uint256 initialPurchase, + address quoteAsset, + address pair, + uint256 applicationId, + string description, + string image, + string twitter, + string telegram, + string youtube, + string website, + BondingConfig.LaunchParams launchParams, + uint256 startTime, + uint256 startTimeDelay + ); + + event MigrateExecuted( + address indexed caller, + address indexed token, + address pair, + address agentToken, + uint256 applicationId, + uint256 assetAmount, + uint256 tokenAmount + ); + error InvalidTokenStatus(); error InvalidInput(); error SlippageTooHigh(); @@ -423,6 +460,38 @@ contract BondingV5 is tokenLaunchParams[token] ); + // Net quote target at the graduation threshold (excludes buy tax / anti-sniper fees). + uint256 targetRaiseAmount = (liquidity * bondingCurveSupply) / + gradThreshold - + liquidity; + + emit TokenCreated( + tokenInfo[token].virtualId, + msg.sender, + token, + name_, + ticker_, + configInitialSupply * (10 ** IAgentTokenV4(token).decimals()), + bondingCurveSupply, + gradThreshold, + targetRaiseAmount, + liquidity * 2, + price, + initialPurchase, + assetToken, + pair, + applicationId, + desc_, + img_, + urls_[0], + urls_[1], + urls_[2], + urls_[3], + tokenLaunchParams[token], + actualStartTime, + actualStartTimeDelay + ); + return (token, pair, tokenInfo[token].virtualId, initialPurchase); } @@ -753,6 +822,15 @@ contract BondingV5 is // ); emit Graduated(tokenAddress_, agentToken); + emit MigrateExecuted( + msg.sender, + tokenAddress_, + pairAddress, + agentToken, + tokenRef.applicationId, + assetBalance, + tokenBalance + ); tokenRef.trading = false; tokenRef.tradingOnUniswap = true; } diff --git a/contracts/launchpadv2/FRouterV3.sol b/contracts/launchpadv2/FRouterV3.sol index c7ffe886..0ec6cfcf 100644 --- a/contracts/launchpadv2/FRouterV3.sol +++ b/contracts/launchpadv2/FRouterV3.sol @@ -52,6 +52,19 @@ contract FRouterV3 is IBondingV5ForRouter public bondingV5; IBondingConfigForRouter public bondingConfig; + struct TradeEventData { + address token; + address trader; + address pair; + bool isBuy; + uint256 amountIn; + uint256 tokenAmount; + uint256 curveQuoteAmount; + uint256 traderQuoteAmount; + uint256 taxFee; + uint256 antiSniperFee; + } + event PrivatePoolDrained( address indexed token, address indexed recipient, @@ -66,6 +79,23 @@ contract FRouterV3 is uint256 veTokenAmount ); + event TradeExecuted( + address indexed token, + address indexed trader, + address indexed pair, + bool isBuy, + address quoteAsset, + uint256 amountIn, + uint256 tokenAmount, + uint256 curveQuoteAmount, + uint256 traderQuoteAmount, + uint256 taxFee, + uint256 antiSniperFee, + uint256 reserveTokenAfter, + uint256 reserveAssetAfter, + uint256 lastPrice + ); + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -169,6 +199,21 @@ contract FRouterV3 is pair.swap(amountIn, 0, 0, amountOut); + _emitTradeExecuted( + TradeEventData({ + token: tokenAddress, + trader: to, + pair: pairAddress, + isBuy: false, + amountIn: amountIn, + tokenAmount: amountIn, + curveQuoteAmount: amountOut, + traderQuoteAmount: amount, + taxFee: txFee, + antiSniperFee: 0 + }) + ); + return (amountIn, amountOut); } @@ -225,6 +270,21 @@ contract FRouterV3 is IFPairV2(pair).swap(0, amountOut, amount, 0); + _emitTradeExecuted( + TradeEventData({ + token: tokenAddress, + trader: to, + pair: pair, + isBuy: true, + amountIn: amountIn, + tokenAmount: amountOut, + curveQuoteAmount: amount, + traderQuoteAmount: amountIn, + taxFee: normalTxFee, + antiSniperFee: antiSniperTxFee + }) + ); + return (amount, amountOut); } @@ -338,6 +398,31 @@ 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; + + emit TradeExecuted( + trade.token, + trade.trader, + trade.pair, + trade.isBuy, + assetToken, + trade.amountIn, + trade.tokenAmount, + trade.curveQuoteAmount, + trade.traderQuoteAmount, + trade.taxFee, + trade.antiSniperFee, + remainingForSale, + totalRaised, + lastPrice + ); + } + function hasAntiSniperTax(address pairAddress) public view returns (bool) { return _calculateAntiSniperTax(pairAddress) > 0; } diff --git a/test/launchpadv5/bondingV5Fixture.js b/test/launchpadv5/bondingV5Fixture.js index 846184a6..53a4cde9 100644 --- a/test/launchpadv5/bondingV5Fixture.js +++ b/test/launchpadv5/bondingV5Fixture.js @@ -353,6 +353,7 @@ async function setupBondingV5Test() { scheduledLaunchParams, deployParams, bondingCurveParams, + FAKE_INITIAL_VIRTUAL_LIQ, // acfFakeInitialVirtualLiq_ (8th arg added on current main) ], { initializer: "initialize" } ); diff --git a/test/launchpadv5/indexingEvents.js b/test/launchpadv5/indexingEvents.js new file mode 100644 index 00000000..6751d408 --- /dev/null +++ b/test/launchpadv5/indexingEvents.js @@ -0,0 +1,237 @@ +// Focused verification of the indexing events added to BondingV5 / FRouterV3. +// Walks one token through preLaunch -> launch -> buy -> sell -> graduation and +// asserts each new event fires with correct, internally-consistent values. +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const { time } = require("@nomicfoundation/hardhat-network-helpers"); +const { increaseTimeByMinutes } = require("../launchpadv2/util.js"); +const { START_TIME_DELAY } = require("../launchpadv2/const.js"); +const { setupBondingV5Test } = require("./bondingV5Fixture.js"); + +const LAUNCH_MODE_NORMAL = 0; +const ANTI_SNIPER_60S = 1; + +function parseOne(contract, receipt, name) { + for (const log of receipt.logs) { + try { + const p = contract.interface.parseLog(log); + if (p && p.name === name) return p; + } catch (e) {} + } + return undefined; +} + +describe("Indexing events (BondingV5 / FRouterV3)", function () { + let setup, contracts, accounts, addresses; + let tokenAddress, pairAddress, pair; + + before(async function () { + this.timeout(120000); + setup = await setupBondingV5Test(); + contracts = setup.contracts; + accounts = setup.accounts; + addresses = setup.addresses; + + // Fixture leaves graduationExcessBurnWallet unset (zero); set it so the + // graduation excess-burn transfer doesn't revert with TransferToZeroAddress. + await contracts.bondingConfig + .connect(accounts.owner) + .setGraduationExcessBurnWallet(ethers.Wallet.createRandom().address); + }); + + it("preLaunch emits TokenCreated with consistent values", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + const tx = await bondingV5.connect(user1).preLaunch( + "Idx Token", + "IDX", + [0, 1, 2], + "desc", + "https://example.com/i.png", + ["tw", "tg", "yt", "web"], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false, + "0x" + ); + const receipt = await tx.wait(); + + const pre = parseOne(bondingV5, receipt, "PreLaunched"); + expect(pre, "PreLaunched emitted").to.not.be.undefined; + tokenAddress = pre.args.token; + pairAddress = pre.args.pair; + pair = await ethers.getContractAt("IFPairV2", pairAddress); + + const tc = parseOne(bondingV5, receipt, "TokenCreated"); + expect(tc, "TokenCreated emitted").to.not.be.undefined; + expect(tc.args.creator).to.equal(user1.address); + expect(tc.args.token).to.equal(tokenAddress); + expect(tc.args.name).to.equal("Idx Token"); + expect(tc.args.symbol).to.equal("IDX"); + expect(tc.args.quoteAsset).to.equal(addresses.virtualToken); + expect(tc.args.pair).to.equal(pairAddress); + // v2 fields: virtualId matches PreLaunched, applicationId + virtual liquidity present + expect(tc.args.virtualId).to.equal(pre.args.virtualId); + expect(tc.args.applicationId).to.be.greaterThan(0n); + expect(tc.args.initialVirtualLiquidity).to.be.greaterThan(0n); + // curve sanity: sale supply above graduation threshold, both positive + expect(tc.args.saleAmount).to.be.greaterThan(0n); + expect(tc.args.graduationThreshold).to.be.greaterThan(0n); + expect(tc.args.saleAmount).to.be.greaterThan(tc.args.graduationThreshold); + // targetRaiseAmount computed without underflow/revert and is positive + expect(tc.args.targetRaiseAmount).to.be.greaterThan(0n); + expect(tc.args.initialPrice).to.be.greaterThan(0n); + expect(tc.args.twitter).to.equal("tw"); + expect(tc.args.website).to.equal("web"); + }); + + it("launch succeeds and enables trading", async function () { + const { bondingV5 } = contracts; + await time.increase(START_TIME_DELAY + 1); + const tx = await bondingV5.launch(tokenAddress); + const receipt = await tx.wait(); + + const launched = parseOne(bondingV5, receipt, "Launched"); + expect(launched, "Launched emitted").to.not.be.undefined; + expect(launched.args.token).to.equal(tokenAddress); + }); + + it("buy emits TradeExecuted with anti-sniper fee active + consistent reserves/price", async function () { + const { user2 } = accounts; + const { bondingV5, fRouterV3, virtualToken } = contracts; + + const token = await ethers.getContractAt("AgentTokenV4", tokenAddress); + const buyAmount = ethers.parseEther("100"); + await virtualToken.connect(user2).approve(addresses.fRouterV3, buyAmount); + + // Independent ground truth: actual balance deltas. + const vBefore = await virtualToken.balanceOf(user2.address); + const tBefore = await token.balanceOf(user2.address); + const taxVaultBefore = await virtualToken.balanceOf(addresses.taxVault); + const antiVaultBefore = await virtualToken.balanceOf(addresses.antiSniperTaxVault); + + const tx = await bondingV5.connect(user2).buy(buyAmount, tokenAddress, 0, (await time.latest()) + 300); + const receipt = await tx.wait(); + + const te = parseOne(fRouterV3, receipt, "TradeExecuted"); + expect(te, "TradeExecuted emitted").to.not.be.undefined; + expect(te.args.isBuy).to.equal(true); + expect(te.args.token).to.equal(tokenAddress); + expect(te.args.pair).to.equal(pairAddress); + expect(te.args.quoteAsset).to.equal(addresses.virtualToken); + // buy semantics: trader pays amountIn total; curve gets amountIn - taxes + expect(te.args.amountIn).to.equal(buyAmount); + expect(te.args.traderQuoteAmount).to.equal(buyAmount); + expect(te.args.taxFee + te.args.antiSniperFee + te.args.curveQuoteAmount).to.equal(buyAmount); + // anti-sniper window still open -> nonzero anti-sniper fee + expect(te.args.antiSniperFee).to.be.greaterThan(0n); + expect(te.args.tokenAmount).to.be.greaterThan(0n); + + // Ground-truth cross-checks: emitted amounts == actual tokens/VIRTUAL moved. + expect(vBefore - (await virtualToken.balanceOf(user2.address))).to.equal(te.args.amountIn); + expect((await token.balanceOf(user2.address)) - tBefore).to.equal(te.args.tokenAmount); + expect((await virtualToken.balanceOf(addresses.taxVault)) - taxVaultBefore).to.equal(te.args.taxFee); + expect((await virtualToken.balanceOf(addresses.antiSniperTaxVault)) - antiVaultBefore).to.equal(te.args.antiSniperFee); + + // reserves/price match the pair post-trade + const [rTok, rAsset] = await pair.getReserves(); + expect(te.args.reserveTokenAfter).to.equal(rTok); + expect(te.args.reserveAssetAfter).to.equal(rAsset); + const expectedPrice = rTok === 0n ? 0n : (rAsset * ethers.parseEther("1")) / rTok; + expect(te.args.lastPrice).to.equal(expectedPrice); + }); + + it("buy after anti-sniper window: antiSniperFee == 0", async function () { + const { user2 } = accounts; + const { bondingV5, fRouterV3, virtualToken } = contracts; + await increaseTimeByMinutes(99); + + const buyAmount = ethers.parseEther("100"); + await virtualToken.connect(user2).approve(addresses.fRouterV3, buyAmount); + const tx = await bondingV5.connect(user2).buy(buyAmount, tokenAddress, 0, (await time.latest()) + 300); + const receipt = await tx.wait(); + + const te = parseOne(fRouterV3, receipt, "TradeExecuted"); + expect(te).to.not.be.undefined; + expect(te.args.isBuy).to.equal(true); + expect(te.args.antiSniperFee).to.equal(0n); + }); + + it("sell emits TradeExecuted with isBuy=false and correct net/tax split", async function () { + const { user2 } = accounts; + const { bondingV5, fRouterV3, virtualToken } = contracts; + + const token = await ethers.getContractAt("AgentTokenV4", tokenAddress); + const bal = await token.balanceOf(user2.address); + const sellAmount = bal / 4n; + await token.connect(user2).approve(addresses.fRouterV3, sellAmount); + + const vBefore = await virtualToken.balanceOf(user2.address); + const tBefore = await token.balanceOf(user2.address); + const taxVaultBefore = await virtualToken.balanceOf(addresses.taxVault); + + const tx = await bondingV5.connect(user2).sell(sellAmount, tokenAddress, 0, (await time.latest()) + 300); + const receipt = await tx.wait(); + + const te = parseOne(fRouterV3, receipt, "TradeExecuted"); + expect(te, "TradeExecuted emitted").to.not.be.undefined; + expect(te.args.isBuy).to.equal(false); + // sell semantics: amountIn and tokenAmount are the token sold + expect(te.args.amountIn).to.equal(sellAmount); + expect(te.args.tokenAmount).to.equal(sellAmount); + expect(te.args.antiSniperFee).to.equal(0n); + // net received = gross curve quote - tax + expect(te.args.traderQuoteAmount).to.equal(te.args.curveQuoteAmount - te.args.taxFee); + + // Ground-truth cross-checks: emitted amounts == actual tokens/VIRTUAL moved. + expect(tBefore - (await token.balanceOf(user2.address))).to.equal(te.args.tokenAmount); + expect((await virtualToken.balanceOf(user2.address)) - vBefore).to.equal(te.args.traderQuoteAmount); + expect((await virtualToken.balanceOf(addresses.taxVault)) - taxVaultBefore).to.equal(te.args.taxFee); + + const [rTok, rAsset] = await pair.getReserves(); + expect(te.args.reserveTokenAfter).to.equal(rTok); + expect(te.args.reserveAssetAfter).to.equal(rAsset); + }); + + it("graduation emits Graduated + MigrateExecuted + a final TradeExecuted", async function () { + const { user1 } = accounts; + const { bondingV5, fRouterV3, virtualToken } = contracts; + + // Buy well past the graduation threshold (target raise ~42000 VT). + const bigBuy = ethers.parseEther("100000"); + await virtualToken.connect(user1).approve(addresses.fRouterV3, bigBuy); + const tx = await bondingV5.connect(user1).buy(bigBuy, tokenAddress, 0, (await time.latest()) + 300); + const receipt = await tx.wait(); + + // The triggering buy still emits its TradeExecuted before graduation. + const te = parseOne(fRouterV3, receipt, "TradeExecuted"); + expect(te, "TradeExecuted emitted on graduating buy").to.not.be.undefined; + expect(te.args.isBuy).to.equal(true); + + const grad = parseOne(bondingV5, receipt, "Graduated"); + expect(grad, "Graduated emitted").to.not.be.undefined; + expect(grad.args.token).to.equal(tokenAddress); + const agentToken = grad.args.agentToken; + expect(agentToken).to.not.equal(ethers.ZeroAddress); + + const me = parseOne(bondingV5, receipt, "MigrateExecuted"); + expect(me, "MigrateExecuted emitted").to.not.be.undefined; + expect(me.args.token).to.equal(tokenAddress); + expect(me.args.pair).to.equal(pairAddress); + expect(me.args.agentToken).to.equal(agentToken); + expect(me.args.assetAmount).to.be.greaterThan(0n); + expect(me.args.tokenAmount).to.be.greaterThan(0n); + + const info = await bondingV5.tokenInfo(tokenAddress); + expect(info.tradingOnUniswap).to.equal(true); + }); +}); From b7df3ae03ecc583e1a35d9702d7df8cf85e05f7a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 03:01:45 +0000 Subject: [PATCH 02/10] refactor(launchpadv2): drop telegram and youtube from TokenCreated event Remove the telegram and youtube fields from the TokenCreated event and its emit in preLaunch. These social URLs are already stored on-chain in tokenInfo and are not needed in the indexing log. twitter and website are retained. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01UN11rp5HvZkdg7EePTrgrP --- contracts/launchpadv2/BondingV5.sol | 4 ---- 1 file changed, 4 deletions(-) diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index df16e686..b63d4431 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -163,8 +163,6 @@ contract BondingV5 is string description, string image, string twitter, - string telegram, - string youtube, string website, BondingConfig.LaunchParams launchParams, uint256 startTime, @@ -484,8 +482,6 @@ contract BondingV5 is desc_, img_, urls_[0], - urls_[1], - urls_[2], urls_[3], tokenLaunchParams[token], actualStartTime, From b8674f8b585e5c96ee3463524824195d1642d6f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 03:25:42 +0000 Subject: [PATCH 03/10] refactor(launchpadv2): rename TradeExecuted amount fields, drop MigrateExecuted TradeExecuted: collapse the four amount fields into three direction-aware fields (amountIn/amountOut/amount), removing the per-direction duplicate (amountIn == traderQuoteAmount on buys, == tokenAmount on sells): - buy: amountIn = quote paid, amountOut = token received, amount = quote into curve after tax - sell: amountIn = token sold, amountOut = gross quote out, amount = net quote to trader after tax MigrateExecuted: removed. It largely duplicated the existing Graduated event (same token/agentToken, same transaction); the remaining fields are recoverable from Graduated, TokenCreated, or storage. Tests updated to the new field names and event set. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01UN11rp5HvZkdg7EePTrgrP --- contracts/launchpadv2/BondingV5.sol | 19 ----------------- contracts/launchpadv2/FRouterV3.sol | 25 +++++++++------------- test/launchpadv5/indexingEvents.js | 32 +++++++++++------------------ 3 files changed, 22 insertions(+), 54 deletions(-) diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index b63d4431..1a40b976 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -169,16 +169,6 @@ contract BondingV5 is uint256 startTimeDelay ); - event MigrateExecuted( - address indexed caller, - address indexed token, - address pair, - address agentToken, - uint256 applicationId, - uint256 assetAmount, - uint256 tokenAmount - ); - error InvalidTokenStatus(); error InvalidInput(); error SlippageTooHigh(); @@ -818,15 +808,6 @@ contract BondingV5 is // ); emit Graduated(tokenAddress_, agentToken); - emit MigrateExecuted( - msg.sender, - tokenAddress_, - pairAddress, - agentToken, - tokenRef.applicationId, - assetBalance, - tokenBalance - ); tokenRef.trading = false; tokenRef.tradingOnUniswap = true; } diff --git a/contracts/launchpadv2/FRouterV3.sol b/contracts/launchpadv2/FRouterV3.sol index 0ec6cfcf..5e37f233 100644 --- a/contracts/launchpadv2/FRouterV3.sol +++ b/contracts/launchpadv2/FRouterV3.sol @@ -58,9 +58,8 @@ contract FRouterV3 is address pair; bool isBuy; uint256 amountIn; - uint256 tokenAmount; - uint256 curveQuoteAmount; - uint256 traderQuoteAmount; + uint256 amountOut; + uint256 amount; uint256 taxFee; uint256 antiSniperFee; } @@ -86,9 +85,8 @@ contract FRouterV3 is bool isBuy, address quoteAsset, uint256 amountIn, - uint256 tokenAmount, - uint256 curveQuoteAmount, - uint256 traderQuoteAmount, + uint256 amountOut, + uint256 amount, uint256 taxFee, uint256 antiSniperFee, uint256 reserveTokenAfter, @@ -206,9 +204,8 @@ contract FRouterV3 is pair: pairAddress, isBuy: false, amountIn: amountIn, - tokenAmount: amountIn, - curveQuoteAmount: amountOut, - traderQuoteAmount: amount, + amountOut: amountOut, + amount: amount, taxFee: txFee, antiSniperFee: 0 }) @@ -277,9 +274,8 @@ contract FRouterV3 is pair: pair, isBuy: true, amountIn: amountIn, - tokenAmount: amountOut, - curveQuoteAmount: amount, - traderQuoteAmount: amountIn, + amountOut: amountOut, + amount: amount, taxFee: normalTxFee, antiSniperFee: antiSniperTxFee }) @@ -412,9 +408,8 @@ contract FRouterV3 is trade.isBuy, assetToken, trade.amountIn, - trade.tokenAmount, - trade.curveQuoteAmount, - trade.traderQuoteAmount, + trade.amountOut, + trade.amount, trade.taxFee, trade.antiSniperFee, remainingForSale, diff --git a/test/launchpadv5/indexingEvents.js b/test/launchpadv5/indexingEvents.js index 6751d408..db2a6ce0 100644 --- a/test/launchpadv5/indexingEvents.js +++ b/test/launchpadv5/indexingEvents.js @@ -128,17 +128,17 @@ describe("Indexing events (BondingV5 / FRouterV3)", function () { expect(te.args.token).to.equal(tokenAddress); expect(te.args.pair).to.equal(pairAddress); expect(te.args.quoteAsset).to.equal(addresses.virtualToken); - // buy semantics: trader pays amountIn total; curve gets amountIn - taxes + // buy semantics: trader pays amountIn total; curve gets `amount` = amountIn - taxes; + // amountOut is the token received by the trader. expect(te.args.amountIn).to.equal(buyAmount); - expect(te.args.traderQuoteAmount).to.equal(buyAmount); - expect(te.args.taxFee + te.args.antiSniperFee + te.args.curveQuoteAmount).to.equal(buyAmount); + expect(te.args.taxFee + te.args.antiSniperFee + te.args.amount).to.equal(buyAmount); // anti-sniper window still open -> nonzero anti-sniper fee expect(te.args.antiSniperFee).to.be.greaterThan(0n); - expect(te.args.tokenAmount).to.be.greaterThan(0n); + expect(te.args.amountOut).to.be.greaterThan(0n); // Ground-truth cross-checks: emitted amounts == actual tokens/VIRTUAL moved. expect(vBefore - (await virtualToken.balanceOf(user2.address))).to.equal(te.args.amountIn); - expect((await token.balanceOf(user2.address)) - tBefore).to.equal(te.args.tokenAmount); + expect((await token.balanceOf(user2.address)) - tBefore).to.equal(te.args.amountOut); expect((await virtualToken.balanceOf(addresses.taxVault)) - taxVaultBefore).to.equal(te.args.taxFee); expect((await virtualToken.balanceOf(addresses.antiSniperTaxVault)) - antiVaultBefore).to.equal(te.args.antiSniperFee); @@ -185,16 +185,16 @@ describe("Indexing events (BondingV5 / FRouterV3)", function () { const te = parseOne(fRouterV3, receipt, "TradeExecuted"); expect(te, "TradeExecuted emitted").to.not.be.undefined; expect(te.args.isBuy).to.equal(false); - // sell semantics: amountIn and tokenAmount are the token sold + // sell semantics: amountIn is the token sold; amountOut is the gross quote out of + // the curve; amount is the net quote received by the trader after tax. expect(te.args.amountIn).to.equal(sellAmount); - expect(te.args.tokenAmount).to.equal(sellAmount); expect(te.args.antiSniperFee).to.equal(0n); - // net received = gross curve quote - tax - expect(te.args.traderQuoteAmount).to.equal(te.args.curveQuoteAmount - te.args.taxFee); + // net received (amount) = gross curve quote (amountOut) - tax + expect(te.args.amount).to.equal(te.args.amountOut - te.args.taxFee); // Ground-truth cross-checks: emitted amounts == actual tokens/VIRTUAL moved. - expect(tBefore - (await token.balanceOf(user2.address))).to.equal(te.args.tokenAmount); - expect((await virtualToken.balanceOf(user2.address)) - vBefore).to.equal(te.args.traderQuoteAmount); + expect(tBefore - (await token.balanceOf(user2.address))).to.equal(te.args.amountIn); + expect((await virtualToken.balanceOf(user2.address)) - vBefore).to.equal(te.args.amount); expect((await virtualToken.balanceOf(addresses.taxVault)) - taxVaultBefore).to.equal(te.args.taxFee); const [rTok, rAsset] = await pair.getReserves(); @@ -202,7 +202,7 @@ describe("Indexing events (BondingV5 / FRouterV3)", function () { expect(te.args.reserveAssetAfter).to.equal(rAsset); }); - it("graduation emits Graduated + MigrateExecuted + a final TradeExecuted", async function () { + it("graduation emits Graduated + a final TradeExecuted", async function () { const { user1 } = accounts; const { bondingV5, fRouterV3, virtualToken } = contracts; @@ -223,14 +223,6 @@ describe("Indexing events (BondingV5 / FRouterV3)", function () { const agentToken = grad.args.agentToken; expect(agentToken).to.not.equal(ethers.ZeroAddress); - const me = parseOne(bondingV5, receipt, "MigrateExecuted"); - expect(me, "MigrateExecuted emitted").to.not.be.undefined; - expect(me.args.token).to.equal(tokenAddress); - expect(me.args.pair).to.equal(pairAddress); - expect(me.args.agentToken).to.equal(agentToken); - expect(me.args.assetAmount).to.be.greaterThan(0n); - expect(me.args.tokenAmount).to.be.greaterThan(0n); - const info = await bondingV5.tokenInfo(tokenAddress); expect(info.tradingOnUniswap).to.equal(true); }); From d8a8674fc1c20b7b7e082d147eb93d1060987ecf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 03:43:48 +0000 Subject: [PATCH 04/10] Revert TokenCreated URL trim; keep all preLaunch metadata URLs Restore telegram and youtube to the TokenCreated event and its emit so preLaunch's creation snapshot is unchanged from the original proposal (twitter, telegram, youtube, website all retained). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01UN11rp5HvZkdg7EePTrgrP --- contracts/launchpadv2/BondingV5.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index 1a40b976..53b6f5b6 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -163,6 +163,8 @@ contract BondingV5 is string description, string image, string twitter, + string telegram, + string youtube, string website, BondingConfig.LaunchParams launchParams, uint256 startTime, @@ -472,6 +474,8 @@ contract BondingV5 is desc_, img_, urls_[0], + urls_[1], + urls_[2], urls_[3], tokenLaunchParams[token], actualStartTime, From ebc45da6d4731533d62ae100f6fb37c9923135b4 Mon Sep 17 00:00:00 2001 From: brianna Date: Mon, 6 Jul 2026 21:01:26 +0800 Subject: [PATCH 05/10] refactor(launchpadv2): drop all social URL fields from TokenCreated event Remove twitter, telegram, youtube and website from the TokenCreated event and its emit in preLaunch. The social URLs remain stored on-chain in tokenInfo; they are not needed in the indexing log. Co-Authored-By: Claude Fable 5 --- contracts/launchpadv2/BondingV5.sol | 8 -------- test/launchpadv5/indexingEvents.js | 2 -- 2 files changed, 10 deletions(-) diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index 53b6f5b6..f4af81f2 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -162,10 +162,6 @@ contract BondingV5 is uint256 applicationId, string description, string image, - string twitter, - string telegram, - string youtube, - string website, BondingConfig.LaunchParams launchParams, uint256 startTime, uint256 startTimeDelay @@ -473,10 +469,6 @@ contract BondingV5 is applicationId, desc_, img_, - urls_[0], - urls_[1], - urls_[2], - urls_[3], tokenLaunchParams[token], actualStartTime, actualStartTimeDelay diff --git a/test/launchpadv5/indexingEvents.js b/test/launchpadv5/indexingEvents.js index db2a6ce0..c76f5902 100644 --- a/test/launchpadv5/indexingEvents.js +++ b/test/launchpadv5/indexingEvents.js @@ -90,8 +90,6 @@ describe("Indexing events (BondingV5 / FRouterV3)", function () { // targetRaiseAmount computed without underflow/revert and is positive expect(tc.args.targetRaiseAmount).to.be.greaterThan(0n); expect(tc.args.initialPrice).to.be.greaterThan(0n); - expect(tc.args.twitter).to.equal("tw"); - expect(tc.args.website).to.equal("web"); }); it("launch succeeds and enables trading", async function () { From a7adaf99d66b2f4e26bf4971bb70195372b98c0d Mon Sep 17 00:00:00 2001 From: Weixiong Tay Date: Tue, 7 Jul 2026 12:31:25 +0800 Subject: [PATCH 06/10] feat: update index events --- contracts/launchpadv2/BondingV5.sol | 8 -------- contracts/launchpadv2/FRouterV3.sol | 6 +++--- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index f4af81f2..35ee04e4 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -153,13 +153,11 @@ contract BondingV5 is uint256 maxSupply, uint256 saleAmount, uint256 graduationThreshold, - uint256 targetRaiseAmount, uint256 initialVirtualLiquidity, uint256 initialPrice, uint256 initialPurchase, address quoteAsset, address pair, - uint256 applicationId, string description, string image, BondingConfig.LaunchParams launchParams, @@ -447,10 +445,6 @@ contract BondingV5 is ); // Net quote target at the graduation threshold (excludes buy tax / anti-sniper fees). - uint256 targetRaiseAmount = (liquidity * bondingCurveSupply) / - gradThreshold - - liquidity; - emit TokenCreated( tokenInfo[token].virtualId, msg.sender, @@ -460,13 +454,11 @@ contract BondingV5 is configInitialSupply * (10 ** IAgentTokenV4(token).decimals()), bondingCurveSupply, gradThreshold, - targetRaiseAmount, liquidity * 2, price, initialPurchase, assetToken, pair, - applicationId, desc_, img_, tokenLaunchParams[token], diff --git a/contracts/launchpadv2/FRouterV3.sol b/contracts/launchpadv2/FRouterV3.sol index 812b9cca..78cecf6a 100644 --- a/contracts/launchpadv2/FRouterV3.sol +++ b/contracts/launchpadv2/FRouterV3.sol @@ -221,8 +221,8 @@ contract FRouterV3 is amountIn: amountIn, amountOut: amountOut, amount: amount, - taxFee: txFee, - antiSniperFee: 0 + taxFee: normalTxFee, + antiSniperFee: antiSniperTxFee }) ); @@ -433,7 +433,7 @@ contract FRouterV3 is (uint256 remainingForSale, uint256 totalRaised) = pair.getReserves(); uint256 lastPrice = remainingForSale == 0 ? 0 - : (totalRaised * 1 ether) / remainingForSale; + : ((totalRaised * 1 ether) / remainingForSale); // last price multiplied by 10 * 10^18 emit TradeExecuted( trade.token, From 3efab8f885736a0965d33d365eec5f4dcdb98c74 Mon Sep 17 00:00:00 2001 From: jaime wang Date: Thu, 30 Jul 2026 14:48:17 +0800 Subject: [PATCH 07/10] feat: add more launch options in extparam --- contracts/launchpadv2/BondingV5.sol | 109 ++++++++- test/launchpadv5/bondingV5.js | 17 +- test/launchpadv5/bondingV5DrainLiquidity.js | 2 +- test/launchpadv5/bondingV5ExtParams.js | 235 ++++++++++++++++++++ test/launchpadv5/bondingV5Fixture.js | 10 + test/launchpadv5/bondingV5Tax.fixture.js | 5 + 6 files changed, 365 insertions(+), 13 deletions(-) create mode 100644 test/launchpadv5/bondingV5ExtParams.js diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index 431ce3ca..c61cbd9d 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -114,11 +114,25 @@ 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 Raw fee delegation recipient bytes from the `extParams_` trailing segment: + /// a 20-byte address for type 1; the raw twitter account id bytes for type 2 + /// (the recipient vault is only created just before `launch`). Empty when no delegation. + /// @dev MUST remain the last state variable (append-only layout). + mapping(address => bytes) public feeDelegationRecipient; + event PreLaunched( address indexed token, address indexed pair, @@ -144,6 +158,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, + bytes feeDelegationRecipient + ); + error InvalidTokenStatus(); error InvalidInput(); error SlippageTooHigh(); @@ -157,12 +180,34 @@ 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 segment (`extParams[32:]`, present only when needed) carries the variable-length + /// `feeDelegationRecipient`, ABI-encoded as `abi.encode(uint256 flags, bytes recipient)`: + /// - type 1 (address): the 20-byte recipient address + /// - type 2 (twitter): the raw twitter account id bytes + /// Future variable-length fields are appended after this 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 + uint256 private constant EXT_PARAMS_FEE_DELEGATION_TYPE_SHIFT = 3; // bits 3-4 + uint256 private constant EXT_PARAMS_FEE_DELEGATION_TYPE_MASK = 0x3; function _loadExtParamsWord( bytes calldata extParams @@ -187,6 +232,37 @@ 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` bytes when present. Length-gated so legacy + /// flags-only payloads (length ⇐ 32) and future longer payloads both decode safely; extra + /// trailing tuple elements added later do not affect this decode. + function _decodeFeeDelegationRecipient( + bytes calldata extParams + ) internal pure returns (bytes memory) { + // abi.encode(uint256, bytes) is at least 3 words (flags + offset + length). + if (extParams.length < 96) { + return ""; + } + (, bytes memory recipient) = abi.decode(extParams, (uint256, bytes)); + return recipient; + } + function initialize( address factory_, address router_, @@ -202,8 +278,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_, @@ -403,6 +482,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_); + bytes memory feeDelegationRecipient_ = _decodeFeeDelegationRecipient(extParams_); + isRobotics[token] = isRobotics_; + feeDelegationType[token] = feeDelegationType_; + feeDelegationRecipient[token] = feeDelegationRecipient_; + // Set Data struct fields newToken.data.token = token; newToken.data.name = tokenName; @@ -435,6 +522,14 @@ contract BondingV5 is tokenLaunchParams[token] ); + emit PreLaunchExtParams( + token, + isFeeDelegation_, + isRobotics_, + feeDelegationType_, + feeDelegationRecipient_ + ); + return (token, pair, tokenInfo[token].virtualId, initialPurchase); } diff --git a/test/launchpadv5/bondingV5.js b/test/launchpadv5/bondingV5.js index fb7e27d2..db268537 100644 --- a/test/launchpadv5/bondingV5.js +++ b/test/launchpadv5/bondingV5.js @@ -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 () { @@ -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( @@ -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 () { @@ -3324,6 +3330,7 @@ describe("BondingV5", function () { fakeInitialVirtualLiq: FAKE_INITIAL_VIRTUAL_LIQ, targetRealVirtual: TARGET_REAL_VIRTUAL, }, + ACF_FAKE_INITIAL_VIRTUAL_LIQ, ], { initializer: "initialize" } ); diff --git a/test/launchpadv5/bondingV5DrainLiquidity.js b/test/launchpadv5/bondingV5DrainLiquidity.js index 211fea65..09c06662 100644 --- a/test/launchpadv5/bondingV5DrainLiquidity.js +++ b/test/launchpadv5/bondingV5DrainLiquidity.js @@ -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 diff --git a/test/launchpadv5/bondingV5ExtParams.js b/test/launchpadv5/bondingV5ExtParams.js new file mode 100644 index 00000000..f90a6ae0 --- /dev/null +++ b/test/launchpadv5/bondingV5ExtParams.js @@ -0,0 +1,235 @@ +/** + * BondingV5 `extParams` v3: append-only, backward-compatible encoding that carries + * isFeeDelegation (bit 0), skipSuffix (bit 1), isRobotics (bit 2), + * feeDelegationType (bits 3-4) and an optional trailing feeDelegationRecipient. + */ +const { expect } = require("chai"); +const { ethers } = require("hardhat"); +const { time } = require("@nomicfoundation/hardhat-network-helpers"); +const { + loadFixture, +} = require("@nomicfoundation/hardhat-toolbox/network-helpers"); +const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); + +const { START_TIME_DELAY } = require("../launchpadv2/const.js"); +const { setupV2V3TaxComparisonTest } = require("./bondingV5Tax.fixture.js"); + +const LAUNCH_MODE_NORMAL = 0; +const ANTI_SNIPER_60S = 1; + +const FLAG_FEE_DELEGATION = 1n; +const FLAG_SKIP_SUFFIX = 2n; +const FLAG_ROBOTICS = 4n; +const FEE_DELEGATION_TYPE_SHIFT = 3n; + +const FEE_DELEGATION_TYPE_ADDRESS = 1; +const FEE_DELEGATION_TYPE_TWITTER = 2; + +function buildFlags({ + isFeeDelegation = false, + skipSuffix = false, + isRobotics = false, + feeDelegationType = 0, +} = {}) { + let word = 0n; + if (isFeeDelegation) word |= FLAG_FEE_DELEGATION; + if (skipSuffix) word |= FLAG_SKIP_SUFFIX; + if (isRobotics) word |= FLAG_ROBOTICS; + word |= (BigInt(feeDelegationType) & 0x3n) << FEE_DELEGATION_TYPE_SHIFT; + return word; +} + +/** Flags-only payload (single word). */ +function encodeFlags(opts) { + return ethers.AbiCoder.defaultAbiCoder().encode(["uint256"], [buildFlags(opts)]); +} + +/** Flags + trailing recipient: abi.encode(uint256 flags, bytes recipient). */ +function encodeFlagsWithRecipient(opts, recipientBytes) { + return ethers.AbiCoder.defaultAbiCoder().encode( + ["uint256", "bytes"], + [buildFlags(opts), recipientBytes] + ); +} + +/** Legacy V1 encoding: abi.encode(bool isFeeDelegation). */ +function encodeLegacyBool(isFeeDelegation) { + return ethers.AbiCoder.defaultAbiCoder().encode(["bool"], [isFeeDelegation]); +} + +async function extParamsFixture() { + return setupV2V3TaxComparisonTest({ includeBondingV4: false }); +} + +describe("BondingV5 extParams — v3 launch settings (robotics + fee delegation)", function () { + let contracts; + let user2; + + before(async function () { + const setup = await loadFixture(extParamsFixture); + contracts = setup.contracts; + user2 = setup.accounts.user2; + }); + + async function preLaunchWithExtParams(extParamsHex) { + const { bondingV5, virtualToken } = contracts; + + await virtualToken + .connect(user2) + .approve(await bondingV5.getAddress(), ethers.MaxUint256); + + const purchaseAmount = ethers.parseEther("1000"); + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + const tx = await bondingV5.connect(user2).preLaunch( + "ExtParams Token", + "EXT", + [0, 1, 2], + "desc", + "https://example.com/i.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false, + extParamsHex + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; + } catch { + return false; + } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + return { tokenAddress, startTime }; + } + + it("defaults to all-off for empty extParams (backward compatible)", async function () { + const { bondingV5 } = contracts; + const { tokenAddress } = await preLaunchWithExtParams("0x"); + + expect(await bondingV5.isFeeDelegation(tokenAddress)).to.equal(false); + expect(await bondingV5.isRobotics(tokenAddress)).to.equal(false); + expect(await bondingV5.feeDelegationType(tokenAddress)).to.equal(0); + expect(await bondingV5.feeDelegationRecipient(tokenAddress)).to.equal("0x"); + }); + + it("keeps legacy abi.encode(bool) working with new fields defaulted", async function () { + const { bondingV5 } = contracts; + const { tokenAddress } = await preLaunchWithExtParams(encodeLegacyBool(true)); + + expect(await bondingV5.isFeeDelegation(tokenAddress)).to.equal(true); + expect(await bondingV5.isRobotics(tokenAddress)).to.equal(false); + expect(await bondingV5.feeDelegationType(tokenAddress)).to.equal(0); + expect(await bondingV5.feeDelegationRecipient(tokenAddress)).to.equal("0x"); + }); + + it("decodes isRobotics from bit 2", async function () { + const { bondingV5 } = contracts; + const { tokenAddress } = await preLaunchWithExtParams( + encodeFlags({ isRobotics: true }) + ); + + expect(await bondingV5.isRobotics(tokenAddress)).to.equal(true); + expect(await bondingV5.isFeeDelegation(tokenAddress)).to.equal(false); + }); + + it("stores address-type fee delegation recipient", async function () { + const { bondingV5 } = contracts; + const recipient = ethers.getAddress( + "0x00000000000000000000000000000000000000aa" + ); + const recipientBytes = ethers.hexlify(ethers.getBytes(recipient)); + + const extParams = encodeFlagsWithRecipient( + { isFeeDelegation: true, feeDelegationType: FEE_DELEGATION_TYPE_ADDRESS }, + recipientBytes + ); + const { tokenAddress } = await preLaunchWithExtParams(extParams); + + expect(await bondingV5.isFeeDelegation(tokenAddress)).to.equal(true); + expect(await bondingV5.feeDelegationType(tokenAddress)).to.equal( + FEE_DELEGATION_TYPE_ADDRESS + ); + expect(await bondingV5.feeDelegationRecipient(tokenAddress)).to.equal( + recipientBytes + ); + }); + + it("stores raw twitter id bytes as fee delegation recipient", async function () { + const { bondingV5 } = contracts; + const twitterId = "1234567890"; + const recipientBytes = ethers.hexlify(ethers.toUtf8Bytes(twitterId)); + + const extParams = encodeFlagsWithRecipient( + { isFeeDelegation: true, feeDelegationType: FEE_DELEGATION_TYPE_TWITTER }, + recipientBytes + ); + const { tokenAddress } = await preLaunchWithExtParams(extParams); + + expect(await bondingV5.feeDelegationType(tokenAddress)).to.equal( + FEE_DELEGATION_TYPE_TWITTER + ); + expect(await bondingV5.feeDelegationRecipient(tokenAddress)).to.equal( + recipientBytes + ); + expect(ethers.toUtf8String(await bondingV5.feeDelegationRecipient(tokenAddress))).to.equal( + twitterId + ); + }); + + it("emits PreLaunchExtParams with the decoded settings", async function () { + const { bondingV5, virtualToken } = contracts; + const recipientBytes = ethers.hexlify(ethers.toUtf8Bytes("42")); + const extParams = encodeFlagsWithRecipient( + { + isFeeDelegation: true, + isRobotics: true, + feeDelegationType: FEE_DELEGATION_TYPE_TWITTER, + }, + recipientBytes + ); + + await virtualToken + .connect(user2) + .approve(await bondingV5.getAddress(), ethers.MaxUint256); + const purchaseAmount = ethers.parseEther("1000"); + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + await expect( + bondingV5 + .connect(user2) + .preLaunch( + "ExtParams Token", + "EXT", + [0, 1, 2], + "desc", + "https://example.com/i.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false, + extParams + ) + ) + .to.emit(bondingV5, "PreLaunchExtParams") + .withArgs( + anyValue, + true, + true, + FEE_DELEGATION_TYPE_TWITTER, + recipientBytes + ); + }); +}); diff --git a/test/launchpadv5/bondingV5Fixture.js b/test/launchpadv5/bondingV5Fixture.js index 846184a6..75efb337 100644 --- a/test/launchpadv5/bondingV5Fixture.js +++ b/test/launchpadv5/bondingV5Fixture.js @@ -23,6 +23,7 @@ const NORMAL_LAUNCH_FEE = ethers.parseEther("100"); const ACF_FEE = ethers.parseEther("10"); const FAKE_INITIAL_VIRTUAL_LIQ = ethers.parseEther("6300"); const TARGET_REAL_VIRTUAL = ethers.parseEther("42000"); +const ACF_FAKE_INITIAL_VIRTUAL_LIQ = ethers.parseEther("14000"); async function setupBondingV5Test() { const setup = {}; @@ -353,6 +354,7 @@ async function setupBondingV5Test() { scheduledLaunchParams, deployParams, bondingCurveParams, + ACF_FAKE_INITIAL_VIRTUAL_LIQ, ], { initializer: "initialize" } ); @@ -363,6 +365,10 @@ async function setupBondingV5Test() { await bondingConfig.setPrivilegedLauncher(owner.address, true); console.log("setPrivilegedLauncher(true) for owner (test default backend)"); + // Graduation transfers bonding-curve excess tokens here to be burned off-chain + await bondingConfig.setGraduationExcessBurnWallet(beOpsWallet.address); + console.log("graduationExcessBurnWallet set to beOpsWallet"); + // 4.2 Deploy BondingV5 console.log("\n--- Deploying BondingV5 ---"); const BondingV5 = await ethers.getContractFactory("BondingV5"); @@ -440,6 +446,10 @@ async function setupBondingV5Test() { await fRouterV3.grantRole(await fRouterV3.EXECUTOR_ROLE(), admin.address); console.log("EXECUTOR_ROLE granted to admin in FRouterV3"); + // drainPrivatePool / drainUniV2Pool are gated by BE_OPS_ROLE + await fRouterV3.grantRole(await fRouterV3.BE_OPS_ROLE(), admin.address); + console.log("BE_OPS_ROLE granted to admin in FRouterV3"); + await agentFactoryV7.grantRole( await agentFactoryV7.REMOVE_LIQUIDITY_ROLE(), await fRouterV3.getAddress() diff --git a/test/launchpadv5/bondingV5Tax.fixture.js b/test/launchpadv5/bondingV5Tax.fixture.js index eb3cdf60..ec72f697 100644 --- a/test/launchpadv5/bondingV5Tax.fixture.js +++ b/test/launchpadv5/bondingV5Tax.fixture.js @@ -26,6 +26,7 @@ const ACF_FEE = ethers.parseEther("10"); 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 BONDING_V4_FEE = 10; @@ -296,11 +297,15 @@ async function setupV2V3TaxComparisonTest(options = {}) { fakeInitialVirtualLiq: FAKE_INITIAL_VIRTUAL_LIQ, targetRealVirtual: TARGET_REAL_VIRTUAL, }, + ACF_FAKE_INITIAL_VIRTUAL_LIQ, ], { initializer: "initialize" } ); await bondingConfig.waitForDeployment(); + // Graduation transfers bonding-curve excess tokens here to be burned off-chain + await bondingConfig.setGraduationExcessBurnWallet(beOpsWallet.address); + let bondingV4; if (includeBondingV4) { console.log("\n--- Deploying BondingV4 (V2 curve; for V2/V3 comparison tests) ---"); From 5b7477b81b214a7a3fec00af442312d7b1e8693f Mon Sep 17 00:00:00 2001 From: jaime wang Date: Thu, 30 Jul 2026 15:22:25 +0800 Subject: [PATCH 08/10] feat: fixed size of fee delegation recipient --- contracts/launchpadv2/BondingV5.sol | 44 ++++++++++--------- test/launchpadv5/bondingV5ExtParams.js | 58 ++++++++++++++++---------- 2 files changed, 59 insertions(+), 43 deletions(-) diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index c61cbd9d..152c920d 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -127,11 +127,12 @@ contract BondingV5 is /// (decoded from bits 3-4 of the `extParams_` flags word). mapping(address => uint8) public feeDelegationType; - /// @notice Raw fee delegation recipient bytes from the `extParams_` trailing segment: - /// a 20-byte address for type 1; the raw twitter account id bytes for type 2 - /// (the recipient vault is only created just before `launch`). Empty when no delegation. + /// @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 => bytes) public feeDelegationRecipient; + mapping(address => bytes32) public feeDelegationRecipient; event PreLaunched( address indexed token, @@ -164,7 +165,7 @@ contract BondingV5 is bool isFeeDelegation, bool isRobotics, uint8 feeDelegationType, - bytes feeDelegationRecipient + bytes32 feeDelegationRecipient ); error InvalidTokenStatus(); @@ -190,11 +191,13 @@ contract BondingV5 is /// bits 3-4: `feeDelegationType` (0 = none, 1 = address, 2 = twitter) /// remaining bits: reserved = 0, available for future flags/enums. /// - /// Trailing segment (`extParams[32:]`, present only when needed) carries the variable-length - /// `feeDelegationRecipient`, ABI-encoded as `abi.encode(uint256 flags, bytes recipient)`: - /// - type 1 (address): the 20-byte recipient address - /// - type 2 (twitter): the raw twitter account id bytes - /// Future variable-length fields are appended after this and decoded defensively by length. + /// 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). @@ -249,18 +252,19 @@ contract BondingV5 is ); } - /// @dev Reads the trailing `feeDelegationRecipient` bytes when present. Length-gated so legacy - /// flags-only payloads (length ⇐ 32) and future longer payloads both decode safely; extra - /// trailing tuple elements added later do not affect this decode. + /// @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 (bytes memory) { - // abi.encode(uint256, bytes) is at least 3 words (flags + offset + length). - if (extParams.length < 96) { - return ""; + ) internal pure returns (bytes32 recipient) { + if (extParams.length < 64) { + return bytes32(0); + } + assembly ("memory-safe") { + recipient := calldataload(add(extParams.offset, 32)) } - (, bytes memory recipient) = abi.decode(extParams, (uint256, bytes)); - return recipient; } function initialize( @@ -485,7 +489,7 @@ contract BondingV5 is // Decode and store the extended launch settings so on-chain is the source of truth. bool isRobotics_ = _decodeIsRobotics(extParams_); uint8 feeDelegationType_ = _decodeFeeDelegationType(extParams_); - bytes memory feeDelegationRecipient_ = _decodeFeeDelegationRecipient(extParams_); + bytes32 feeDelegationRecipient_ = _decodeFeeDelegationRecipient(extParams_); isRobotics[token] = isRobotics_; feeDelegationType[token] = feeDelegationType_; feeDelegationRecipient[token] = feeDelegationRecipient_; diff --git a/test/launchpadv5/bondingV5ExtParams.js b/test/launchpadv5/bondingV5ExtParams.js index f90a6ae0..42f10e04 100644 --- a/test/launchpadv5/bondingV5ExtParams.js +++ b/test/launchpadv5/bondingV5ExtParams.js @@ -44,14 +44,22 @@ function encodeFlags(opts) { return ethers.AbiCoder.defaultAbiCoder().encode(["uint256"], [buildFlags(opts)]); } -/** Flags + trailing recipient: abi.encode(uint256 flags, bytes recipient). */ -function encodeFlagsWithRecipient(opts, recipientBytes) { +/** Flags + trailing recipient: abi.encode(uint256 flags, bytes32 recipient). */ +function encodeFlagsWithRecipient(opts, recipient32) { return ethers.AbiCoder.defaultAbiCoder().encode( - ["uint256", "bytes"], - [buildFlags(opts), recipientBytes] + ["uint256", "bytes32"], + [buildFlags(opts), recipient32] ); } +/** Recipient as a right-aligned integer in bytes32, matching the contract wire. */ +function addressToRecipient32(addr) { + return ethers.zeroPadValue(addr, 32); // uint160, right-aligned +} +function twitterIdToRecipient32(id) { + return ethers.toBeHex(BigInt(id), 32); // uint64, right-aligned +} + /** Legacy V1 encoding: abi.encode(bool isFeeDelegation). */ function encodeLegacyBool(isFeeDelegation) { return ethers.AbiCoder.defaultAbiCoder().encode(["bool"], [isFeeDelegation]); @@ -118,7 +126,9 @@ describe("BondingV5 extParams — v3 launch settings (robotics + fee delegation) expect(await bondingV5.isFeeDelegation(tokenAddress)).to.equal(false); expect(await bondingV5.isRobotics(tokenAddress)).to.equal(false); expect(await bondingV5.feeDelegationType(tokenAddress)).to.equal(0); - expect(await bondingV5.feeDelegationRecipient(tokenAddress)).to.equal("0x"); + expect(await bondingV5.feeDelegationRecipient(tokenAddress)).to.equal( + ethers.ZeroHash + ); }); it("keeps legacy abi.encode(bool) working with new fields defaulted", async function () { @@ -128,7 +138,9 @@ describe("BondingV5 extParams — v3 launch settings (robotics + fee delegation) expect(await bondingV5.isFeeDelegation(tokenAddress)).to.equal(true); expect(await bondingV5.isRobotics(tokenAddress)).to.equal(false); expect(await bondingV5.feeDelegationType(tokenAddress)).to.equal(0); - expect(await bondingV5.feeDelegationRecipient(tokenAddress)).to.equal("0x"); + expect(await bondingV5.feeDelegationRecipient(tokenAddress)).to.equal( + ethers.ZeroHash + ); }); it("decodes isRobotics from bit 2", async function () { @@ -146,11 +158,12 @@ describe("BondingV5 extParams — v3 launch settings (robotics + fee delegation) const recipient = ethers.getAddress( "0x00000000000000000000000000000000000000aa" ); - const recipientBytes = ethers.hexlify(ethers.getBytes(recipient)); + // Address stored as a right-aligned uint160 in bytes32. + const recipient32 = addressToRecipient32(recipient); const extParams = encodeFlagsWithRecipient( { isFeeDelegation: true, feeDelegationType: FEE_DELEGATION_TYPE_ADDRESS }, - recipientBytes + recipient32 ); const { tokenAddress } = await preLaunchWithExtParams(extParams); @@ -158,43 +171,42 @@ describe("BondingV5 extParams — v3 launch settings (robotics + fee delegation) expect(await bondingV5.feeDelegationType(tokenAddress)).to.equal( FEE_DELEGATION_TYPE_ADDRESS ); - expect(await bondingV5.feeDelegationRecipient(tokenAddress)).to.equal( - recipientBytes - ); + const stored = await bondingV5.feeDelegationRecipient(tokenAddress); + expect(stored).to.equal(recipient32); + // The address is recoverable from the low 20 bytes. + expect(ethers.getAddress(ethers.dataSlice(stored, 12, 32))).to.equal(recipient); }); - it("stores raw twitter id bytes as fee delegation recipient", async function () { + it("stores twitter id as a right-aligned uint64 recipient", async function () { const { bondingV5 } = contracts; const twitterId = "1234567890"; - const recipientBytes = ethers.hexlify(ethers.toUtf8Bytes(twitterId)); + const recipient32 = twitterIdToRecipient32(twitterId); const extParams = encodeFlagsWithRecipient( { isFeeDelegation: true, feeDelegationType: FEE_DELEGATION_TYPE_TWITTER }, - recipientBytes + recipient32 ); const { tokenAddress } = await preLaunchWithExtParams(extParams); expect(await bondingV5.feeDelegationType(tokenAddress)).to.equal( FEE_DELEGATION_TYPE_TWITTER ); - expect(await bondingV5.feeDelegationRecipient(tokenAddress)).to.equal( - recipientBytes - ); - expect(ethers.toUtf8String(await bondingV5.feeDelegationRecipient(tokenAddress))).to.equal( - twitterId - ); + const stored = await bondingV5.feeDelegationRecipient(tokenAddress); + expect(stored).to.equal(recipient32); + // The numeric id is recovered by reading the word as an integer. + expect(BigInt(stored).toString()).to.equal(twitterId); }); it("emits PreLaunchExtParams with the decoded settings", async function () { const { bondingV5, virtualToken } = contracts; - const recipientBytes = ethers.hexlify(ethers.toUtf8Bytes("42")); + const recipient32 = twitterIdToRecipient32("42"); const extParams = encodeFlagsWithRecipient( { isFeeDelegation: true, isRobotics: true, feeDelegationType: FEE_DELEGATION_TYPE_TWITTER, }, - recipientBytes + recipient32 ); await virtualToken @@ -229,7 +241,7 @@ describe("BondingV5 extParams — v3 launch settings (robotics + fee delegation) true, true, FEE_DELEGATION_TYPE_TWITTER, - recipientBytes + recipient32 ); }); }); From beeef476277554de4444619049d948d869423e63 Mon Sep 17 00:00:00 2001 From: jaime wang Date: Thu, 30 Jul 2026 16:04:02 +0800 Subject: [PATCH 09/10] chore: improve bit manipulation comment --- contracts/launchpadv2/BondingV5.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index 152c920d..01ac7b45 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -209,7 +209,9 @@ contract BondingV5 is 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 - uint256 private constant EXT_PARAMS_FEE_DELEGATION_TYPE_SHIFT = 3; // bits 3-4 + // `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( From 477c4dd867a7c1cb1d8b3a7047c70fc24c747148 Mon Sep 17 00:00:00 2001 From: jaime wang Date: Wed, 12 Aug 2026 11:16:07 +0800 Subject: [PATCH 10/10] fix: upgrade bsc bonding --- .openzeppelin/bsc-testnet.json | 525 +++++++++++++++++++++++- scripts/launchpadv5/upgradeBondingV5.ts | 98 +++++ 2 files changed, 622 insertions(+), 1 deletion(-) create mode 100644 scripts/launchpadv5/upgradeBondingV5.ts diff --git a/.openzeppelin/bsc-testnet.json b/.openzeppelin/bsc-testnet.json index 57ffa65e..aa4b7d17 100644 --- a/.openzeppelin/bsc-testnet.json +++ b/.openzeppelin/bsc-testnet.json @@ -48,7 +48,6 @@ }, { "address": "0x272b5917d81b0085fb0F9DedfE883928eB226CFC", - "txHash": "0x303e4f7680bd9e23fa043b387f8d7839f6c9674a82d58b120c007819e1030d51", "kind": "transparent" } ], @@ -2513,6 +2512,530 @@ ] } } + }, + "57bf9084ddfe16fc41c31606f669785bbcde50bac493c8aeddaa56149fc168c4": { + "address": "0xAaA896c97a2d3c5C14a2A99CcAaF51F17f73C4bC", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "factory", + "offset": 0, + "slot": "0", + "type": "t_contract(IFFactoryV2Minimal)9886", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:95" + }, + { + "label": "router", + "offset": 0, + "slot": "1", + "type": "t_contract(IFRouterV3Minimal)9948", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:96" + }, + { + "label": "agentFactory", + "offset": 0, + "slot": "2", + "type": "t_contract(IAgentFactoryV7Minimal)10013", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:97" + }, + { + "label": "bondingConfig", + "offset": 0, + "slot": "3", + "type": "t_contract(BondingConfig)9848", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:98" + }, + { + "label": "tokenInfo", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_struct(Token)9139_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:100" + }, + { + "label": "tokenInfos", + "offset": 0, + "slot": "5", + "type": "t_array(t_address)dyn_storage", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:101" + }, + { + "label": "tokenLaunchParams", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_struct(LaunchParams)9150_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:107" + }, + { + "label": "tokenGradThreshold", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:110" + }, + { + "label": "isFeeDelegation", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:112" + }, + { + "label": "tokenPreLaunchExtParams", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_address,t_bytes_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:115" + }, + { + "label": "tokenFakeInitialVirtualLiq", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_address,t_uint256)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:120" + }, + { + "label": "isRobotics", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:124" + }, + { + "label": "feeDelegationType", + "offset": 0, + "slot": "12", + "type": "t_mapping(t_address,t_uint8)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:128" + }, + { + "label": "feeDelegationRecipient", + "offset": 0, + "slot": "13", + "type": "t_mapping(t_address,t_bytes32)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:135" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)443_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)151_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)582_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_contract(BondingConfig)9848": { + "label": "contract BondingConfig", + "numberOfBytes": "20" + }, + "t_contract(IAgentFactoryV7Minimal)10013": { + "label": "contract IAgentFactoryV7Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFFactoryV2Minimal)9886": { + "label": "contract IFFactoryV2Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFRouterV3Minimal)9948": { + "label": "contract IFRouterV3Minimal", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bytes32)": { + "label": "mapping(address => bytes32)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bytes_storage)": { + "label": "mapping(address => bytes)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(LaunchParams)9150_storage)": { + "label": "mapping(address => struct BondingConfig.LaunchParams)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Token)9139_storage)": { + "label": "mapping(address => struct BondingConfig.Token)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint8)": { + "label": "mapping(address => uint8)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Data)9100_storage": { + "label": "struct BondingConfig.Data", + "members": [ + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "ticker", + "type": "t_string_storage", + "offset": 0, + "slot": "3" + }, + { + "label": "supply", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "price", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "marketCap", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "liquidity", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "volume", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "volume24H", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "prevPrice", + "type": "t_uint256", + "offset": 0, + "slot": "10" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(LaunchParams)9150_storage": { + "label": "struct BondingConfig.LaunchParams", + "members": [ + { + "label": "launchMode", + "type": "t_uint8", + "offset": 0, + "slot": "0" + }, + { + "label": "airdropBips", + "type": "t_uint16", + "offset": 1, + "slot": "0" + }, + { + "label": "needAcf", + "type": "t_bool", + "offset": 3, + "slot": "0" + }, + { + "label": "antiSniperTaxType", + "type": "t_uint8", + "offset": 4, + "slot": "0" + }, + { + "label": "isProject60days", + "type": "t_bool", + "offset": 5, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Token)9139_storage": { + "label": "struct BondingConfig.Token", + "members": [ + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "pair", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "agentToken", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "data", + "type": "t_struct(Data)9100_storage", + "offset": 0, + "slot": "4" + }, + { + "label": "description", + "type": "t_string_storage", + "offset": 0, + "slot": "16" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "17" + }, + { + "label": "image", + "type": "t_string_storage", + "offset": 0, + "slot": "18" + }, + { + "label": "twitter", + "type": "t_string_storage", + "offset": 0, + "slot": "19" + }, + { + "label": "telegram", + "type": "t_string_storage", + "offset": 0, + "slot": "20" + }, + { + "label": "youtube", + "type": "t_string_storage", + "offset": 0, + "slot": "21" + }, + { + "label": "website", + "type": "t_string_storage", + "offset": 0, + "slot": "22" + }, + { + "label": "trading", + "type": "t_bool", + "offset": 0, + "slot": "23" + }, + { + "label": "tradingOnUniswap", + "type": "t_bool", + "offset": 1, + "slot": "23" + }, + { + "label": "applicationId", + "type": "t_uint256", + "offset": 0, + "slot": "24" + }, + { + "label": "initialPurchase", + "type": "t_uint256", + "offset": 0, + "slot": "25" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "26" + }, + { + "label": "launchExecuted", + "type": "t_bool", + "offset": 0, + "slot": "27" + } + ], + "numberOfBytes": "896" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + }, + "allAddresses": [ + "0xAaA896c97a2d3c5C14a2A99CcAaF51F17f73C4bC", + "0x25a5b4D651ad40B0280f28Aae1aE9123C6082f49" + ] } } } diff --git a/scripts/launchpadv5/upgradeBondingV5.ts b/scripts/launchpadv5/upgradeBondingV5.ts new file mode 100644 index 00000000..0fa4ff1c --- /dev/null +++ b/scripts/launchpadv5/upgradeBondingV5.ts @@ -0,0 +1,98 @@ +/** + * Upgrade BondingV5 transparent proxy using CONTRACT_CONTROLLER (ProxyAdmin owner). + * + * Usage: + * BONDING_V5_ADDRESS=0x... npx hardhat run scripts/launchpadv5/upgradeBondingV5.ts --network bsc_testnet + * + * Requires: CONTRACT_CONTROLLER_PRIVATE_KEY, CONTRACT_CONTROLLER, BONDING_V5_ADDRESS + */ +import { ethers, upgrades } from "hardhat"; + +(async () => { + try { + const proxyAddress = + process.env.BONDING_V5_ADDRESS || + "0x272b5917d81b0085fb0F9DedfE883928eB226CFC"; + + if (!process.env.CONTRACT_CONTROLLER_PRIVATE_KEY) { + throw new Error("CONTRACT_CONTROLLER_PRIVATE_KEY is not set"); + } + if (!process.env.CONTRACT_CONTROLLER) { + throw new Error("CONTRACT_CONTROLLER is not set"); + } + + const controllerWallet = new ethers.Wallet( + process.env.CONTRACT_CONTROLLER_PRIVATE_KEY, + ethers.provider + ); + console.log("Using CONTRACT_CONTROLLER:", controllerWallet.address); + console.log("Expected CONTRACT_CONTROLLER:", process.env.CONTRACT_CONTROLLER); + + if ( + controllerWallet.address.toLowerCase() !== + process.env.CONTRACT_CONTROLLER.toLowerCase() + ) { + throw new Error( + "CONTRACT_CONTROLLER_PRIVATE_KEY does not match CONTRACT_CONTROLLER address" + ); + } + + const currentImpl = await upgrades.erc1967.getImplementationAddress(proxyAddress); + const admin = await upgrades.erc1967.getAdminAddress(proxyAddress); + console.log("Proxy:", proxyAddress); + console.log("Current implementation:", currentImpl); + console.log("ProxyAdmin:", admin); + + const proxyAdmin = await ethers.getContractAt( + ["function owner() view returns (address)"], + admin + ); + const owner = await proxyAdmin.owner(); + console.log("ProxyAdmin.owner:", owner); + if (owner.toLowerCase() !== controllerWallet.address.toLowerCase()) { + throw new Error( + `ProxyAdmin owner ${owner} != CONTRACT_CONTROLLER ${controllerWallet.address}` + ); + } + + const BondingV5 = await ethers.getContractFactory("BondingV5"); + const contract = BondingV5.connect(controllerWallet); + + // Proxy may have been upgraded outside this manifest (or by another machine). + // Import the existing deployment so OZ can validate storage + perform the upgrade. + try { + console.log("Force-importing existing proxy into OpenZeppelin manifest..."); + await upgrades.forceImport(proxyAddress, contract, { kind: "transparent" }); + console.log("forceImport OK"); + } catch (importErr: any) { + // Already registered is fine + const msg = importErr?.message || String(importErr); + if (!/already imported|already registered/i.test(msg)) { + console.log("forceImport note:", msg); + } + } + + console.log("Upgrading BondingV5..."); + // Parent-initializer order warning is pre-existing on BondingV5; allow so the upgrade can proceed. + // redeployImplementation: 'always' — forceImport can wrongly bind the *new* artifact hash to the + // *old* impl address when local source has advanced; without this OZ may skip deploying. + const upgraded = await upgrades.upgradeProxy(proxyAddress, contract, { + unsafeAllow: ["incorrect-initializer-order"], + redeployImplementation: "always", + }); + await upgraded.waitForDeployment(); + + const newImpl = await upgrades.erc1967.getImplementationAddress(proxyAddress); + console.log("Upgraded proxy:", await upgraded.getAddress()); + console.log("Previous implementation:", currentImpl); + console.log("New implementation:", newImpl); + if (newImpl.toLowerCase() === currentImpl.toLowerCase()) { + throw new Error( + "Implementation address unchanged after upgrade — redeploy did not take effect" + ); + } + } catch (e) { + console.error("Error:", e); + process.exit(1); + } +})();