{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"f963ea4d-f3a8-4ff7-9f13-1d330a275035","kind":"audit","nodes":[{"acceptedSubmissionHash":"3a82f193d205470b91ab3034566f1dc4ff876e7ebc69797eed81838d479320b4","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"6d8ac86f0ad2763445438133a2cc463bad91b28537dbf6626bf09d0a827ac6cb","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"17af39026248d7af87bb65570a84031b711b586323ac12adc783046ff40a0535","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"5175f906644fec367673ebafb4c02afc3040e4a91a71d5d2d452e39efd990c9f","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"f2ae248c5e5944a29c3d8327bbe04d433ade9555651510ce0d9754bc8fab46b1","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"Project: PepesFamily launchpad v5, re-check after audit 8f96baf6\nRepo: github.com/0xtenang/PepesFamily (commit 5e84e99)\nScope: contracts/src/PepesFamily.sol, contracts/src/PepesFamilyLens.sol, contracts/src/PepesFamilyRouter.sol\nTests: contracts/test/PepesFamily.t.sol (section “v5 audit (8f96baf6)”), contracts/test/Fork.t.sol\n\nChanges since 8f96baf6\n\nFinding 1: afterSwap reverts with PartialFill unless the pool traded the whole specified amount, net of the specified-side fee or burn taken in beforeSwap (FEE_SLOT + BURN_SLOT).\nFindings 2 and 3: _burn mints ERC-6909 claims of the token to the launchpad (pendingBurn). flush(token) burns those claims and takes the tokens to 0x…dEaD, then pays holder fees. Mid-unlock, only our routers may flush.\nFinding 4: creatorFee and holderFee are computed from their own bps; the protocol takes the remainder.\nFinding 5: lens paging uses limit > n - offset.\nFinding 6: marketCap moved to the lens, using supply minus totalBurned minus pendingBurn.\nFinding 7: creator payout is two-step: setCreatorPayout proposes (address(0) cancels), and acceptCreatorPayout must be called by the proposed address.\nPlease check\n\nCan the full-fill check be bypassed, or does it reject any legitimate full-fill swap? Think about rounding in exact-in and exact-out, and tiny amounts.\nAre the token claims always backed: claims of each token equal pendingBurn[token]? Does flush mid-unlock from our routers always have the tokens to take?\nCould pending burns be stuck or griefed? Does flush with no holder fees but a pending burn behave correctly?\nAny regression of v4 guarantees or of earlier findings.","parentJobId":null,"planHash":"12d2eb28036e1b1f85dfefe9b88d260923bcd83edc162d7b6577cc79db728737","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"f963ea4d-f3a8-4ff7-9f13-1d330a275035","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51566","feedbackHash":"062561781fd5e549f32449eb84ce9a70e3afcaf366c1daa26f760142755bc579","nodeKey":"audit_economics","submissionHash":"3a82f193d205470b91ab3034566f1dc4ff876e7ebc69797eed81838d479320b4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51040","feedbackHash":"370506db678bde8ccc97a6d72f12d2157ec0dccd336e71a97e8c940cc9d6aed6","nodeKey":"audit_flow","submissionHash":"6d8ac86f0ad2763445438133a2cc463bad91b28537dbf6626bf09d0a827ac6cb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51510","feedbackHash":"e205df9df0c570a078e1331ebcfd3b7792cdaad3c4efcb6a388e0bd73a5ddbc3","nodeKey":"audit_judge","submissionHash":"17af39026248d7af87bb65570a84031b711b586323ac12adc783046ff40a0535","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52189","feedbackHash":"d19673ec3f9af53a663c7d380f08eb80573cc256053cc6df5c43e7c97f0ee366","nodeKey":"audit_math","submissionHash":"5175f906644fec367673ebafb4c02afc3040e4a91a71d5d2d452e39efd990c9f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52011","feedbackHash":"9d2f81569983b8c5f7e937ada6d92711acf2b72e7d9fe6ea87c24bf4622922ff","nodeKey":"audit_permissions","submissionHash":"f2ae248c5e5944a29c3d8327bbe04d433ade9555651510ce0d9754bc8fab46b1","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"5ca3b28f8a35570523ef8d606d98e8bda8bbf8b554cac03f3a3e80432e709cb4","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8d428b115b0ebd64","findings":[{"citation":"resolved","description":"Merged from audit_math, audit_flow and audit_economics, which reported the same mechanism. AUDIT.md section 2 states the v5 invariant as an equality (the launchpad's ERC-6909 claims of each token equal pendingBurn[token]; its IMD claims equal pendingProtocolFees + sum pendingHolderFees + sum pendingCreatorFees) and the repository test helper _assertClaimsBacked (test/PepesFamily.t.sol:1237-1248) asserts it with assertEq. The launchpad itself keeps the equality on every path I exercised: _burn adds exactly the minted amount to pendingBurn (line 464-465), _flush burns exactly pendingBurn (525-530), _chargeFee mints exactly the fee it books (454-457), and _collect/_collectCreator burn exactly the booked amounts. I confirmed claims == pendingBurn and IMD claims == booked fees after every step of 512 random sequences mixing router buys/sells, third-party exact-in/exact-out buys and sells, standalone flush and both collects, for splits (0,0,300), (100,100,100), (50,150,100) in both currency orders, and for amounts 1-9601 wei in all four swap kinds. What the contract cannot prevent is a third party crediting claims to it: PoolManager.mint(to, id, amount) and the ERC-6909 transfer let any locker mint or move claims of any currency to any address, including the launchpad. After that the launchpad's claim balance exceeds the booked amount and no code path ever burns or takes the surplus, because _flush, _collect and _collectCreator only burn what is booked; the donated claims (and the tokens or IMD backing them in the PoolManager) are stranded forever. Impact: none on solvency or on any user. The direction that matters for safety, claims >= booked, always holds, the launchpad never pays out more than it owes, the lens marketCap reads pendingBurn rather than claim balances so it is unaffected, and only the donor loses anything. It is reported so the invariant is documented and tested as '>=' (or 'claims - booked is a non-negative constant that only donations change'), so a future stateful/invariant fuzz with external actors, which AUDIT.md section 9 asks for, does not fail on a harmless donation, and so nobody later 'fixes' a surplus of IMD claims by paying it out. Fix: restate the invariant in AUDIT.md section 2 and change the two assertEq in _assertClaimsBacked to assertGe (or assert the difference is constant); optionally, if exact equality of token claims is wanted, have _flush burn poolManager.balanceOf(address(this), id) instead of pendingBurn and take that amount to DEAD, which is a sensible destination for donated token claims but not for donated IMD claims, so leave the IMD side as '>='.","line":529,"path":"contracts/src/PepesFamily.sol","reproduction":"State: any v5 token T launched with FeeSplit(0,0,300) and one buy of 20 IMD through PepesFamilyRouter, so the router's inline flush leaves pendingBurn[T] == 0 and the launchpad's claims of T == 0. Input: a contract D holding 1e18 T calls poolManager.unlock and in its unlockCallback does sync(T); T.transfer(poolManager, 1e18); settle(); mint(address(pad), uint256(uint160(T)), 1e18). Expected per AUDIT.md section 2 and _assertClaimsBacked: poolManager.balanceOf(pad, uint256(uint160(T))) == pad.pendingBurn(T) == 0. Actual: poolManager.balanceOf(pad, id(T)) == 1e18 while pendingBurn(T) == 0; pad.flush(T) returns at line 473 (nothing booked) and leaves the 1e18 claims; a further router buy of 1 IMD runs _flush normally and still leaves 1e18 claims; no function can ever burn them. Verified with a scratch Foundry test (test_spec_donatedClaimsExceedPendingBurn) that passes against commit 5e84e99. The same holds for IMD claims via mint(pad, id(IMD), x).","severity":"info","snippet":"            poolManager.burn(address(this), Currency.wrap(token).toId(), burn);","title":"Stated invariant 'token claims == pendingBurn' (and 'IMD claims == pending fees') is only '>=': anyone can mint or transfer ERC-6909 claims to the launchpad, and _flush/_collect burn only the accounte"},{"citation":"resolved","description":"From audit_permissions, reproduced. payingEth (line 158) is true only when the launch's quote is address(0), but PepesFamily._launch reverts UnsupportedQuote for any quote other than IMD (PepesFamily.sol:272), so pad.launches(token).quote is IMD for every token and the branch at line 172 can never execute; msg.value on an IMD trade is rejected at line 159, so no ETH can ever sit in the router. The comment's premise is also stale since finding 1 of audit 8f96baf6: a swap stopped by its price limit (including the end of the curve) now reverts in afterSwap with PartialFill (PepesFamily.sol:400) instead of leaving an unspent remainder, so even with an ETH quote there would be nothing to refund. The NatSpec of buy (line 121, 'send ETH as msg.value, or approve this router for IMD') and of launch (line 92, 'For IMD launches approve this router') likewise still describe a two-quote router. Dead code and misleading comments only; no funds are at risk. Fix: drop payingEth and the refund branch, make the `payable`/msg.value check a plain `if (msg.value != 0) revert BadAmount()`, and reword the three comments, or state explicitly that the branch is kept for a future ETH quote.","line":171,"path":"contracts/src/PepesFamilyRouter.sol","reproduction":"Call router.launch(\"X\",\"X\",\"\",address(0),0,0) or pad.launchWithSplit(\"X\",\"X\",\"\",address(0),FeeSplit(0,300,0)): both revert UnsupportedQuote (repo test test_onlyImd; scratch test test_spec_routerEthBranchDead), so every launched token has quote == IMD and payingEth is false on every call to _swap; router.buy{value: 1}(token, 1e18, 0, deadline) on an IMD token reverts BadAmount at line 159. Expected per the comment at line 171: a refund path for ETH left over when a swap hits the end of the curve. Actual: the branch is unreachable, and such a swap reverts with PartialFill (repo test test_v5audit_partialFillsRevert).","severity":"info","snippet":"        // Refund unspent ETH (only possible if the swap hit the end of the curve).\n        if (payingEth && address(this).balance > 0) address(0).transferOut(msg.sender, address(this).balance);","title":"Router: the ETH refund branch and its comment are dead in v5 (quote is always IMD, and a swap that reaches the end of the curve now reverts PartialFill instead of leaving a remainder); the buy/launch "},{"citation":"resolved","description":"From audit_math, reproduced as a coverage gap, not a code defect. test_v5audit_partialFillsRevert launches with quoteIsCurrency0 == true only, so the four PartialFill cases (exact-in/out x buy/sell) are checked for one orientation of the sign-flipping logic in afterSwap (quoteSpecified = (exactIn == zeroForOne) == quoteIs0 at PepesFamily.sol:390, and q/t picked from amount0/amount1 at 382-383). The other orientation, which every launch whose token address sorts below IMD gets, has no partial-fill test, and the trailing 'a limit that is never reached still trades' assertion is exact-in only; the committed suite's exact-out swaps all go through test_split_feesForEverySwapKind and testFuzz_split with open limits, so a regression that made the check reject full exact-out fills in the token-is-currency0 order would only show up there indirectly. I verified independently that the check is correct in both orders: with a limit 0.1% past spot every kind reverts in the token-is-currency0 order too, and with the open limit exact-in buy/sell and exact-out buy/sell of 1, 2, 3, 7, 23, 24, 25, 26, 99, 100, 101, 9599, 9600 and 9601 wei fill without PartialFill for splits (0,300,0), (0,0,300), (100,100,100) and (200,0,100) in both orders, with claims equal to the books after each; after a full exit (price on the start tick, no active liquidity) a buy still fills in both orders. The pool's delta passed to afterSwap has specified side equal to amountToSwap minus the unfilled remainder (v4-core Pool.sol:453-461), so the two inequalities at PepesFamily.sol:400 are true exactly when the remainder is non-zero. Fix: loop the launch over both orders as test_split_feesForEverySwapKind does (o < 2, with buy = zeroForOne flipped when the token is currency0), and add explicit open-limit exact-out buy and sell swaps after the reverting cases.","line":1330,"path":"contracts/test/PepesFamily.t.sol","reproduction":"Run the existing test: it only ever launches _launchSplit(0, 150, 150, true). Change the argument to false and flip the zfo table (buy is zeroForOne == false when the token is currency0): nothing in the committed suite asserts those four reverts or an exact-out full fill for that orientation. A scratch test (test_q1_partialFillRevertsTokenIsCurrency0 and test_q1_tinyFullFillsPass, both orders, exact-in and exact-out, buy and sell, limit 0.1% past spot and open limits) passes on commit 5e84e99, confirming the behaviour, not the coverage.","severity":"info","snippet":"        PadToken t = _launchSplit(0, 150, 150, true);","title":"Test gap: the PartialFill regression test covers only the quote-is-currency0 order, and no test asserts that legitimate exact-out swaps pass the full-fill check in either order"}],"hash":"17af39026248d7af87bb65570a84031b711b586323ac12adc783046ff40a0535","nodeId":"3a5f8a57-9efd-4c6e-b221-4afe1e4aede0","outcome":"completed","summary":"The findings file is in place at the repository root (excluded from git by the harness's own exclude rule, as intended), the scratch tests are removed, and the tree is otherwise untouched. The review is complete.\n\n## Outcome\n\nNo defect of medium or higher severity exists on commit 5e84e99. The specialists' five items reduce to three informational findings, all reproduced, and the four questions in the brief hold.\n\n**Findings kept, written to `.imd-findings.json`:**\n\n- **Info, merged from three specialists.** The stated invariant that the launchpad's ERC-6909 claims equal `pendingBurn` and the booked IMD fees is only enforceable as \"greater or equal\". Any locker can mint or transfer claims to the launchpad, and `_flush` and the collectors burn only the booked amounts, so a gifted surplus is stranded. No one but the donor loses anything. The fix is to restate the invariant and the test helper as `>=`.\n- **Info, from audit_permissions.** The router's ETH refund branch is unreachable because every launch must use IMD, and its comment is stale because an end-of-curve swap now reverts with `PartialFill`. Two NatSpec lines still describe paying with ETH.\n- **Info, from audit_math.** The partial-fill regression test covers only the quote-is-currency0 order and never asserts a legitimate exact-out fill. I confirmed the behaviour is correct in both orders, so this is a coverage gap.\n\n**The brief's four questions, each checked with scratch Foundry tests that pass on this commit:**\n\n1. **Full-fill check.** The pool hands `afterSwap` a delta whose specified side is exactly the amount swapped minus the unfilled remainder, so the two inequalities fire precisely when the remainder is non-zero. Exact-in and exact-out buys and sells of 1 to 9601 wei fill in both currency orders across four splits, and a limit 0.1% past spot reverts every kind.\n2. **Claims backing.** Token claims equalled `pendingBurn` and IMD claims equalled the booked fees after every step of 512 random sequences mixing both routers, third-party trades, standalone flush and both collects. Both routers settle the seller's tokens before flushing and flush before the buyer takes, so a mid-unlock take always has tokens. The ETH router handled a sell whose burn exceeded the pool's remaining tokens.\n3. **Stuck or griefed burns.** Flush from outside an unlock is permissionless and burns before the holder-fee branch. With no holder fees it burns to the dead address and returns. The token's `claim` also triggers it.\n4. **Regressions.** None found. All 129 repository tests pass, and the 11 fork tests pass against live Robinhood Chain state, including the deployed V4Quoter matching the router's output with the burn included.\n\nNo finding carries a proof file, since none is critical or high.","treeHash":null,"usage":{"cachedInputTokens":2790213,"inputTokens":674,"model":"claude-fable-5-1","outputTokens":51630,"runtime":"claude","turns":42,"wallClockMs":696714}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d0653dc91b6e2259","findings":[{"citation":"resolved","description":"AUDIT.md section 2 states the v5 invariant as an equality: the launchpad's ERC-6909 claims of each token equal pendingBurn[token] (and its IMD claims equal pendingProtocolFees + sum of pendingHolderFees + sum of pendingCreatorFees). The test helper _assertClaimsBacked in test/PepesFamily.t.sol asserts the same equality. The equality holds for every path the launchpad itself takes (_burn mints exactly what it adds to pendingBurn, _flush burns exactly pendingBurn), but it is not a property of the system: the PoolManager's mint(to, id, amount) and ERC6909.transfer(receiver, id, amount) let any locker credit claims of any currency to any address, including the launchpad. After such a donation the launchpad holds more claims than it accounts for, and nothing can ever burn or take the surplus, because _flush, _collect and _collectCreator burn only the accounted amounts. No user funds are at risk and the launchpad never pays out more than it owes; the accounting direction is safe (claims >= owed). This is reported so the invariant is documented and tested as '>=' rather than '==', so a future stateful/invariant fuzz with external actors (which the brief asks for) does not fail on a harmless donation, and so nobody later 'fixes' the surplus by paying it out, which would be wrong for a donation of claims that were never trades.","line":178,"path":"contracts/src/PepesFamily.sol","reproduction":"State: any v5 token T launched with FeeSplit(0,0,300), one buy through PepesFamilyRouter so the router's inline flush leaves pendingBurn[T] == 0. Input: a third-party contract D holding 1e18 T calls poolManager.unlock and in its unlockCallback does sync(T); T.transfer(poolManager, 1e18); settle(); mint(address(pad), uint256(uint160(T)), 1e18). Expected per AUDIT.md: poolManager.balanceOf(pad, id(T)) == pad.pendingBurn(T) == 0. Actual: poolManager.balanceOf(pad, id(T)) == 1e18 while pendingBurn(T) == 0; pad.flush(T) returns early (nothing pending) and the 1e18 claims stay with the launchpad forever. Verified with a scratch Foundry test (test_claimsCanExceedPendingBurn) that passes on this commit. The same holds for IMD claims via mint(pad, id(IMD), x). Fix: none required in code; restate invariants 3 and the v5 burn invariant as 'claims >= accounted' (or 'claims - accounted is a constant that only donations change') in AUDIT.md and in _assertClaimsBacked, or if exact equality is wanted, have _flush burn poolManager.balanceOf(this, id) instead of pendingBurn (the surplus would then also be burned to 0xdEaD, which is a reasonable destination for donated token claims but not for donated IMD claims).","severity":"info","snippet":"    mapping(address token => uint256) public pendingBurn;","title":"Stated invariant 'token claims == pendingBurn' is only '>=': anyone can mint or transfer ERC-6909 claims to the launchpad, and the surplus is unrecoverable"}],"hash":"3a82f193d205470b91ab3034566f1dc4ff876e7ebc69797eed81838d479320b4","nodeId":"2065f3dd-c4e6-4f81-a4de-1822f0b71f03","outcome":"completed","summary":"The review is complete and the findings file is written. Here is the recap.\n\n**Outcome: no exploitable defect found in the v5 changes.** The file `.imd-findings.json` holds one info-level finding. The project's own launchpad suite passes at this commit, and my scratch tests were removed after use.\n\n**What I verified, with concrete probes**\n\n- **Full-fill check cannot be bypassed and rejects no legitimate fill.** The pinned v4-core `computeSwapStep` consumes the specified remainder exactly when no limit or liquidity edge is hit, so the pool's specified-side delta equals `amount - fee` (exact-in) or `amount + fee` (exact-out) to the wei. A 1500-run fuzz over all four swap kinds, both currency orders, every valid split, and amounts from 1 wei upward never produced `PartialFill` on a no-limit swap, and 40 swaps whose limit was set to their own quoted end price all filled. The only `PartialFill` reverts were swaps that asked for more IMD than the pool held, which cannot be filled by anyone.\n- **Transient slot handoff is safe.** Each branch of `afterSwap` clears the slot its `beforeSwap` branch wrote. A swap whose pool call reverts inside a caller's try/catch also reverts its transient writes, so no stale fee or burn value can leak into a later swap.\n- **Claims stay backed.** The fuzz asserted after every swap that pad IMD claims equal protocol plus holder plus creator pending, and token claims equal `pendingBurn`. Mid-unlock flushes from both routers always run after the input is settled, so the PoolManager holds the burn tokens when `take` runs.\n- **Pending burns cannot be stuck or griefed.** Standalone `flush` with a burn and no holder fees burns and returns cleanly. A holder's `claim` on a burn-only token also burns. The PadToken transfer from PoolManager to the dead address cannot revert.\n- **No regression** of the v3 flash-holder guards, the v4 expiry paths, fee correctness, or two-step creator payout.\n\n**The one finding (info)**\n\nThe brief states the token claim invariant as an equality, but anyone can mint or transfer ERC-6909 claims to the launchpad, after which claims exceed `pendingBurn` and the surplus is stuck forever. No funds are at risk. The finding includes the exact call sequence and suggests documenting and testing the invariant as \"greater or equal\". Full details are in `.imd-findings.json`.","treeHash":null,"usage":{"cachedInputTokens":2395197,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":56813,"runtime":"claude","turns":47,"wallClockMs":1192025}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fe5e46fb044c6af1","findings":[{"citation":"resolved","description":"The brief states the v5 invariant 'its claims of each token = pendingBurn[token]'. The hook keeps that equality for everything it does itself: _burn adds exactly the minted amount to pendingBurn and _flush burns exactly pendingBurn (verified for exact-in/exact-out, buy/sell, both currency orders, amounts from 1 wei up, and with holderBps == 0). What the contract cannot prevent is a third party settling tokens into the PoolManager inside its own unlock and minting (or transferring) ERC-6909 claims of the PadToken to the launchpad address. After that the launchpad's claim balance exceeds pendingBurn[token]; _flush only burns pendingBurn, so the surplus claims are stranded forever (no path reads or burns them) and the lens marketCap, which subtracts totalBurned + pendingBurn, keeps counting those tokens as circulating although they are locked in the PoolManager with no owner able to redeem them. Solvency is never at risk: the launchpad always holds at least pendingBurn claims, so 'claims >= pendingBurn' (and 'IMD claims >= protocol + holder + creator', which has the same exposure) is the invariant that actually holds, and it is the one the fuzz/unit test _assertClaimsBacked should state (assertGe) if anyone ever donates claims. The same is already true in v4 for IMD claims; v5 just adds a second currency class. A fix, if wanted, is cosmetic: document the invariant as '>=', or let _flush burn the launchpad's whole claim balance of the token (poolManager.balanceOf(address(this), id)) to DEAD instead of only pendingBurn, so gifted claims are burned too and the equality is restored after every flush.","line":465,"path":"contracts/src/PepesFamily.sol","reproduction":"Launch a token with split (0,0,300) and buy 20 IMD through PepesFamilyRouter so pendingBurn is 0 afterwards. Bob transfers 1e18 tokens to a helper contract. The helper calls poolManager.unlock, and in its callback does sync(token); token.transfer(poolManager, 1e18); settle(); mint(address(pad), uint256(uint160(token)), 1e18). Expected (per brief): poolManager.balanceOf(pad, tokenId) == pendingBurn[token] == 0. Actual: poolManager.balanceOf(pad, tokenId) == 1e18 while pendingBurn[token] == 0; a further router buy flushes normally and leaves the 1e18 claims untouched (still 1e18 after flush). Verified with a scratch Foundry test (test_claimGiftBreaksEquality) against commit 5e84e99.","severity":"info","snippet":"        poolManager.mint(address(this), Currency.wrap(token).toId(), amount);","title":"Burn-claim invariant is only ever '>=': anyone can gift ERC-6909 token claims to the launchpad, so claims == pendingBurn[token] is not enforceable and the surplus is stuck forever"},{"citation":"resolved","description":"test_v5audit_partialFillsRevert launches with quoteIsCurrency0 == true only, so the four PartialFill cases are checked for one orientation of the sign-flipping logic in afterSwap (quoteSpecified = (exactIn == zeroForOne) == quoteIs0, q/t picked from amount0/amount1). The other orientation, the one most launches get whenever the token address sorts below IMD, has no partial-fill test, and no test asserts that a legitimate exact-out swap passes the check at all (the suite's exact-out swaps run under the broader split tests). I verified independently that the check is correct in both orders: with a limit 0.1% past the current price every kind reverts, and with the open limit exact-in buy/sell and exact-out buy/sell of 1, 2, 3, 7, 24, 25, 26, 99, 100, 9600, 9601 and 10000 wei fill without PartialFill for splits (0,300,0), (0,0,300), (100,100,100) and (200,0,100) in both orders, with the launchpad's IMD and token claims equal to its books after each. So this is a coverage gap rather than a code defect; adding the second orientation to the loop (o < 2 as in test_split_feesForEverySwapKind) closes it.","line":1330,"path":"contracts/test/PepesFamily.t.sol","reproduction":"Run the existing test: it launches only _launchSplit(0, 150, 150, true). Change the launch to _launchSplit(0, 150, 150, false) and the zfo/amount tables accordingly (buy = zeroForOne is false when the token is currency0): the four reverts and the trailing full fill are not asserted anywhere in the committed suite for that orientation. A scratch test (test_partialFillsBothOrders, both orders, exact-in and exact-out, buy and sell, limit 0.1% past spot) passes on commit 5e84e99, confirming the behaviour, not the coverage.","severity":"info","snippet":"        PadToken t = _launchSplit(0, 150, 150, true);","title":"The v5 partial-fill regression test only exercises one currency order (quote is currency0); the token-is-currency0 order of the PartialFill check is untested"}],"hash":"5175f906644fec367673ebafb4c02afc3040e4a91a71d5d2d452e39efd990c9f","nodeId":"73cfd6e8-4a89-4bea-b3ef-4c9bd0929048","outcome":"completed","summary":"The review is complete and the findings file is written.\n\n**Result: no defects of low severity or above in the v5 changes.** The file `.imd-findings.json` holds two informational findings, each with a concrete reproduction.\n\n**What I verified, against commit 5e84e99, by reading v4-core and running scratch Foundry tests (since removed):**\n\n- **Full-fill check cannot be bypassed and rejects no legitimate swap.** The delta passed to afterSwap is the pool's own delta, before hook deltas. For exact-in, v4's \"exhaust\" branch consumes exactly the requested input, so the pool side equals amount minus the beforeSwap fee. For exact-out, the capped step outputs exactly amount plus fee. Both give equality, so only a price limit or empty liquidity triggers PartialFill. Tested all four kinds, both currency orders, four splits, amounts of 1, 2, 3, 7, 24, 25, 26, 99, 100, 9600, 9601 and 10000 wei: nothing reverts, and claims equal the books after each.\n- **Partial fills revert in both orders**, including exact-out, with a limit 0.1% past spot. The committed test covers only the quote-is-currency0 order, which is the second info finding.\n- **Claims are backed.** The hook's own paths keep token claims exactly equal to pendingBurn and IMD claims equal to protocol plus holder plus creator fees. Mid-unlock flush from our routers always has the tokens: the PoolManager holds pool reserves plus every settled claim, and the flush runs before the buyer's tokens leave or after the seller's arrive.\n- **Burn-only tokens.** Pending burn from a third-party trade stays pending through a foreign-unlock flush, and is burned by a standalone flush, a router trade, and a holder's claim. Flush with no holder fees burns and returns before distribute, which is correct.\n- **Rounding.** The remainder-to-protocol split keeps zero shares exactly zero, and the lens market cap is unchanged by a flush and loses negligible precision at both tick extremes.\n\n**Findings recorded:**\n\n1. **Info.** Anyone can gift ERC-6909 token claims to the launchpad, so the brief's \"claims equal pendingBurn\" invariant only holds as \"greater or equal\". The surplus is stranded and still counted as circulating by the lens. Harmless to solvency. Fix is documentation, or burning the whole claim balance in flush.\n2. **Info.** The v5 partial-fill regression test exercises one currency order only.\n\n**Not reported as defects but worth knowing:** aggregators that pass a slippage-derived price limit now get PartialFill instead of a partial trade, which is the intended design. No tests were run against a fork.","treeHash":null,"usage":{"cachedInputTokens":2041904,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":55153,"runtime":"claude","turns":29,"wallClockMs":1145922}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"df74f6c887684f20","findings":[{"citation":"resolved","description":"The brief states the invariant 'the launchpad's claims of each token = pendingBurn[token]' (and IMD claims = pendingProtocolFees + sum pendingHolderFees + sum pendingCreatorFees). The code upholds the direction that matters for safety: every claim the hook mints is matched by a pendingBurn / pending-fee increment, and _flush/_collect burn exactly those amounts, so the launchpad never holds fewer claims than it owes (verified after each of the four swap kinds, both currency orders, all split presets, amounts 1-5 wei and random amounts, and after flush). The equality, however, is not enforced: PoolManager.mint(to, id, amount) and ERC-6909 transfer let any locker credit claims of any currency to the launchpad address. Those claims are not reflected in pendingBurn (or the IMD pendings), and since _flush/_collect/_collectCreator only burn the recorded pending amounts there is no code path that can ever burn or take them. They are stuck in the PoolManager forever. Nobody but the donor loses anything (the tokens or IMD backing them stay in the PoolManager), the lens marketCap is unaffected (it reads pendingBurn, not claim balances), and no accounting the contract relies on reads its own claim balance, so this is informational: the invariant as worded should read 'claims >= pendings', or the contract could expose a permissionless sweep of (claimBalance - pending) to DEAD for tokens and to feeRecipient for IMD if the exact equality is wanted.","line":525,"path":"contracts/src/PepesFamily.sol","reproduction":"State: any launched token t. Input: a contract calls poolManager.unlock, and in its unlockCallback does sync(t); t.transfer(poolManager, 1e18); settle(); mint(address(pad), uint256(uint160(t)), 1e18). Expected (per the brief's invariant): pm.balanceOf(pad, id(t)) == pad.pendingBurn(t). Actual: pm.balanceOf(pad, id(t)) == 1e18 while pad.pendingBurn(t) == 0; pad.flush(t) returns early (nothing pending) and the 1e18 claims remain at the launchpad with no function able to burn them. Reproduced in a Foundry test against commit 5e84e99 (test_donatedClaimsAreStuck).","severity":"info","snippet":"        uint256 burn = pendingBurn[token];\n        if (burn != 0) {","title":"Launchpad ERC-6909 claims can exceed pendingBurn (and the IMD pendings) through third-party mint/transfer; the excess is unrecoverable"}],"hash":"6d8ac86f0ad2763445438133a2cc463bad91b28537dbf6626bf09d0a827ac6cb","nodeId":"ef5fb76e-57c5-4d2d-9dfe-a80e1ab13a88","outcome":"completed","summary":"The review is complete. The findings file holds one informational finding, no source files were changed, and my scratch tests are removed.\n\n**Outcome: no exploitable defect in the v5 re-check scope.** Every question in the brief was checked by reading the hook against the pinned v4-core swap math and by running probe tests against commit 5e84e99.\n\n**Full-fill check (finding 1).** It cannot be bypassed and rejects no legitimate full fill. In v4's swap loop with a zero LP fee, the final step consumes exactly the remaining input (exact-in) or delivers exactly the remaining output (exact-out), so the pool delta equals `amount - fee` or `amount + fee` whenever the price limit is not hit, and the strict `<` comparisons are tight. Probes covered 1 to 5 wei and boundary amounts in all four swap kinds, both currency orders and four splits, plus 256 fuzz runs that also asserted the trader's specified side is honoured to the wei. A consequence worth knowing: exact-in sells can never partially fill, because every circulating token left the pool with rounding in the pool's favour, so `PartialFill` on the sell side only protects exact-out requests and user-set price limits.\n\n**Claims backing (findings 2 and 3).** The launchpad never holds fewer claims than it owes. Each mint is matched by a pending increment, each flush or collect burns exactly the recorded amount, and the take always succeeds because the PoolManager's balance covers claims plus pool reserves. Mid-unlock flush from the routers always happens after the trader's input is settled and before output is taken. A flash-borrower that takes the whole pool and calls flush or claim mid-unlock gets a no-op and earns nothing.\n\n**Stuck or griefed burns.** Flush with a pending burn and no holder fees burns and returns correctly, standalone, via `claim`, and via the routers. Nothing can block it since the only external effect is a token transfer to the dead address.\n\n**Regressions.** PadToken is unchanged from v4. Fee split rounding, lens paging, lens market cap and the two-step payout behave as described. The existing 71-test suite passes.\n\n**The one finding (info):** anyone can mint or transfer ERC-6909 claims to the launchpad, so claims can exceed `pendingBurn` and the IMD pendings, and nothing can ever burn the excess. The brief's invariant holds as \"greater or equal\", not equality. No one but the donor loses anything.\n\n**Not covered:** the fork tests need an RPC and were not run. The ETH router and PadToken were read for interactions but are outside the listed scope.","treeHash":null,"usage":{"cachedInputTokens":3984382,"inputTokens":866,"model":"claude-fable-5-1","outputTokens":61276,"runtime":"claude","turns":43,"wallClockMs":1181439}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a406deaac63a93b0","findings":[{"citation":"resolved","description":"`payingEth` (line 158) is true only when the launch's quote is address(0), but PepesFamily._launch reverts UnsupportedQuote for any quote other than IMD (PepesFamily.sol:272), so no launched token can have an ETH quote and the branch can never execute. The comment's premise is also stale: since finding 1 of audit 8f96baf6, a swap that stops at its price limit (the end of the curve) reverts in afterSwap with PartialFill (PepesFamily.sol:400) instead of leaving an unspent remainder. Dead code and a misleading comment only; no funds are at risk, and msg.value is still rejected on IMD buys (line 159), so no ETH can be stranded in the router. This is the only item found. The assigned questions were all checked and hold on this commit: (1) the full-fill check cannot be bypassed and rejects no legitimate full fill, because the pool's specified delta passed to afterSwap equals amountToSwap minus the unfilled remainder, so `spec + specFee < amount` (exact-in) and `spec < amount + specFee` (exact-out) are true exactly when the remainder is non-zero; verified for 1, 2, 23, 24, 25 and 1000 wei in all four swap kinds and both currency orders, and after a full exit with the price resting on the start tick with zero active liquidity. (2) Token ERC-6909 claims of the launchpad equal pendingBurn[token] after every step of 256 random sequences mixing router trades, third-party exact-in/out buys and sells, standalone flush and both collects, and the PoolManager's token balance always covers them; our routers flush only after the seller's tokens are settled or from the pool's own output on a buy, so the mid-unlock take always succeeds. (3) Pending burns cannot be stuck: flush from outside an unlock is permissionless, _flush burns and takes to 0xdEaD before the holder-fee branch, and with no holder fees it returns after the burn (TokensBurned emitted, totalBurned updated); claim() also triggers it. Stray ERC-6909 claims transferred to the launchpad by a third party only sit there and do not affect flush. (4) No regression of the v4 guarantees or of findings 1-7 was found; the repo's own v5 audit tests and the v3 flash-holder tests pass.","line":171,"path":"contracts/src/PepesFamilyRouter.sol","reproduction":"Call `router.launch(\"X\",\"X\",\"\",address(0),0,0)` or `pad.launchWithSplit(..., address(0), split)`: both revert UnsupportedQuote (repo test test_onlyImd), so `pad.launches(token).quote` is IMD for every token and `payingEth` is false on every call to `_swap`. Expected per the comment: a refund path for ETH left over when a swap hits the end of the curve. Actual: the branch is unreachable, and such a swap reverts with PartialFill (repo test test_v5audit_partialFillsRevert). Fix: drop the `payingEth` logic and the comment, or reword the comment to say the branch is kept for a future ETH quote.","severity":"info","snippet":"        // Refund unspent ETH (only possible if the swap hit the end of the curve).\n        if (payingEth && address(this).balance > 0) address(0).transferOut(msg.sender, address(this).balance);","title":"Router ETH refund branch and its comment are unreachable in v5: quote is always IMD and an end-of-curve swap now reverts with PartialFill"}],"hash":"f2ae248c5e5944a29c3d8327bbe04d433ade9555651510ce0d9754bc8fab46b1","nodeId":"66724763-2368-4b41-9eb9-bfe631f03f5d","outcome":"completed","summary":"The review is complete. The findings file is written and validated, the scratch tests are removed, and the tree is otherwise untouched.\n\n**Outcome: no exploitable defect found in the three in-scope contracts.** The findings file holds one informational item. Every question in the brief was checked both by reading the v4-core swap path and by Foundry probes against this commit.\n\n**What was verified**\n\n- **Full-fill check cannot be bypassed and rejects no legitimate fill.** The delta the PoolManager passes to afterSwap is exactly `amountToSwap − remaining`, and `amountToSwap` already has the beforeSwap fee folded in. So `spec + specFee < amount` (exact-in) and `spec < amount + specFee` (exact-out) are true precisely when the pool left a remainder. Probed with 1, 2, 23, 24, 25 and 1000 wei in all four swap kinds, both currency orders, and after a full exit with the price resting on the start tick with zero active liquidity. Rounding is always in the pool's favour, so holders selling everything never overshoot the start tick, and a whole-curve buy-out needs more IMD than fits in int128, so it reverts rather than partially filling.\n- **Token claims are always backed.** Across 256 random sequences mixing router trades, third-party exact-in and exact-out swaps, standalone flush and both collects, the launchpad's ERC-6909 token claims equalled `pendingBurn` after every step, and the PoolManager's token balance covered them. Our routers flush only after the seller's tokens are settled, or from pool output on a buy, so the mid-unlock take always has the tokens.\n- **Pending burns cannot be stuck.** Flush outside an unlock is permissionless, burns before the holder-fee branch, and with no holder fees returns after the burn. A claim also triggers it. Stray claims a third party transfers to the launchpad just sit there.\n- **No regression.** The repo's v5 audit tests, the split tests and the v3 flash-holder tests pass. The ETH router's native-ETH send lets a user reenter mid-unlock, but flush and distribute stay no-ops for them, as in v4.\n\n**Reported (info)**: the router's ETH refund branch at `contracts/src/PepesFamilyRouter.sol:171` is dead code with a stale comment. The quote is always IMD, and an end-of-curve swap now reverts with PartialFill, so no leftover ETH can exist. No funds are at risk.\n\n**Not reported, for the record**: `collectProtocolFees` mid-unlock can shrink a third-party locker's synced IMD reserves if that locker calls untrusted code between sync and settle. That is a v4 integration hazard present since v3, not a v5 change, and our routers are not exposed. Owner powers (`setStartTick`, `setFeeRecipient`) remain the documented trust assumptions; v5 adds no new privileged power.\n\n**Coverage limits**: no fork run (no RPC in this environment), no Slither, and fuzzing was bounded to 256 runs per property.","treeHash":null,"usage":{"cachedInputTokens":4235268,"inputTokens":866,"model":"claude-fable-5-1","outputTokens":78228,"runtime":"claude","turns":49,"wallClockMs":1513898}}],"verification":[]}