{"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":"b17295c2-b8a1-4cfc-a730-d3693ee8c6c6","kind":"audit","nodes":[{"acceptedSubmissionHash":"e8d1ce6a613fc0573c27838217500060920b9f17448cc60a05e6f97f7877788e","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":"778399d45aefd60d06b27a984f0a5429a5c32cf90f0cff4fa84fd6e404ea003e","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":"1ecbd80033c9b34cdb8e261b3fe0d4b941d92495d9340db5284b3998f73eaf53","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":"fc3ab96dcb2df12bb2be00e2415dc756b68c7d6b8f5191d0ac428b582af9b40e","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":"0d47fb6243df125a4dcad4fc497fbf5e42a4cf4a776954f04a0e98005a09b15b","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":"Independently audit revision r3 of the permanent CLAUS core protections. Exact candidate is candidates/phase1/contract/src and manifest. Review all four entrypoints: ProtectedToken, ProtectedHook, TradeExtensionHost and PhaseOneFinalizer, including every inherited legacy contract and fixed delegate target. This is NOT r1: owner explicitly requires replaceable trade programs, so the module directory-only r1 is rejected and must never be activated. Current mainnet remains unchanged. Original r1/r2 paid reviews remain separate historical reviews, not authority for this revision.\n\nRejecting a specifically oversized caller payload or an insufficient caller gas budget is allowed; neither may persistently affect a later valid trade. Check that no owner, new program, unexpected caller, malformed program quote or composition can freeze transfers/buys/sells/LP exits, confiscate holder assets, redirect previously accrued rewards or exceed 15% aggregate core plus actual main-pool protocol fees. The hook bounds its TOTAL fee plan to 14.9%, reserving 0.1% for v4 protocol fees and checking actual proportional charges after swap. Six pips allocations sum to one fee, not six independent limits. All four buy/sell/exact-input/exact-output modes, rounding, zero-rate plans, partial fills, max protocol fees, zero stakers, invalid data and integer limits matter.\n\nTradeExtensionHost code is fixed and hash-checked by the core constructor, but activeModule remains replaceable by token owner. Module fee plans are gas-bounded STATICCALLs, with exact six-word output and no arbitrary code/storage authority. Malformed, excessive, write-attempting, reverting or OOG plans fall back to default 2.3%. Do not mistake fee plan approval for arbitrary delegate authority. Plans and module identity are snapshotted in transient slots; test reentrancy and contamination across multi-swaps, internal buybacks and malicious views. Address-dependent quotes still cannot exceed the common ceiling or reject trading. Unrelated gas and market slippage, or independent third-party pool fees, are outside the ceiling.\n\nOnly newly earned fees can be minted as a PoolManager claim to the program itself, per its bounded plan; no old vault, NFT backing or LP assets can be taken. Check complete allocation arithmetic and original default fee recipients. Future rates and distribution are intentionally changeable, but past ownership/claims are not. Legacy rate getters represent the DEFAULT policy; actual integrations need the per-trade quote. Confirm no hidden baseline charge stacks on the plan.\n\nNo stateful arbitrary CALL executes inside a user's swap. It could poison unlocked PoolManager deltas even without reverting. Instead, fixed host record() attests a trade, commits to its original module/data/amounts, and emits a receipt. Anyone can call execute() separately while PM is locked, at most once after success. It requires original data, original module, gas bounds and callback selector. Failure must roll back only delivery and preserve receipt; replay and replacement must not redirect it. Sender is the PM caller (often a router), not proven beneficial owner. No receipt is proof that a program actually completed business work; no autonomous dispatcher is in this contract release. Examine locked checks, return bombs, forged receipts, reentrancy, callback effects and gas griefing. A malicious optional program must not block the original trade.\n\nThe token retains ERC20/ownership/metadata namespaces and removes privileged mint/restriction/upgrade logic. The finalizer atomically upgrades both actual proxies then renounces both actual ProxyAdmin owners; cancellation before completion must recover both even after partial handover. Remaining token owner may select programs and edit metadata, not core protections. Check all codehash/constructor checks, alternative selectors and delegate chains. Fixed legacy burn/liquidity processors remain by exact address AND runtime hash. Existing timed-burn interval is preserved, no new activation. New programs can use their own future allocations for different independent processors.\n\nMainnet addresses: token 0x1b54e762aa34cf6e28e9c082f2848e28e45da6b8; hook 0x37bfb8ac7c960e558657871d41ca70e07e7dbfff; tokenAdmin 0xfb4b9d2ba3ef17787bfe3fd90c3ea7b319807a57; hookAdmin 0x1f22e4b065b2f15fed5be556dc0fa636048d29bc. Exact baselines in manifest/inventory; fork tests use isolated chain31337 at the exact Ethereum block recorded in the manifest and injected test-user funds, not production writes. EIP170 size, layout, custody and atomic finalization require scrutiny because final core is irreversible.\n\nReport concrete findings with severity, affected code, exploit preconditions and required fix. Do not deploy, mutate repositories, accept terms, send social posts or spend funds. Passing local cases are not an independent audit. Distinguish verified bugs from hypothetical concerns and limits of review. Trade-freezing, custody, ceiling and alternative-upgrade findings are release-blocking.\n\n\nRevision r3 addresses actual accepted findings in the original r1 audit that also affected r2: PhaseOneFinalizer now pins the canonical proxies/admins and checks raw implementation roles and current token ownership; ProtectedHook pins all thirteen existing mandatory dependency addresses/runtime hashes plus attester, question hash and spacing. Verify these fixes rather than assuming they work. ProtectedToken and fee accounting are unchanged from r2. TradeExtensionHost additionally fixes the independently reported caller-input bypass: oversized user data (>4096 bytes) and insufficient caller gas reject instead of choosing a cheaper default plan or dropping the receipt. Valid-input program failures still fall back safely. Check that this distinction works in real routed trades as well as view quotations. Never treat either earlier review as approval of this revision.\n\nThe existing sources for the previous OpenTransferToken, TimedBuybackHook and ClausLpRewards are supplied in candidates/phase1/references. Compare metadata/storage and mandatory custody dependencies. The additional local cases assert exact metadata and nonempty 12-hour schedule slots survive finalization. There are 19 core and four pool-program cases. The example under test/support is local test code only, not a production deployment target: it reads a test oracle and uses its own earned claims for buy/burn and own LP add/rebalance/remove. Verify failed oracle/foreign-claim actions do not prevent ordinary user trades or exits. No automatic executor or live selected module is part of phase one.\n\nKnown retained boundaries: fallback after a failed program quote still records the program with zero program fees; receipts do not prove successful quotation or paid work, and programs must inspect actual fee fields. fee plans may change immediately within the shared ceiling, so slippage applies; no mandatory timelock was requested. Dust whose protocol fee rounds above the strict 15% cap is intentionally rejected rather than overcharged. Historical and new NFT accrual events are aliases for the same single payment. Evaluate real exploit paths and material residual risks; do not silently weaken the user's ceiling or require removing useful pool actions.\n","parentJobId":null,"planHash":"ea3c006c8290da0ce012a6f71ae56a18768379906c129e73a27a011652fa188a","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"b17295c2-b8a1-4cfc-a730-d3693ee8c6c6","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":"52682","feedbackHash":"8b03780f2ec4186e42cfe1d32434ec01249d31911920ba28bb1d8e42c0c024c0","nodeKey":"audit_economics","submissionHash":"e8d1ce6a613fc0573c27838217500060920b9f17448cc60a05e6f97f7877788e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52611","feedbackHash":"631ddbbf8bf7e8e041c2e551ad4697ce36b3df37b574904c0d76c39222ac0fd4","nodeKey":"audit_flow","submissionHash":"778399d45aefd60d06b27a984f0a5429a5c32cf90f0cff4fa84fd6e404ea003e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51039","feedbackHash":"4d5104fc219273f70bf27ec50f471a42ab1e1380839a614b31517fe06bef530e","nodeKey":"audit_judge","submissionHash":"1ecbd80033c9b34cdb8e261b3fe0d4b941d92495d9340db5284b3998f73eaf53","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51251","feedbackHash":"cebef9437cd996ab81b5693e2388e25eb7335ae589e76e5665c9644fb0f80a0d","nodeKey":"audit_math","submissionHash":"fc3ab96dcb2df12bb2be00e2415dc756b68c7d6b8f5191d0ac428b582af9b40e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52265","feedbackHash":"218c6c40f550f361e83c9b3dee8c5ee9b7847093ed17612a4c287d821d03d760","nodeKey":"audit_permissions","submissionHash":"0d47fb6243df125a4dcad4fc497fbf5e42a4cf4a776954f04a0e98005a09b15b","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"76d1e3d5e51dec8eb70f9ee188c01b10a864b2fbff2262aa1c6810cdd4adeb06","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8db4eebdd7bd2744","findings":[{"citation":"resolved","description":"The 4096-byte limit is enforced before the `module==address(0)` early return. In the phase-one initial state (no program selected) the payload is never read, yet any main-pool swap whose router attaches more than 4096 bytes of hookData reverts in beforeSwap (ProtectedHook._beforeSwapExtension -> extensions.quote). The current mainnet hook ignores hookData entirely, so this is a behaviour change for integrators that pack large hookData, independent of any program. The task allows rejecting oversized caller payloads, so this is reported as a low-severity behaviour change rather than a freeze: the caller controls the payload and nothing persists. Required fix (if the no-program path should stay identical to today): move the length check after the `module==address(0)` return, or document that main-pool swaps must carry at most 4096 bytes of hookData from finalization onward.","line":69,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"State: activeModule == address(0) (default after deployment). Input: host.quote(ROUTER, true, -0.005 ether, new bytes(4097)) -> reverts HookDataTooLarge; a routed swap with the same hookData reverts the whole swap. Expected (today's hook): hookData ignored, swap succeeds. Actual: revert. With new bytes(4096) the same swap succeeds.","severity":"low","snippet":"        if(data.length>4096) revert HookDataTooLarge();","title":"Oversized hookData is rejected even when no trade program is selected (phase-one initial state)"},{"citation":"resolved","description":"afterSwap receives the PoolManager's swap delta before the hook delta is subtracted (v4 Hooks.afterSwap passes `swapDelta` and subtracts the hook delta afterwards). FeeEfficientHook._settleFee relies on exactly this (`expected = requested - fee` for exact-input buys). The receipt therefore stores amount0 = -(requested - fee) for an exact-input buy, and amount0 = requested + fee for an exact-output sell, not what the user paid or received. A program that treats `amount0` as the trader's gross ETH undercounts by `feeEth` on buys and overcounts on exact-output sells. Not a custody or ceiling defect; receipts are explicitly informational. Fix: document the delta basis in IClausTradeModule / TradeRecorded, or store the user-side amounts (pool delta adjusted by the hook delta) in the Trade struct.","line":95,"path":"candidates/phase1/contract/src/ProtectedHook.sol","reproduction":"Program active with rate 149000, exact-input buy of 0.005 ether (testReplaceableProgramFeeBudgetAndFourTradeModes setup): fee = 0.005e18*149000/1e6 = 7.45e14; trade.amount0 == -(5e15 - 7.45e14) = -4.255e15 while the user paid 5e15. Expected by a naive program: -5e15. Actual: -4.255e15; the difference equals trade.feeEth.","severity":"info","snippet":"            extensions.record(module,sender,params.zeroForOne,delta.amount0(),delta.amount1(),fee,extra,keccak256(data));","title":"Receipt amounts are pool-side deltas, not user-side amounts; programs must add feeEth"},{"citation":"resolved","description":"execute() is permissionless and the caller selects gasLimit. The host guarantees the module receives the full gasLimit (verified: gasleft at the CALL is >= gasLimit*64/63 + ~24k, so the 63/64 rule never trims it) and that a failed delivery rolls back and preserves the receipt. However a program whose onTrade returns the selector without completing its work when gas is short (for example a try/catch around its own pool action) has its receipt consumed by a third party calling execute(trade, data, 10_000). This is a residual risk inside the documented design (receipts do not prove paid work); the host itself has no defect. Programs should revert when gasleft() is below their own requirement so the receipt survives for a later, properly funded delivery.","line":108,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"State: a receipt recorded for module M whose onTrade wraps its work in try/catch and always returns onTrade.selector. Input: any address calls execute(trade, originalData, 10000). Expected by the program author: delivery with enough gas to act. Actual: onTrade runs with exactly 10,000 gas, its inner action fails silently, the selector is returned, pending[id] is deleted and TradeExecuted is emitted; the receipt can never be redelivered.","severity":"info","snippet":"        if(gasLimit<10_000 || gasLimit>5_000_000 || gasleft()<gasLimit+gasLimit/63+30_000) revert ModuleExecutionFailed();","title":"Any caller chooses the delivery gas limit (minimum 10,000); programs must defend their own receipts"},{"citation":"resolved","description":"setModule has no timelock and the quote receives `sender` (the PoolManager caller, usually a router). A program may return [0,0,0,0,0,149000] for every sender except the owner's own router and a zero plan for that router, directing 14.9% of all other volume to itself as new PoolManager claims. The ceiling, non-rejection and old-fund isolation all hold (verified: per-field and sum caps at MAX_RATE, fallback on any malformed output, only newly earned fee is minted to the module in FeeNftStakingHook._accrue), so this is the intended retained authority documented in README/REVIEW-FOLLOWUP, not a bypass. It is recorded here so the requester's users can see the asymmetry: quotes are per-address and can change between quotation and execution; router slippage is the only user-side protection.","line":55,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"Owner calls setModule(P) where P.quote returns [0,0,0,0,0,149000] when sender != OWNER_ROUTER and [0,0,0,0,0,0] otherwise. Next exact-input buy of 1 ether via any other router: fee = 0.149 ether minted as claim to P; the same trade via OWNER_ROUTER: fee 0. Both are accepted by the core (sum <= 149000).","severity":"info","snippet":"        if(msg.sender != IClausGovernor(TOKEN).owner()) revert NotAuthorized();","title":"Trust assumption: token owner can install any fee program instantly, including address-dependent plans up to 14.9%"},{"citation":"resolved","description":"The r3 finalizer correctly pins the two canonical proxies and admins, checks the live token owner at construction and at finalize, snapshots tokenImpl_/hookImpl_ codehashes and re-checks them before the atomic upgrade, and post-checks version/kind/ceiling on both proxies before renouncing both ProxyAdmins (verified by reading; cancellation returns whichever admins were handed over). What it cannot do is prove that hookImpl_ is a ProtectedHook built from the reviewed source: any contract answering protectionKind()/protectionVersion()/maximumTotalTaxPips() passes. ProtectedHook's own constructor pins all thirteen dependencies, attester, question hash, spacing and the TradeExtensionHost runtime hash, so a genuine ProtectedHook cannot be mis-wired, but the owner could still finalize a different contract. This matches the requester's own disposition ('a self-reported protectionKind is not proof of source'). Open item for the release process: publish the expected hookImplementation/tokenImplementation addresses and the finalizer's hookCodeHash/tokenCodeHash and have independent parties reproduce them before the admins are transferred.","line":43,"path":"candidates/phase1/contract/src/PhaseOneFinalizer.sol","reproduction":"Deploy contract F with protectionKind()=keccak256('CLAUS_HOOK'), protectionVersion()=1, maximumTotalTaxPips()=150000 and arbitrary other logic. new PhaseOneFinalizer(OWNER, TOKEN, HOOK, TA, HA, tokenCandidate, address(F)) succeeds; after both admin transfers, finalize() installs F permanently. Expected: only a ProtectedHook built from the pinned source can be installed. Actual: any contract with those three getters can be, so verification of the implementation address/codehash must happen off-chain before handover.","severity":"info","snippet":"        if (IProtectedCore(tokenImpl_).protectionKind() != keccak256(\"CLAUS_TOKEN\")","title":"Finalizer binds implementations by codehash but only verifies self-reported protectionKind; reviewers must compare hookCodeHash out of band"},{"citation":"resolved","description":"ProtectedToken keeps the ERC-7201 Ownable namespace (owner() is what TradeExtensionHost.setModule and the finalizer consult) and adds Ownable2StepUpgradeable. If the existing implementation already uses Ownable2Step and the proxy's Ownable2Step namespace slot (0x237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00) holds a non-zero pendingOwner at finalization, that address can call acceptOwnership() afterwards and take the remaining owner powers (program selection, metadata). The reference OpenTransferToken inherits BurnableToken from a package not included in the bundle, so this could not be checked from the supplied sources, and no fork RPC was available to read the slot. This is a pre-finalization state check, not a code defect.","line":10,"path":"candidates/phase1/contract/src/ProtectedToken.sol","reproduction":"Check: vm.load(0x1b54E762aa34CF6E28E9C082F2848e28E45DA6b8, 0x237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00) at the finalization block must be zero. If it is X != 0, then after finalize(): vm.prank(X); T.acceptOwnership(); T.owner() == X and X can call host.setModule. Expected: only the current owner retains program selection.","severity":"info","snippet":"contract ProtectedToken is ERC20Upgradeable, Ownable2StepUpgradeable {","title":"Finalization must confirm the live token proxy has no stale Ownable2Step pendingOwner"},{"citation":"resolved","description":"When block.timestamp < nftStakingStartsAt, _accrue delegates to FeeLpRewardsHook._accrue, which computes project = gross/50 and platform = totalFee - project with checked arithmetic. Under any program plan whose rate is below 2% (including the supported zero-fee plan) totalFee < gross/50 and the subtraction underflows, reverting the swap: a trade freeze, not a fallback. nftStakingStartsAt is an immutable read from the pinned ClausNftStaking (0x4e59...CAD4) whose startsAt precedes the manifest block, and the finalizer can only install against mainnet, so the branch is dead in production (confirmed by the requester's note that the pinned staking start precedes the target chain state). Reported as information because the final core is irreversible and the dead branch is the only path in the accounting kernel that can turn a valid plan into a revert; removing it (or routing it through the plan-based accounting) would eliminate the hazard at no behavioural cost.","line":43,"path":"candidates/phase1/contract/src/legacy/privacy/FeeNftStakingHook.sol","reproduction":"Hypothetical state only: block.timestamp < nftStakingStartsAt, active program returning a zero plan (mode 7 of ProgrammableTestModule). Exact-input buy of 1 ether: totalFee = 0, project = 2e16, platform = 0 - 2e16 -> Panic(0x11), swap reverts. Expected: zero-fee trade succeeds. On mainnet at block 26197496 the condition is false, so the path is not reachable.","severity":"info","snippet":"        if (block.timestamp < nftStakingStartsAt) {","title":"Unreachable pre-staking accrual branch would revert every swap under a low-rate plan if it were ever live"}],"hash":"0d47fb6243df125a4dcad4fc497fbf5e42a4cf4a776954f04a0e98005a09b15b","nodeId":"a136a6fd-6996-4eea-a8b6-d76a4433a770","outcome":"completed","summary":"The findings file is written at the repository root and validated. Here is the outcome of the review.\n\n**Result: no release-blocking defect found in revision r3.** The findings file holds one low and six informational items. None carries a proof test because none reaches high or critical.\n\n**What was verified in depth**\n\n- **Access control inventory.** Every state-changing entry point on all four contracts and the inherited hook chain has the guard its effect requires: hook callbacks are pool-manager only, `record` is hook only, `setModule` is token-owner only, `execute` is permissionless but bounded, the finalizer is owner only, the two legacy delegate targets are immutable and pinned by address and runtime hash. The weather attester and arena paths touch no fee or transfer logic after the burn-rate override.\n- **r3 fixes.** The finalizer pins the canonical proxies and admins, checks the live token owner at construction and at finalize, re-checks implementation codehashes, post-checks both proxies, and renounces both admins atomically. Cancellation returns whichever admins were handed over. The hook constructor pins all thirteen dependencies plus attester, question hash, spacing and the host runtime hash.\n- **Gas and payload bypass.** I built a fork-free harness with mocks at the constant addresses. At the lowest accepted caller gas, a program consuming its maximum admissible budget still has its plan applied. Below that, the quote reverts instead of selecting the default. The 63/64 rule never trims the program's budget in either `quote` or `execute`. Oversized data rejects before and after a program is active.\n- **Ceiling and allocation arithmetic.** Six allocations sum to one rate, capped at 149,000 pips per field and in total. The protocol-fee check runs on the actual accrued charge after the swap for both directions. Rounding never lets the component sum exceed the collected fee in any of the four trade modes, so the project remainder cannot underflow. No baseline charge stacks on the plan. Only newly earned fee is minted to a program.\n- **Receipts.** Binding to module, data hash and all fields holds. Replay, redirection, in-swap execution and reentry through the busy flag are rejected. Failed delivery preserves the receipt.\n\n**Items reported**\n\n- **Low.** Oversized hookData reverts even with no program selected, a behaviour change from the current hook for the phase-one initial state.\n- **Info.** Receipt amounts are pool-side deltas; programs must add the fee field. Any caller picks the delivery gas limit, so programs must revert when short rather than return the selector. Owner program selection is instant and may be address-dependent within the ceiling. The finalizer only sees self-reported kind, so the implementation codehash must be reproduced out of band before handover. The token proxy's Ownable2Step pending-owner slot must be zero at finalization. A dead pre-staking accrual branch would freeze swaps under a sub-2% plan if it were ever reachable.\n\n**Limits of this review**\n\n- No fork RPC was available, so the nineteen core and four pool-program fork cases were not re-run here. Claims about live proxy admin wiring, metadata slot equality and the pending-owner slot rest on the recorded r3 fork results and should be confirmed on the exact block before admins are transferred.\n- The old token's base contract is not in the bundle, so storage comparison for the token relied on the recorded fork assertions.\n- Legacy processor and vault code was reviewed for the paths the core calls into, not re-audited in full.","treeHash":null,"usage":{"cachedInputTokens":2239233,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":61358,"runtime":"claude","turns":41,"wallClockMs":934996}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9ab27edcfd62be02","findings":[{"citation":"resolved","description":"finalize() checks ICurrentTokenOwner(token).owner()==owner (line 52) while the OLD implementation is still active, upgrades both proxies, and then verifies only protectionVersion/protectionKind/maximumTotalTaxPips (lines 56-58). Those three selectors are pure constants in ProtectedToken/ProtectedHook, so they prove an identity claim, not that the new implementation's ERC-7201 Ownable/ERC20/metadata namespaces coincide with the proxy's live storage. Because renounceOwnership() of both ProxyAdmins follows in the same transaction (line 59), a namespace mismatch would be permanent: owner() would read address(0), so TradeExtensionHost.setModule and ProtectedToken.updateMetadata would be unusable forever, or balances/name would read empty. The real r3 ProtectedToken is NOT affected (control test: with ProtectedToken as implementation the owner survives; the live proxy's Ownable slot 0x9016d0..9300 holds 0xBb145c.. and its Ownable2Step pendingOwner slot 0x237e15..2c00 reads zero via RPC at the current head, so no stale acceptOwnership path exists today). The gap is that the irreversible step relies on the operator passing the right artifact rather than on the finalizer checking continuity itself. Required fix (keeps the atomic design, a few hundred bytes): capture ownerBefore, totalSupply() and keccak256(name()) at the top of finalize(), and after the two upgradeAndCall calls revert InvalidFinalization() unless owner()==owner, totalSupply()==supplyBefore and name hash unchanged, before calling renounceOwnership(). Merged from audit_economics (finalizer continuity) and audit_permissions (stale pendingOwner state check, resolved by RPC read).","line":56,"path":"candidates/phase1/contract/src/PhaseOneFinalizer.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {ProxyAdmin} from \"@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol\";\nimport {PhaseOneFinalizer} from \"src/PhaseOneFinalizer.sol\";\nimport {ProtectedToken} from \"src/ProtectedToken.sol\";\n\n/// Minimal transparent proxy standing in for the canonical proxies (etched at their addresses).\ncontract MiniProxy {\n    bytes32 constant IMPL = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;\n    bytes32 constant ADMIN = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;\n    function _load(bytes32 s) private view returns (address a) { assembly { a := sload(s) } }\n    function upgradeToAndCall(address impl, bytes calldata data) external payable {\n        require(msg.sender == _load(ADMIN), \"not admin\");\n        bytes32 s = IMPL; assembly { sstore(s, impl) }\n        if (data.length > 0) { (bool ok,) = impl.delegatecall(data); require(ok); }\n    }\n    fallback() external payable {\n        address impl = _load(IMPL);\n        assembly {\n            calldatacopy(0, 0, calldatasize())\n            let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)\n            returndatacopy(0, 0, returndatasize())\n            if iszero(ok) { revert(0, returndatasize()) }\n            return(0, returndatasize())\n        }\n    }\n}\n/// Current implementation stand-in: owner lives in the ERC-7201 Ownable namespace.\ncontract LegacyTokenImpl {\n    bytes32 constant OWNER_SLOT = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300;\n    function owner() external view returns (address a) { bytes32 s = OWNER_SLOT; assembly { a := sload(s) } }\n}\n/// Candidate that passes every finalizer identity check but reads owner from the wrong namespace.\ncontract WrongNamespaceToken {\n    address private _owner; // slot 0: never written in the proxy\n    function owner() external view returns (address) { return _owner; }\n    function protectionVersion() external pure returns (uint256) { return 1; }\n    function protectionKind() external pure returns (bytes32) { return keccak256(\"CLAUS_TOKEN\"); }\n    function maximumTotalTaxPips() external pure returns (uint24) { return 150_000; }\n}\ncontract HookImplStub {\n    function protectionVersion() external pure returns (uint256) { return 1; }\n    function protectionKind() external pure returns (bytes32) { return keccak256(\"CLAUS_HOOK\"); }\n    function maximumTotalTaxPips() external pure returns (uint24) { return 150_000; }\n}\n\ncontract FinalizerPostCheckTest is Test {\n    address constant OWNER = 0xBb145cA83272c3806D4dDc75cA1D5514789cF1C5;\n    address constant TOKEN = 0x1b54E762aa34CF6E28E9C082F2848e28E45DA6b8;\n    address constant HOOK = 0x37Bfb8AC7C960E558657871D41Ca70E07e7DbfFf;\n    address constant TA = 0xfB4b9d2ba3eF17787bFE3FD90C3eA7b319807a57;\n    address constant HA = 0x1f22E4B065b2f15Fed5BE556DC0fA636048d29bc;\n    bytes32 constant IMPL = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;\n    bytes32 constant ADMIN = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;\n    bytes32 constant OWNER_SLOT = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300;\n\n    function setUp() public {\n        bytes memory proxyCode = address(new MiniProxy()).code;\n        bytes memory adminCode = address(new ProxyAdmin(OWNER)).code;\n        address legacyToken = address(new LegacyTokenImpl());\n        address legacyHook = address(new HookImplStub());\n        vm.etch(TOKEN, proxyCode); vm.etch(HOOK, proxyCode);\n        vm.store(TOKEN, ADMIN, bytes32(uint256(uint160(TA)))); vm.store(HOOK, ADMIN, bytes32(uint256(uint160(HA))));\n        vm.store(TOKEN, IMPL, bytes32(uint256(uint160(legacyToken)))); vm.store(HOOK, IMPL, bytes32(uint256(uint160(legacyHook))));\n        vm.store(TOKEN, OWNER_SLOT, bytes32(uint256(uint160(OWNER))));\n        vm.etch(TA, adminCode); vm.etch(HA, adminCode);\n        vm.store(TA, bytes32(0), bytes32(uint256(uint160(OWNER)))); vm.store(HA, bytes32(0), bytes32(uint256(uint160(OWNER))));\n        assertEq(LegacyTokenImpl(TOKEN).owner(), OWNER);\n        assertEq(ProxyAdmin(TA).owner(), OWNER);\n    }\n    function _handover(PhaseOneFinalizer f) internal {\n        vm.startPrank(OWNER);\n        ProxyAdmin(TA).transferOwnership(address(f)); ProxyAdmin(HA).transferOwnership(address(f));\n        vm.stopPrank();\n    }\n    /// Fails on current code: finalize() succeeds although the upgraded proxy no longer reports the owner,\n    /// and both ProxyAdmins are renounced in the same transaction. Passes once finalize() re-checks owner()\n    /// through the new implementation before renouncing.\n    function testFinalizeMustRevertWhenOwnerIsLostAfterUpgrade() public {\n        PhaseOneFinalizer f = new PhaseOneFinalizer(OWNER, TOKEN, HOOK, ProxyAdmin(TA), ProxyAdmin(HA), address(new WrongNamespaceToken()), address(new HookImplStub()));\n        _handover(f);\n        vm.prank(OWNER);\n        vm.expectRevert(PhaseOneFinalizer.InvalidFinalization.selector);\n        f.finalize();\n        assertEq(LegacyTokenImpl(TOKEN).owner(), OWNER, \"owner must survive or finalization must fail\");\n    }\n    /// Control: the real ProtectedToken keeps the owner; unaffected by the fix.\n    function testRealProtectedTokenPreservesOwner() public {\n        PhaseOneFinalizer f = new PhaseOneFinalizer(OWNER, TOKEN, HOOK, ProxyAdmin(TA), ProxyAdmin(HA), address(new ProtectedToken()), address(new HookImplStub()));\n        _handover(f);\n        vm.prank(OWNER); f.finalize();\n        assertEq(ProtectedToken(TOKEN).owner(), OWNER);\n        assertEq(ProxyAdmin(TA).owner(), address(0)); assertEq(ProxyAdmin(HA).owner(), address(0));\n    }\n}","reproduction":"Local test test/scratch/FinalizerPostCheck.t.sol (no fork): the canonical proxy/admin addresses are etched with a minimal transparent proxy and OZ ProxyAdmin, the proxy's ERC-7201 Ownable slot holds the owner, and the candidate token implementation returns the three expected getters but reads owner() from slot 0. Constructor and both admin handovers succeed. Owner calls finalize(). Expected: revert InvalidFinalization (owner lost). Actual: finalize() succeeds, token.owner()==address(0), both ProxyAdmin.owner()==address(0). forge test --match-path test/scratch/FinalizerPostCheck.t.sol: testFinalizeMustRevertWhenOwnerIsLostAfterUpgrade FAILS ('next call did not revert as expected'); control testRealProtectedTokenPreservesOwner PASSES.","severity":"low","snippet":"        if(IProtectedCore(token).protectionVersion()!=1 || IProtectedCore(hook).protectionVersion()!=1","title":"finalize() renounces both ProxyAdmins without re-verifying owner/storage continuity through the new implementations"},{"citation":"resolved","description":"The 4096-byte limit is enforced before the module==address(0) early return on line 71. In the phase-one initial state no program is selected and the payload is never read, yet any main-pool swap whose router attaches more than 4096 bytes of hookData reverts in beforeSwap (ProtectedHook._beforeSwapExtension -> extensions.quote -> HookDataTooLarge). The live FeePrivacyHook ignores hookData entirely, so aggregators that forward large opaque bytes would start failing at finalization, before any program exists. The brief allows rejecting an oversized caller payload and nothing persists (the same caller succeeds with <=4096 bytes), so this is not a freeze; it is an unannounced change to the no-program path. Fix if the no-module path should stay identical to today: move the length check after the module==address(0) return (record() only runs when a module is set, so receipts are unaffected), or document the 4096-byte limit for integrators. Merged from audit_permissions and audit_flow.","line":69,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"test/scratch/HostUnit.t.sol testOversizedDataRejectedWithoutModule (passes, confirming the behaviour): activeModule==address(0); host.quote(addr, true, -1 ether, new bytes(4097)) reverts HookDataTooLarge; host.quote(..., new bytes(4096)) returns (address(0), DEFAULT_PLAN). Expected per the live hook: hookData ignored.","severity":"low","snippet":"        if(data.length>4096) revert HookDataTooLarge();","title":"Oversized hookData (>4096 bytes) is rejected even when no trade program is selected (behaviour change from the live hook)"},{"citation":"resolved","description":"execute() has no caller restriction and accepts any gasLimit in [10_000, 5_000_000]; the host only requires that onTrade returns its selector (line 117). The host does guarantee the module receives the full gasLimit (the 63/64 check on this line) and that a failed delivery rolls back and keeps the receipt. But a program whose onTrade catches its own failures (try/catch or low-level call) and still returns the selector has its receipt permanently consumed by anyone calling execute(trade, data, smallGas): pending[id] is deleted, TradeExecuted is emitted, and the receipt cannot be re-issued. This is inside the documented boundary (a receipt is not proof of paid work) and touches no user funds or core accounting, but the consequence is one-shot and irreversible per receipt. Fix: document that onTrade must revert (not swallow) on any failed step, and/or let the module declare a minimum delivery gas (e.g. a view consulted by execute()) that is enforced as a floor on gasLimit. Merged from audit_permissions and audit_flow.","line":108,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"test/scratch/HostUnit.t.sol testThirdPartyLowGasExecuteConsumesReceipt (passes, confirming the behaviour): HOOK records a receipt for module M whose onTrade does target.call{gas:3000}(work()) and returns the selector regardless. An unrelated address calls host.execute(trade, '', 20_000). Expected by the program author: work done or receipt kept. Actual: pending[1]==0, target.n()==0 (work never ran), and a later execute(trade, '', 2_000_000) reverts InvalidReceipt.","severity":"low","snippet":"        if(gasLimit<10_000 || gasLimit>5_000_000 || gasleft()<gasLimit+gasLimit/63+30_000) revert ModuleExecutionFailed();","title":"Permissionless execute() lets any third party pick the delivery gas, consuming a receipt whose program swallows internal failure"},{"citation":"resolved","description":"r3 rejects >4096-byte data and insufficient caller gas, but the fixed 80,000-gas STATICCALL budget is shared between the program's own logic and whatever work the caller's data induces. A program whose quote cost scales with data length can be pushed out of gas by a swapper supplying up to 4096 bytes of junk; the host then applies DEFAULT_PLAN (module allocation 0) and, in a real swap, still records a receipt with moduleFeeEth=0. When the program's plan is above 2.3% this is a swapper-side fee reduction; when below, a swapper-side overcharge, always inside the 15% ceiling and never a revert. This is the retained 'program failures fall back safely' boundary, not a host defect; recorded so program authors know a quote must be O(1) in data length and well under 80k gas for any 4096-byte input, and must not treat a receipt as proof the quoted plan applied. Optional hardening: pass the program only keccak256(data) (plus a bounded prefix) so data-dependent work is capped in the host.","line":78,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"test/scratch/HostUnit.t.sol testCallerDataCanStarveProgramQuoteIntoDefault (passes): owner installs a module whose quote loops over data and returns [0,0,0,0,0,149000]. host.quote(addr,true,-1 ether,'') yields rate 149000; host.quote(..., new bytes(4096)) yields exactly DEFAULT_PLAN (2.3%, module 0). Expected under the program's intent: 14.9%. Actual: 2.3%.","severity":"info","snippet":"            ok := staticcall(80000,module,add(input,32),mload(input),allocations,192)","title":"Caller hookData within the 4096-byte limit can still starve a data-dependent program quote into the default 2.3% plan"},{"citation":"resolved","description":"setModule has no timelock and the quote receives the PoolManager caller (usually a router) plus full chain context. A program may therefore (a) return [0,0,0,0,0,149000] for every router except the owner's own and zero for that one, directing 14.9% of all other volume to itself as new PoolManager claims, and (b) answer differently to quoteTradeFee (an eth_call, MANAGER.isUnlocked()==false) and to the swap-time quote (isUnlocked()==true), so the view getter the hook exposes on ProtectedHook.sol line 83 is indicative, not a commitment. All core bounds hold: per-field and sum caps at MAX_RATE, fallback on malformed output, only newly earned fee minted to the module in FeeNftStakingHook._accrue, no rejection path. This is the intended retained authority (README/REVIEW-FOLLOWUP: quotes are indicative, slippage applies). Recorded so users and integrators see the asymmetry; router slippage is the only user-side protection. No core change requested; a voluntary delay on module changes is optional. Merged from audit_economics (two-faced quote) and audit_permissions/audit_flow (owner trust assumption).","line":55,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"Code-level: TradeExtensionHost.quote line 75 forwards sender and data to the program; IClausTradeModule.quote is a view that may call MANAGER.isUnlocked() or branch on sender; nothing in the host compares the swap-time plan with any earlier quotation. Owner calls setModule(P) with P.quote returning [0,0,0,0,0,149000] when sender!=OWNER_ROUTER (or when isUnlocked()) and zeros otherwise: both plans pass lines 83-90 (sum<=149000), the next buy pays 14.9% to P while quoteTradeFee reported 0. The specialist's fork case (module claim grew by 0.005 ether*149000/1e6 wei) was not independently rerun here.","severity":"info","snippet":"        if(msg.sender != IClausGovernor(TOKEN).owner()) revert NotAuthorized();","title":"Trust assumption: token owner can install any program instantly; quotes are sender- and context-dependent and only the 14.9% ceiling is binding"},{"citation":"resolved","description":"v4 Hooks.afterSwap passes swapDelta to the hook and subtracts the hook's returned delta afterwards (Hooks.sol afterSwap: callHookWithReturnDelta(..., swapDelta, ...) then hookDelta applied in PoolManager.swap). FeeEfficientHook._settleFee relies on exactly this (expected = requested - fee for exact-input buys, line 254). The receipt therefore stores amount0 = -(requested - fee) for an exact-input buy and requested + fee for an exact-output sell, not what the user paid or received. A program treating amount0 as the trader's gross ETH undercounts by feeEth on buys and overcounts on exact-output sells. Not a custody or ceiling defect; receipts are informational. Fix: document the delta basis in IClausTradeModule/TradeRecorded, or store the user-side amounts (pool delta adjusted by the hook delta).","line":95,"path":"candidates/phase1/contract/src/ProtectedHook.sol","reproduction":"Exact-input buy of 0.005 ether with an active 149000 plan: fee = 0.005e18*149000/1e6 = 7.45e14; the delta passed to afterSwap has amount0 = -(5e15 - 7.45e14) = -4.255e15 (verified from Hooks.afterSwap ordering and _settleFee's `expected = requested - fee` check), while the user paid 5e15. Expected by a naive program: -5e15. Actual: -4.255e15; the difference equals trade.feeEth.","severity":"info","snippet":"            extensions.record(module,sender,params.zeroForOne,delta.amount0(),delta.amount1(),fee,extra,keccak256(data));","title":"Receipt amounts are pool-side swap deltas, not user-side amounts; programs must add feeEth"},{"citation":"resolved","description":"r3 correctly pins the canonical proxies/admins (lines 33-37), checks the live token owner at construction and at finalize, snapshots the implementation codehashes and re-checks them before the atomic upgrade, and post-checks version/kind/ceiling before renouncing both ProxyAdmins; cancel() returns whichever admins were handed over. What it cannot do is prove that tokenImpl_/hookImpl_ were built from the reviewed source: any contract answering the three getters passes, and its codehash is then frozen as the irreversible final core. A genuine ProtectedHook cannot be mis-wired (its constructor pins all thirteen dependencies, attester, question hash, spacing and the host runtime hash), but the owner could still finalize a different contract. This matches the requester's own disposition ('a self-reported protectionKind is not proof of source'). Open release-process item: publish the expected implementation addresses and the finalizer's tokenCodeHash/hookCodeHash, and have independent parties reproduce them (manifest runtime hashes with immutables applied) before transferring the ProxyAdmins; or embed the expected codehashes as constants in the finalizer. Merged from audit_permissions and audit_flow.","line":43,"path":"candidates/phase1/contract/src/PhaseOneFinalizer.sol","reproduction":"test/scratch/FinalizerPostCheck.t.sol testRealProtectedTokenPreservesOwner (passes): the hook implementation passed to the finalizer is HookImplStub, a stub with only protectionVersion()/protectionKind()/maximumTotalTaxPips() and no hook logic. Constructor accepts it, finalize() installs it on the hook proxy and renounces both admins. Expected (operator intent): only the reviewed ProtectedHook is accepted. Actual: any such stub is accepted.","severity":"info","snippet":"        if (IProtectedCore(tokenImpl_).protectionKind() != keccak256(\"CLAUS_TOKEN\")","title":"Finalizer accepts any implementation that self-reports protectionKind/version/ceiling; artifact hashes must be verified off-chain before admin handover"},{"citation":"resolved","description":"FeeNftStakingHook._accrue (line 43) delegates to this legacy _accrue when block.timestamp < nftStakingStartsAt. That path hard-codes project = gross/50 (FeeMath.projectFromGross) and computes platform = totalFee - project with checked arithmetic. Under r3 totalFee is the program-selected rate, which may be anywhere in [0, 14.9%]; any rate below 20_000 pips (including the supported zero-fee plan) underflows and reverts the swap, i.e. a trade freeze rather than a fallback. nftStakingStartsAt is an immutable read from the pinned ClausNftStaking 0x4e59..CAD4 whose startsAt() reads 1791672095 via RPC, earlier than the manifest fork block 26197496 timestamp 1792062275, so on the target chain the branch is unreachable and the plan-based path (lines 47-79 of FeeNftStakingHook) is the only live one. Recorded as a latent hazard because the final core is irreversible and this is the only path in the accounting kernel that can turn a valid plan into a revert; removing the branch or routing it through the plan-based arithmetic costs nothing behaviourally. Merged from audit_economics, audit_permissions and audit_flow.","line":161,"path":"candidates/phase1/contract/src/legacy/privacy/FeeLpRewardsHook.sol","reproduction":"Hypothetical state only: block.timestamp < 1791672095 and a module plan [0,0,0,0,0,0] (or any plan with sum < 20000). Exact-input buy of 1 ether: _settleFee computes fee=0, _accrue(1e18, 0): project = 2e16, platform = 0 - 2e16 -> Panic(0x11), swap reverts. Expected: zero-fee trade settles. Not reachable at or after the recorded fork block (startsAt already past).","severity":"info","snippet":"        uint256 platform = totalFee - project;","title":"Dead pre-staking accrual branch would underflow and revert every swap for any plan below 2% if it were reachable"},{"citation":"resolved","description":"The buy-side ceiling compares floor(gross*plan) plus the v4 protocol charge against floor(gross*15%). The hook fee and the ceiling floor while v4's protocol fee rounds up (Pool.sol: with lpFee 0, swapFee==protocolFee so the entire SwapMath feeAmount = remaining - floor(remaining*(1e6-fee)/1e6) goes to the protocol), so small trades fail even though no party overcharges. This is the boundary REVIEW-FOLLOWUP calls intentional; this entry only quantifies it so integrators can set minimums. With a 149,000 pip plan and a 1,000 pip zeroForOne protocol fee, gross amounts up to 5,933 wei can revert TotalTaxExceeded (the specialist counted 1,130 distinct failing values); with the default 23,000 pip plan only gross 1-6 wei revert. On the sell side (line 335) token inputs of 1-6 atoms whose protocol fee rounds to one atom revert. No state persists, later trades are unaffected, and no amount above 5,933 wei fails. No fix is required if the strict ceiling is the requested behaviour; the only alternative (integer cross-multiplication of the ceiling) would let one-atom protocol rounding slightly exceed 15% on sub-20,000-wei trades, which the owner has declined.","line":330,"path":"candidates/phase1/contract/src/legacy/FeeEfficientHook.sol","reproduction":"Arithmetic reproduced by hand: protocol fee 1000|(1000<<12), plan sum 149,000, exact-input buy amountSpecified=-5933: fee=floor(5933*149000/1e6)=884, poolIn=5049, protocol=5049-floor(5049*999000/1e6)=6, 884+6=890 > floor(5933*0.15)=889 -> TotalTaxExceeded. amountSpecified=-5934: fee=884, poolIn=5050, protocol=6, 890 <= 890 -> succeeds. Default plan: amountSpecified=-6: fee=0, protocol=1, 1 > 0 -> reverts; -7 succeeds.","severity":"info","snippet":"            if(fee+protocol>gross*150_000/1_000_000) revert TotalTaxExceeded();","title":"Quantified dust boundary of the strict 15% ceiling (intentional): exact-input buys up to 5,933 wei can revert at the maximum plan with the maximum protocol fee"},{"citation":"resolved","description":"The division-by-zero guard protocol==input also fires when both are zero. A oneForZero swap whose price limit is reached before any in-range liquidity (v4 returns a zero BalanceDelta rather than reverting) reaches afterSwap with input=0 and protocol=0: _settleFee computes fee=0 from ethAmount=0, then this check reverts under the ceiling error. The buy side rejects the same situation as PartialNativeFill. Impact is nil: no fee is charged, nothing persists, and the main pool currently has full-range liquidity so the state needs a limit set inside an empty range. Reported for completeness; a fix is `if(input==0) revert PartialNativeFill();` ahead of the ceiling check, or leaving it since the revert is harmless.","line":335,"path":"candidates/phase1/contract/src/legacy/FeeEfficientHook.sol","reproduction":"State: no liquidity between the current sqrt price and the caller's sqrtPriceLimitX96. Input: swap(zeroForOne=false, amountSpecified=-1e18, sqrtPriceLimitX96 = current price + 1 tick). Expected: a no-fill revert or zero-delta success. Actual (by code): delta.amount1()==0 so input=0, protocolFeesAccrued diff=0, `protocol==input` is true and the swap reverts TotalTaxExceeded, misreporting a tax violation. Not executed on a pool locally.","severity":"info","snippet":"            if(protocol>input*150_000/1_000_000 || protocol==input) revert TotalTaxExceeded();","title":"A sell that consumes zero input reverts with TotalTaxExceeded instead of a descriptive no-fill error"}],"hash":"1ecbd80033c9b34cdb8e261b3fe0d4b941d92495d9340db5284b3998f73eaf53","nodeId":"798ea211-6028-42bb-b84b-dbb7b6910b7d","outcome":"completed","summary":"The review is complete. The findings file at the repository root holds 10 merged, reproduced findings, and the candidate source tree is unchanged (only my scratch tests under `candidates/phase1/contract/test/scratch/` exist, and those are not kept).\n\n**What I verified and kept**\n\n- **Low, with proof:** `finalize()` renounces both ProxyAdmins after checking only the three constant identity getters through the new implementations. My local test etches the canonical proxies and admins, installs a token candidate that passes every finalizer check but reads the owner from the wrong namespace, and shows finalization succeeds while the owner becomes zero forever. The test fails on current code and passes with a one-line post-upgrade owner re-check. The real ProtectedToken is not affected, and the live pending-owner slot reads zero over RPC, so the stale Ownable2Step note was folded in as resolved.\n- **Low:** hookData over 4096 bytes is rejected before the no-module early return, a behaviour change from the live hook. Confirmed by a direct call on the host.\n- **Low:** permissionless `execute()` lets anyone choose the delivery gas, so a program that swallows its own failures loses its receipt permanently. Confirmed locally with a swallowing module at 20,000 gas.\n- **Info:** caller data within the limit can starve a data-dependent quote into the default plan. Confirmed locally.\n- **Info:** owner trust assumption and non-binding, sender- or context-dependent quotes, merged from three specialist entries. Reproduced at code level only.\n- **Info:** receipt deltas are pool-side amounts. Confirmed from the vendored v4 Hooks afterSwap ordering.\n- **Info:** the finalizer accepts any self-reporting implementation. My control test installed a stub hook implementation through it.\n- **Info:** the dead pre-staking accrual branch would underflow below a 2% plan. RPC confirms the staking start precedes the fork block, so it is unreachable.\n- **Info:** the dust boundary of the strict ceiling, arithmetic reproduced by hand against v4's protocol fee rounding. Intentional.\n- **Info:** a zero-input sell reverts with the ceiling error rather than a no-fill error. Code-level only.\n\n**Dropped or merged:** the three duplicate pre-staking entries became one, the two oversized-data entries one, the two execute-gas entries one, the two finalizer-trust entries one, and the stale pending-owner check was closed by the RPC read.\n\n**Limits of this review:** no fork tests were rerun, so the specialist's routed-trade assertions for the two-faced quote and the dust boundary were checked by code and arithmetic, not executed. No release-blocking trade-freeze, custody, ceiling, or alternative-upgrade defect was found in the r3 core, and a clean pass is not proof of absence.","treeHash":null,"usage":{"cachedInputTokens":1593344,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":35865,"runtime":"claude","turns":37,"wallClockMs":620227}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2a4ad9be53991100","findings":[{"citation":"resolved","description":"The 4096-byte hookData limit is enforced before the `module==address(0)` early return. In phase one no module is selected, yet any main-pool swap carrying more than 4096 bytes of hookData now reverts in beforeSwap with HookDataTooLarge. The live FeePrivacyHook ignores hookData entirely, so integrators that attach large opaque data (some aggregators forward arbitrary bytes) will see swaps start failing immediately at finalization, not only after a program is selected. The brief allows rejecting an oversized caller payload, so this is not a freeze (the same caller succeeds with <=4096 bytes and nothing persists), but it is an unannounced change to the no-module path and should be documented or scoped to the module path.","line":69,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"State: activeModule == address(0) (phase-one default). Input: route any swap on the main pool with hookData = new bytes(4097). Expected (live hook, and per 'no program selected'): swap executes at the default 2.3% plan. Actual: ProtectedHook._beforeSwapExtension -> TradeExtensionHost.quote reverts HookDataTooLarge and the whole swap reverts. Minimal fix if the no-module path should stay permissive: move the length check after `if(module==address(0)) return(module,packed);` (receipts and program quotes are unaffected because record() only runs when a module is set).","severity":"low","snippet":"        if(data.length>4096) revert HookDataTooLarge();","title":"Oversized hookData is rejected even when no trade program is active (behavioral change from the live hook)"},{"citation":"resolved","description":"Anyone may deliver a receipt with any gasLimit in [10_000, 5_000_000]. The host only requires that onTrade returns its selector. A program whose onTrade catches internal failures (try/catch, low-level calls) and still returns the selector will have its receipt permanently consumed by a griefer who supplies just enough gas for the wrapper but not for the business action; the original receipt cannot be re-issued. This matches the documented boundary ('a receipt is not proof of paid work'), but the consequence is one-shot and irreversible per receipt, so program authors must revert (not swallow) on failure. No user funds or core accounting are affected.","line":104,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"State: a module M whose onTrade does `(bool ok,)=target.call{gas:gasleft()-5000}(...); return onTrade.selector;` (swallowing failure). Any receipt R for M. Input: attacker calls host.execute(R, data, 10_000). Expected by a program author: delivery either completes the work or leaves R pending. Actual: inner call runs out of gas, onTrade still returns the selector, host deletes pending[R.id] and emits TradeExecuted; R can never be executed again. Mitigation: document that onTrade must revert on any failed step, and/or let record() carry a minimum gas the module declares (e.g. via a view on the module) that execute() enforces as a floor for gasLimit.","severity":"low","snippet":"    function execute(Trade calldata trade,bytes calldata hookData,uint256 gasLimit) external {","title":"Permissionless execute() lets any third party choose the program's gas budget, which can consume a receipt with degraded delivery"},{"citation":"resolved","description":"FeeNftStakingHook._accrue delegates to FeeLpRewardsHook._accrue when block.timestamp < nftStakingStartsAt. That legacy path hard-codes project = gross/50 and computes platform = totalFee - project, which underflows (checked arithmetic -> revert) whenever the per-trade plan rate is below 20_000 pips, e.g. a zero-rate program plan. On the pinned chain state nftStaking.startsAt() = 1791672095 (verified via RPC) is already in the past at block 26197496, so the branch is unreachable for the sealed core; it is reported only so the retained historical code is not mistaken for a supported path.","line":161,"path":"candidates/phase1/contract/src/legacy/privacy/FeeLpRewardsHook.sol","reproduction":"Hypothetical state only: block.timestamp < nftStakingStartsAt and a module whose quote returns [0,0,0,0,0,0] (sum 0, valid plan). Any buy: _settleFee computes fee=0, calls _accrue(gross,0) -> FeeLpRewardsHook._accrue: project=gross/50 > 0, `totalFee - project` reverts with arithmetic underflow, freezing trades for the window. Not reachable on mainnet after 1791672095; no change required beyond documentation.","severity":"info","snippet":"        uint256 platform = totalFee - project;","title":"Pre-staking accrual branch underflows for any program plan below 2% (dead on mainnet; staking already started)"},{"citation":"resolved","description":"r3 correctly pins the canonical proxies/admins (verified on mainnet: ERC-1967 admin slots of the token and hook proxies hold 0xfB4b...807a57 and 0x1f22...29bc, implementation slots hold 0xDdB5...6F81 / 0xD770...2f2c, and both ProxyAdmins and the token are owned by 0xBb14...F1C5). The implementation side remains a trust assumption: any contract returning the expected protectionKind/version/maximumTotalTaxPips is accepted and its codehash frozen as the irreversible final core. The operator must independently verify that the deployed implementations' runtime hashes equal the reviewed artifacts (manifest runtimeTemplateKeccak with immutables applied) before transferring the ProxyAdmins. This is a deployment-procedure control, not a code defect.","line":43,"path":"candidates/phase1/contract/src/PhaseOneFinalizer.sol","reproduction":"Input: deploy any contract X with protectionKind()==keccak256(\"CLAUS_TOKEN\"), protectionVersion()==1, maximumTotalTaxPips()==150000; call new PhaseOneFinalizer(owner, token, hook, TA, HA, X, hookImpl). Expected (operator intent): only the reviewed ProtectedToken is accepted. Actual: constructor succeeds and finalize() would irreversibly install X. Fix (procedural or code): compare tokenImpl_.codehash / hookImpl_.codehash against constants embedded in the finalizer, or verify hashes off-chain before transferring admin ownership.","severity":"info","snippet":"        if (IProtectedCore(tokenImpl_).protectionKind() != keccak256(\"CLAUS_TOKEN\")","title":"Finalizer trusts self-reported protectionKind and pins whatever implementation bytecode it is given"},{"citation":"resolved","description":"After finalization the remaining owner powers are: setModule (any contract; its quote may raise the aggregate hook fee from 2.3% to 14.9% for the next block with no delay, and may discriminate by router address) and updateMetadata (name/symbol/URI). Verified bounds: plans above 149_000 total or any element above 149_000 are ignored (default 2.3%), module code runs only under an 80k-gas STATICCALL with exact 192-byte output, quote gas starvation is rejected rather than used to select a cheaper plan (scratch test: at the minimum passing outer gas the module still observes ~79k), receipts bind the original module/data hash, and execute() cannot run while the PoolManager is unlocked. These are intentional powers per the brief and are listed for the record, not as defects.","line":54,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"Owner calls host.setModule(M) where M.quote returns [0,0,0,0,0,149000]. Next buy of 1 ETH: hook fee = 0.149 ETH minted as a claim to M; _checkTotalTax passes with max protocol fee (0.1%). No timelock or user notice exists; users rely on router slippage. Metadata: owner calls T.updateMetadata(\"X\",\"X\",\"\") and the on-chain name/symbol change immediately. No fix requested; document and consider a voluntary delay on module changes.","severity":"info","snippet":"    function setModule(address module) external {","title":"Token owner retains immediate, non-timelocked control of the fee plan up to 14.9% and of token metadata (trust assumptions)"}],"hash":"778399d45aefd60d06b27a984f0a5429a5c32cf90f0cff4fa84fd6e404ea003e","nodeId":"0e1dfa72-16ec-4381-b0fa-61bba85ab5fa","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the result: no release-blocking defects, two low findings and three informational items, each with a concrete trigger. No repository files were changed.\n\n**Outcome.** I found no trade-freezing, custody, ceiling or alternative-upgrade defect in revision r3. The r3 fixes were verified rather than assumed:\n\n- **Finalizer pins** hold against live state. Mainnet reads show the token and hook proxies' ERC-1967 admin slots are the pinned ProxyAdmins, the implementation slots match the manifest baselines, and the token and both ProxyAdmins are owned by the same owner address the constructor requires.\n- **Dependency pins** match mainnet. The runtime code hashes of the processor, remainder processor, NFT staking vault and LP vault equal the constants in the hook constructor.\n- **Token storage** is safe to swap. The live token implementation bytecode contains the exact metadata slot constant plus the OpenZeppelin ERC20, Ownable and Ownable2Step namespaced slots the new token reads, so balances, allowances, ownership and metadata survive the upgrade.\n- **Gas starvation cannot pick a cheaper plan.** A scratch probe shows the quote reverts below the threshold and, at the smallest passing budget, the program still observes about 79k of its 80k gas.\n- **Fee arithmetic** is consistent across all four modes. Floor-of-sum bounds prove the six allocations never exceed the single quoted rate, the exact-output net fee never exceeds the gross-basis check, and the 14.9% plan cap plus 0.1% protocol fee stays inside the 15% check except for documented dust.\n- **Receipts** bind module, data hash and amounts, cannot execute while the pool manager is unlocked, cannot re-enter, and failed delivery restores the receipt.\n\n**Findings recorded.**\n\n- Low: hookData over 4096 bytes is rejected even with no program selected, a behavior change from the live hook on the no-module path.\n- Low: anyone can deliver a receipt with the minimum gas budget, so programs that swallow inner failures lose the receipt permanently.\n- Info: a pre-staking accrual branch would underflow for plans under 2%, but staking already started on the pinned chain, so it is unreachable.\n- Info: the finalizer accepts any implementation that self-reports the right kind, so operators must verify implementation hashes before transferring the admins.\n- Info: owner powers over fee programs and metadata are immediate and untimelocked, listed as trust assumptions.\n\n**Limits of review.** The full candidate project cannot compile on this machine because the 1.5 GB memory cgroup kills solc, so I could not run the 23 fork cases or independently confirm the 24,261-byte hook size. Unit reproductions ran in a reduced scratch project outside the repository. Nothing in this review constitutes approval of the earlier r1 or r2 reports.","treeHash":null,"usage":{"cachedInputTokens":2132806,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":55930,"runtime":"claude","turns":48,"wallClockMs":1258341}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a0f4c3f900dedd4a","findings":[{"citation":"resolved","description":"finalize() checks ICurrentTokenOwner(token).owner()==owner on line 52 while the OLD implementation is still active, performs both upgrades, and then verifies only protectionVersion/protectionKind/maximumTotalTaxPips of the NEW implementations (lines 56-58) before renouncing both ProxyAdmins. Nothing re-reads owner(), totalSupply() or name() through the new implementation. Those three selectors are pure constants in ProtectedToken, so they prove the implementation's identity claim, not that its ERC-7201 Ownable/ERC20/metadata namespaces coincide with the proxy's existing storage. Because renounceOwnership follows in the same transaction, a namespace mismatch would be permanent: the token owner (and therefore TradeExtensionHost.setModule and updateMetadata) would be gone, or balances would read as zero. For the actual r3 ProtectedToken I verified on mainnet that the proxy's Ownable slot 0x9016d0..9300 holds 0xBb145c.., the ERC20 namespace totalSupply slot holds the live supply, and the metadata slot 0xaf7f..b400 holds 'Claus'/'CLAUS', so the deployed candidate is NOT affected; the gap is that the finalizer relies on the operator passing the right implementation rather than checking state continuity itself. Note the base contract claus-launch/BurnableToken.sol of the current OpenTransferToken was not supplied in candidates/phase1/references, so the metadata/owner namespaces could only be checked against live storage, not source. Required fix: after the two upgradeAndCall calls, add `if (ICurrentTokenOwner(token).owner()!=owner || IERC20(token).totalSupply()!=supplyBefore || keccak256(bytes(IERC20Metadata(token).name()))!=nameHashBefore) revert InvalidFinalization();` with the before-values captured at the top of finalize(); this costs a few hundred bytes and keeps the atomic/irreversible design.","line":56,"path":"candidates/phase1/contract/src/PhaseOneFinalizer.sol","reproduction":"State: finalizer constructed with a tokenImpl_ that returns protectionKind()==keccak256('CLAUS_TOKEN'), protectionVersion()==1, maximumTotalTaxPips()==150000 but reads owner from a different namespace (e.g. a copy of ProtectedToken compiled against a non-ERC7201 Ownable, owner at slot 0). Both ProxyAdmins transferred to the finalizer. Input: owner calls finalize(). Expected: revert, because the live owner 0xBb145c.. is no longer the token owner after the upgrade. Actual: finalize() succeeds, both ProxyAdmins are renounced, token.owner() returns address(0) forever, setModule/updateMetadata are permanently unusable. With the real r3 ProtectedToken artifact the post-upgrade owner is preserved (fork test testAtomicSealPreservesCustodyAndRemovesBothUpgradeOwners passed on my independent chain-31337 fork at block 26197648), so this is a hardening gap, not a live defect.","severity":"low","snippet":"        if(IProtectedCore(token).protectionVersion()!=1 || IProtectedCore(hook).protectionVersion()!=1","title":"Finalizer verifies ownership and storage continuity only before the irreversible upgrade, not after it"},{"citation":"resolved","description":"quoteTradeFee and the swap-time _beforeSwapExtension both STATICCALL the program with the same arguments, but the program can observe context that differs between the two, most simply IPoolManager.isUnlocked() (false in an eth_call quote, true inside PoolManager.swap), block.number parity, or the sender address. The aggregate ceiling, fallback, and non-blocking guarantees all hold (I verified the trade still completes, the module receives exactly 0.005 ether*149000/1e6 and all four trade modes succeed), so this cannot exceed 15% or freeze trading. It does mean that the view getter the hook exposes as the per-trade quote is only indicative, as REVIEW-FOLLOWUP already states for address-dependent quotes; the owner's stated acceptance that 'quotes are indicative; user slippage applies' covers it. Recorded so that integrators do not treat quoteTradeFee as a commitment: the only binding bound is maximumTotalTaxPips. No core change is required; if a tighter guarantee is wanted, the host could snapshot the last view quote per (module, data hash) and reject a swap-time plan above it, but that would be a design change the owner has not requested.","line":83,"path":"candidates/phase1/contract/src/ProtectedHook.sol","reproduction":"Program: `function quote(...) view returns(uint256[6] memory a){ if (MANAGER.isUnlocked()) a=[0,0,0,0,0,149000]; else a=[0,0,0,0,0,0]; }` set via setModule by the token owner, protocol fee 1000|1000<<12. Call live.quoteTradeFee(ROUTER,true,-1e18,\"\") -> totalPips 0. Then ALICE buys 0.005 ether exact-input through the Universal Router -> module claim balance grows by 0.005 ether*149000/1e6 = 745000000000000 wei, i.e. 14.9% was charged. Scratch test testTwoFacedQuoteIsBoundedAndNotBlocking (fork, chain 31337, block 26197648) passed with exactly these assertions.","severity":"info","snippet":"    function quoteTradeFee(address sender,bool buy,int256 amountSpecified,bytes calldata data) public view returns(address module,uint24 totalPips) {","title":"View quotes are not binding: a program can answer 0% to quoteTradeFee and charge 14.9% inside the swap (verified)"},{"citation":"resolved","description":"r3 correctly rejects >4096-byte data and insufficient caller gas (verified: InsufficientQuoteGas at 100k gas, HookDataTooLarge at 4097 bytes, boundary 4096-byte trade records the module plan). However the fixed 80,000-gas STATICCALL budget is shared between the program's logic and whatever work the caller's data induces. A program whose quote cost scales with data (parsing, per-byte loops, external lookups keyed by data) can be made to run out of gas by a swapper who supplies up to 4096 bytes of junk; the host then applies DEFAULT_PLAN (2.3%, module allocation 0) and still records a receipt with moduleFeeEth=0. When the program's plan is more expensive than 2.3% this is a swapper-side fee reduction, and when it is cheaper it is a swapper-side overcharge within the ceiling. This is the retained design boundary (program failures fall back safely) rather than a host defect, and REVIEW-FOLLOWUP documents that programs must inspect actual fee fields. It is recorded so program authors are told explicitly: a quote must be O(1) in data length and well under 80k gas for any 4096-byte input, and must not treat a receipt as proof the quoted plan was applied. No core change required; an optional hardening is to pass the program only keccak256(data) plus the first 32 bytes, which caps data-dependent work in the host itself.","line":78,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"Program: `function quote(...,bytes calldata data) view returns(uint256[6] memory a){ uint256 s; for(uint i;i<data.length;i++) s+=uint8(data[i]); a=[0,0,0,0,0,149000]; }` installed by the owner. Swap with empty hookData -> plan 14.9%, module receives 14.9%. Same swap with 4096 bytes of hookData -> the loop costs roughly 4096*~60 gas > 80,000, staticcall fails, host returns DEFAULT_PLAN, trade pays 2.3% to the default recipients and the receipt carries moduleFeeEth=0. Expected under the program's intent: 14.9%; actual: 2.3%. The same mechanism with a program cheaper than 2.3% raises the swapper's fee to 2.3%, still inside the 15% ceiling.","severity":"info","snippet":"            ok := staticcall(80000,module,add(input,32),mload(input),allocations,192)","title":"Caller-chosen hookData within the 4096-byte limit can still push a gas-heavy program quote into the cheaper default plan"},{"citation":"resolved","description":"When block.timestamp < nftStakingStartsAt, _accrue delegates to FeeLpRewardsHook._accrue, which computes project = gross/50 (fixed 2%) and platform = totalFee - project with checked arithmetic. Under r3 totalFee is the program-selected rate, which may be anywhere in [0, 14.9%]; any rate below 2% makes platform underflow and reverts every swap, i.e. a trade freeze. The hook pins ClausNftStaking 0x4e59..CAD4 whose startsAt() I read on mainnet as 1791672095, earlier than the current chain time (block 26197648), so on the target chain state this branch can never execute and the live default/new-plan paths (lines 47-79) are the only reachable ones. Recorded as a latent hazard: if the same source were ever re-pinned to a vault with a future start, a zero-rate or sub-2% plan would freeze trading until that start. Fix (optional, keeps behaviour): drop the pre-start branch entirely or make it use the plan-based arithmetic too.","line":44,"path":"candidates/phase1/contract/src/legacy/privacy/FeeNftStakingHook.sol","reproduction":"State: vm.warp to any timestamp < 1791672095 (not possible on the real chain), module plan [0,0,0,0,0,0] (rate 0) or any plan with sum < 20000. Input: any buy or sell. Expected: trade settles with the plan; actual: panic 0x11 in FeeLpRewardsHook._accrue line 161 (`platform = totalFee - project`) and the swap reverts. Not reachable at or after the recorded fork block because startsAt is already past.","severity":"info","snippet":"            super._accrue(gross, totalFee);","title":"Pre-staking accrual branch underflows for any program plan below 2%; unreachable because the pinned staking start is already in the past"}],"hash":"e8d1ce6a613fc0573c27838217500060920b9f17448cc60a05e6f97f7877788e","nodeId":"2c98d518-821c-4d8f-be1f-4b6729702293","outcome":"completed","summary":"The review is complete and the findings file is written. Here is the recap.\n\n**Outcome: no release-blocking defect substantiated in r3.** I found no path by which an owner, a replaceable program, an unexpected caller, a malformed quote, or a composition can freeze transfers, buys, sells or LP exits, take custody of holder or vault assets, redirect already-accrued claims, or push the combined hook plus v4 protocol charge above 15%. The file `.imd-findings.json` holds one low and three informational findings, each with a concrete input.\n\n**What I verified**\n\n- Fee ceiling and allocation arithmetic. The six pips allocations are packed into one plan whose sum is the single rate, capped at 149,000 before packing. I proved that the sum of the floored per-allocation amounts never exceeds the floored or net-derived total fee, so the project remainder cannot underflow in the live accrual path, including exact-output trades and all-to-one-recipient plans. LP fee is overridden to zero on every swap, so nothing stacks on the plan.\n- Transient snapshots. Plan and module are started in beforeSwap and cleared in afterSwap. Reverted swaps roll back the tstores. A two-swap router call on the same pool in one unlock produced two receipts and correct module fees with no contamination.\n- Host hardening claims. Oversized data and insufficient gas revert instead of falling back. The 80k STATICCALL always receives its full budget once the pre-check passes. Return bombs, write attempts, reverts and OOG all fall back to the default plan. Receipts are hook-only, bound to the original module and data, executed at most once, and only while the PoolManager is locked.\n- Finalizer and pins. Live reads confirmed the token's Ownable, ERC-20 and metadata namespaces match ProtectedToken, both ProxyAdmins share one codehash and are owned by the stated owner, the hook and token implementations match the supplied references, and the pinned staking vault and schedule slot match live state.\n- Runtime sizes: the hook is 24,261 bytes, 315 bytes under the EIP-170 limit.\n\n**Independent execution**\n\n| Suite | Result |\n| --- | --- |\n| 19 core fork cases | pass |\n| 4 pool-program fork cases | pass |\n| 4 scratch cases I wrote (all-to-module plan, all-to-platform dust, two-faced quote, multi-swap) | pass |\n\nThese ran on an isolated chain-31337 anvil fork of mainnet at block 26197648, not the manifest block 26197496, because the public archive node had no state at that block.\n\n**Findings written**\n\n1. Low. The finalizer re-checks only the three constant selectors after the irreversible upgrade, so owner and balance continuity are assumed rather than verified. The real r3 artifact is not affected.\n2. Info. A program can answer 0% to the view quote and charge 14.9% inside the swap. Bounded by the ceiling and consistent with the owner's documented \"quotes are indicative\" stance.\n3. Info. Data-dependent quote cost within the 4096-byte limit can push a program into the default plan. This is the retained fallback boundary, recorded for program authors.\n4. Info. The pre-staking accrual branch underflows for any plan below 2%, but it is unreachable because the pinned vault's start time is already past.\n\n**Limits of this review.** The base contract of the current token implementation was not in the reference bundle, so its namespaces were checked against live storage rather than source. Slither was not available. The finalizer accepts any implementation that self-reports the protection kind, so verifying the deployed implementation bytecode before calling finalize remains an operational step for the owner and reviewers.","treeHash":null,"usage":{"cachedInputTokens":3042892,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":58747,"runtime":"claude","turns":53,"wallClockMs":1082880}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"723b11f958c65250","findings":[{"citation":"resolved","description":"The buy-side ceiling compares floor(gross*14.9%) plus the v4 protocol charge (which v4 rounds UP per step on the post-hook-fee pool input) against floor(gross*15%). Because both the hook fee and the ceiling floor while the protocol fee ceils, small trades fail the check even though no party overcharges. This is the boundary REVIEW-FOLLOWUP.md calls intentional; this finding only quantifies it so integrators can set minimums. Modelled on v4 SwapMath (last-step feeAmount = remaining - floor(remaining*0.999)): with a 149,000 pip plan and a 1,000 pip zeroForOne protocol fee, 1,130 distinct gross amounts between 1 and 5,933 wei revert TotalTaxExceeded; with the default 23,000 pip plan only gross 1-6 wei revert. On the sell side (line 335) any token input of 1-6 atoms whose protocol fee rounds to one atom reverts because floor(input*0.15) is 0 or protocol equals input. No persistent state is touched, later valid trades are unaffected, and no amount above 5,933 wei fails. The arithmetic was fuzzed locally (six-way split never underflows for exact-in and exact-out, allowedPips never underflows once protocol <= floor(input*0.15), total fee never exceeds the quoted rate). No fix is required if the strict ceiling is the requested behaviour; if dust rejection is unwanted, the only sound alternative is to compute the ceiling as gross*150_000 vs (fee+protocol)*1_000_000 in integer cross-multiplication and accept that one-atom protocol rounding then slightly exceeds 15% on sub-20,000-wei trades, which the owner has explicitly declined.","line":330,"path":"candidates/phase1/contract/src/legacy/FeeEfficientHook.sol","reproduction":"State: pool protocol fee set to 1000|(1000<<12) (max), active module returning allocations summing to 149,000 pips. Input: exact-input buy (zeroForOne, amountSpecified = -5933). Expected under a tolerant reading: trade completes with 884 wei hook fee + 6 wei protocol fee. Actual: fee=884, poolIn=5049, protocol=ceil(5049*0.001)=6, 890 > floor(5933*0.15)=889, afterSwap reverts TotalTaxExceeded. Same input with amountSpecified=-5934 succeeds (884+6 <= 890). Default plan: amountSpecified=-6 reverts (0+1 > 0), -7 succeeds.","severity":"info","snippet":"            if(fee+protocol>gross*150_000/1_000_000) revert TotalTaxExceeded();","title":"Measured dust boundary of the strict 15% ceiling (documented, intentional): exact-input buys up to 5,933 wei revert at the maximum plan with a maximum protocol fee"},{"citation":"resolved","description":"The division-by-zero guard `protocol==input` also fires when both are zero. A oneForZero swap whose price limit is reached before any in-range liquidity (v4 returns a zero BalanceDelta rather than reverting) therefore reaches afterSwap with input=0, protocol=0 and reverts under the ceiling error. The buy side rejects the same situation as PartialNativeFill. Impact is nil: no fee is charged, nothing persists, and the main pool currently has full-range liquidity so the state is not reachable on mainnet without the limit being set inside an empty range. Reported for completeness of the boundary review; a fix would be `if(input==0) revert PartialNativeFill();` ahead of the ceiling check, or simply leaving it since the revert is harmless.","line":335,"path":"candidates/phase1/contract/src/legacy/FeeEfficientHook.sol","reproduction":"State: no liquidity between the current sqrt price and the caller's sqrtPriceLimitX96 (e.g. an isolated local pool with a single position entirely below the current tick). Input: swap(zeroForOne=false, amountSpecified=-1e18, sqrtPriceLimitX96 = current price + 1 tick). Expected: a no-fill revert or zero-delta success. Actual: afterSwap computes input=0, protocol=0, the `protocol==input` branch reverts TotalTaxExceeded, misreporting a tax violation.","severity":"info","snippet":"            if(protocol>input*150_000/1_000_000 || protocol==input) revert TotalTaxExceeded();","title":"Sell that consumes zero input reverts with TotalTaxExceeded instead of a descriptive no-fill error"}],"hash":"fc3ab96dcb2df12bb2be00e2415dc756b68c7d6b8f5191d0ac428b582af9b40e","nodeId":"21309b7d-1c3b-47d4-8f0b-799265182c97","outcome":"completed","summary":"The math review of revision r3 is complete and the findings file is written. No release-blocking numerical defect was found.\n\n**Outcome.** The findings file at the repository root holds two informational entries. Both quantify boundaries the owner already declared intentional, so neither is a defect requiring a revision. No critical, high or medium finding survived verification, so no proof test was attached.\n\n**What was verified in depth**\n\n- **Six-way plan arithmetic.** The host accepts a plan only when every allocation and their sum stay at or below 149,000 pips, and packs the sum plus one into the low 24 bits with no field overlap. A scratch fuzz over all six allocations, both exact-in and exact-out, showed the project-wallet remainder never underflows and the total fee never exceeds the quoted rate. The exact-out net fee is provably at least the sum of the per-destination floors.\n- **Ceiling check in all four modes.** The buy side compares the hook fee plus the actual accrued protocol fee to 15% of gross. The sell side maps the token-side protocol charge and the ETH-side hook fee onto one pre-fee basis. I re-derived that formula, confirmed it is conservative under price impact, and fuzzed that the allowed-pips expression cannot underflow once the preceding guard passes. Partial fills on exact-in buys and exact-out sells revert as designed; exact-in sells charge on the actual output.\n- **Fallbacks and snapshots.** Default plan, zero-rate plan, failed quotes, zero stakers, and the module-fee field all resolve consistently through the transient slots, which are cleared at the end of every swap and reverted with the frame on failure.\n- **Gas bounds.** The 25,000 gas margin before the 80,000 gas staticcall and the 63/64 check before execute both guarantee the full requested gas is forwarded.\n- **Timed buyback stream.** Release math has no division by zero and the stored available-ETH accounting stays consistent with the claim balance across the active and expired paths.\n\n**Measured boundaries reported as info**\n\n- At the maximum 14.9% plan with a maximum protocol fee, exact-input buys up to 5,933 wei revert. Under the default plan only 1 to 6 wei fail. Sells of 1 to 6 token atoms fail when the protocol fee rounds to one atom.\n- A sell that consumes zero input reverts under the ceiling error rather than a no-fill error. It is harmless and unreachable on the live pool.\n\n**Limits of this review**\n\n- The fork suite could not run here because the fork RPC environment variables are not set, so the nineteen core and four pool-program cases were read, not executed. The candidate tree does compile under its own profile.\n- The token's metadata slot could not be traced to the live implementation because the supplied reference imports a base contract that is not in the bundle. The fork case that asserts name, symbol and URI survive finalization is the only evidence for that slot.\n- Scope was the fee and boundary arithmetic. Custody, finalizer sequencing and codehash pinning were read for context only and were not independently exercised.","treeHash":null,"usage":{"cachedInputTokens":1831006,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":47912,"runtime":"claude","turns":36,"wallClockMs":919329}}],"verification":[]}