From 7c34f354c1c31bf671bae5d81394f25db1874d7d Mon Sep 17 00:00:00 2001 From: brianna Date: Fri, 3 Jul 2026 17:33:18 +0800 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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,