{"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":"a9a19d30-475b-4c9a-9339-046c2d5004ee","kind":"audit","nodes":[{"acceptedSubmissionHash":"54a6ae1e7a02de91fd0280394e494e5bc1ceb6c21f5724dd68e2d959e2ad5e04","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":"99b84273f3cca41c06fadff5f39d58670163db788fa226bb3efffa98115f58f6","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":"dfba283b9e0d4842cd77d7fcaeeb59b16f26407d369928f4e60b5de89ae5baa1","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":"de9f8a5a1796a7db6900f06038bf2cf44eb7fd90d477dfc9da346c5a321058cc","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":"0848af20617a153c5e11bce446ca0425719d27749de946a9abfd52bd95bab1f9","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 r2 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 paid review remains a separate running job, not authority for this revision.\n\nCheck that no owner, new program, unexpected caller, malformed 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 Ethereum26197295 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","parentJobId":null,"planHash":"baf0455f54e7adc957f82912f56ea7a917763c980fb2e776ef05c83482b56d03","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"a9a19d30-475b-4c9a-9339-046c2d5004ee","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":"52642","feedbackHash":"5d82a865295251f222ddd803defa2cff887bcf25b51f2a4f9397685396147301","nodeKey":"audit_economics","submissionHash":"54a6ae1e7a02de91fd0280394e494e5bc1ceb6c21f5724dd68e2d959e2ad5e04","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52210","feedbackHash":"63c9361e26ede8d76d48fa553e5c515a609e21da024524c6ae2f2b7aac64b7f4","nodeKey":"audit_flow","submissionHash":"99b84273f3cca41c06fadff5f39d58670163db788fa226bb3efffa98115f58f6","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52121","feedbackHash":"8f1c05f224b943864c1671d8c9144053fc86dde95a62cf9f54c3fdf670a02a57","nodeKey":"audit_judge","submissionHash":"dfba283b9e0d4842cd77d7fcaeeb59b16f26407d369928f4e60b5de89ae5baa1","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51886","feedbackHash":"571124208753a5450110d11b07910923946bae1f9a2ea63ec55841d409661c9a","nodeKey":"audit_math","submissionHash":"de9f8a5a1796a7db6900f06038bf2cf44eb7fd90d477dfc9da346c5a321058cc","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52192","feedbackHash":"edd9e767e6c4302e1313c0bcfbe8897ea387a3085864f8c79a4dc114d1111036","nodeKey":"audit_permissions","submissionHash":"0848af20617a153c5e11bce446ca0425719d27749de946a9abfd52bd95bab1f9","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"2bc97c80d0b943fa209298ec3149538567886755ccd62f691ba4b6c1d11c0dd0","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a7b8747ac077deb9","findings":[{"citation":"resolved","description":"TradeExtensionHost.quote() returns (address(0), DEFAULT_PLAN) whenever the swap's hookData exceeds 4096 bytes, without consulting the selected program at all. The hook then charges the 2.3% default, mints nothing to the program (pips(5)=0, module==0 so _accrue skips the module mint) and records no receipt (ProtectedHook._afterSwapExtension only calls record() when module!=0). hookData is chosen freely by the swapper (IV4Router ExactInput/ExactOutputSingleParams.hookData), so every rate above the default that an owner-selected program sets is voluntary: a trader pays ~65k gas of calldata to opt out, and the program also loses the trade receipt it would otherwise be delivered. This defeats the owner's stated requirement that replaceable programs price trades up to 14.9%, while a program charging less than 2.3% is never bypassed (nobody pads to pay more). Verified on a local fork of Ethereum block 26197295 after finalization: with a program returning [0,0,0,0,0,149000], quoteTradeFee(ROUTER,true,-0.005e18,\"\") = 149000 and a 0.005 ETH buy mints 0.000745 ETH of claims to the program and records receipt #1; the identical buy with 4097 bytes of hookData is quoted (address(0), 23000), mints 0 to the program and records no receipt. Fix: do not treat oversized input as 'no program'. Keep the gas bound by quoting the program with the data truncated to the first 4096 bytes or with empty data (e.g. `bytes calldata quoted = data.length>4096 ? data[:0] : data;`), keep module set so the receipt is recorded, and record keccak256 of the data actually quoted (ProtectedHook._afterSwapExtension must hash the same truncated data) so execute() still binds to the original quote input. Alternatively reject oversized hookData for the main pool explicitly in the hook if the owner prefers (that would be a trade rejection, so truncation is the design-preserving option).","line":67,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {TradeExtensionHost, IClausTradeModule} from \"src/TradeExtensionHost.sol\";\nimport {ClausFeeBudget} from \"src/ClausFeeBudget.sol\";\n\n/// Minimal stand-ins etched at the fixed mainnet addresses the host hardcodes.\ncontract MockPoolManager {\n    function extsload(bytes32) external pure returns (bytes32) { return bytes32(uint256(1 << 96)); }\n    function isUnlocked() external pure returns (bool) { return false; }\n}\ncontract MockToken { address public owner; constructor(address o) { owner = o; } }\n\n/// A legitimate program charging the maximum 14.9% entirely to itself, for any data.\ncontract MaxFeeModule {\n    function quote(address, bool, int256, uint160, uint128, bytes calldata) external pure returns (uint256[6] memory p) {\n        p[5] = 149_000;\n    }\n    function onTrade(TradeExtensionHost.Trade calldata, bytes calldata) external pure returns (bytes4) {\n        return IClausTradeModule.onTrade.selector;\n    }\n}\n\ncontract OversizedHookDataBypassTest is Test {\n    address constant OWNER = address(0xBb145cA83272c3806D4dDc75cA1D5514789cF1C5);\n    TradeExtensionHost host;\n    MaxFeeModule module;\n\n    function setUp() public {\n        host = new TradeExtensionHost();\n        vm.etch(address(host.MANAGER()), type(MockPoolManager).runtimeCode);\n        vm.etch(host.TOKEN(), address(new MockToken(OWNER)).code);\n        vm.store(host.TOKEN(), bytes32(0), bytes32(uint256(uint160(OWNER))));\n        module = new MaxFeeModule();\n        vm.prank(OWNER);\n        host.setModule(address(module));\n    }\n\n    /// The selected program must price every main-pool trade. A trader who appends more than\n    /// 4096 bytes of hookData currently gets the 2.3% default and no receipt; the program is\n    /// never consulted and earns nothing, so any rate above the default is voluntary.\n    function testOversizedHookDataStillPricedByActiveProgram() public view {\n        bytes memory small = new bytes(4096);\n        (address m1, uint256 packed1) = host.quote(address(0xBEEF), true, -1 ether, small);\n        assertEq(m1, address(module));\n        assertEq((packed1 & 0xffffff) - 1, 149_000, \"program rate applies to bounded data\");\n\n        bytes memory oversized = new bytes(4097);\n        (address m2, uint256 packed2) = host.quote(address(0xBEEF), true, -1 ether, oversized);\n        // Expected: the active program still sets the rate (e.g. quoted on empty/truncated data)\n        // and remains bound to the receipt. Actual: module == address(0), rate == 23000 default.\n        assertEq(m2, address(module), \"oversized hookData must not detach the active program\");\n        assertEq((packed2 & 0xffffff) - 1, 149_000, \"oversized hookData must not reset the fee to the default\");\n        assertTrue(packed2 != ClausFeeBudget.DEFAULT_PLAN);\n    }\n}","reproduction":"Non-fork: deploy TradeExtensionHost, etch a minimal PoolManager (extsload returns nonzero price, isUnlocked=false) at 0x000000000004444c5dc75cB358380D2e3dE08A90 and an owner() stub at 0x1b54E762aa34CF6E28E9C082F2848e28E45DA6b8, setModule(module) from owner 0xBb145cA83272c3806D4dDc75cA1D5514789cF1C5 where module.quote returns [0,0,0,0,0,149000]. host.quote(0xBEEF,true,-1e18,new bytes(4096)) -> (module, rate 149000). host.quote(0xBEEF,true,-1e18,new bytes(4097)) -> expected (module, 149000); actual (address(0), 23000 default). Fork (chain 31337 @ 26197295, finalized): swap 0.005 ETH buy via router 0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85 with 4097-byte hookData -> program claim balance +0, nextReceipt unchanged; same swap with empty hookData -> +745000000000000 wei claims and one receipt.","severity":"medium","snippet":"        if(data.length>4096) return(address(0),packed);","title":"Any trader can detach the active trade program and drop its fee to the 2.3% default by padding hookData past 4096 bytes"},{"citation":"resolved","description":"_checkTotalTax compares the hook fee plus the protocol fee actually accrued during the swap against 15% of the gross amount. Uniswap v4 computes the protocol fee with rounding that charges at least 1 wei on any nonzero input (SwapMath: amountRemainingLessFee = amount*(1e6-fee)/1e6, feeAmount = amount - amountIn). For inputs below 7 wei the 1 wei protocol fee alone exceeds 15% of gross (gross*150_000/1_000_000 == 0 for gross<7), and in the oneForZero branch `protocol==input` also triggers for a 1 wei token sell, so the hook reverts the whole swap with TotalTaxExceeded (wrapped by the PoolManager as Hook.Wrap__FailedHookCall 0x90bfb865 / 0xe8f1a8aa). Without the protocol fee the same swaps succeed. Economic impact is nil (sub-7-wei trades of an 18-decimal token and of ETH), but it is a trade rejection path introduced by the ceiling check that the design states must never reject trading, and it is the only input class where the ceiling check itself fails. Fix: tolerate the one-wei rounding floor, e.g. `if(fee+protocol>gross*150_000/1_000_000 + 1)` only when protocol<=1, or skip the protocol component when `protocol<=1`, or document the minimum trade size.","line":330,"path":"candidates/phase1/contract/src/legacy/FeeEfficientHook.sol","reproduction":"Fork chain 31337 @ Ethereum 26197295, finalized candidate, no program selected. (1) ExactInputSingle buy with amountIn=1 wei and protocolFee=0 -> succeeds. (2) vm.prank(PM.protocolFeeController()); PM.setProtocolFee(main, 1000|(1000<<12)); ExactInputSingle buy with amountIn in {1..6} wei -> reverts with 0x90bfb865 (Hook call failed) wrapping 0xe8f1a8aa TotalTaxExceeded; amountIn=7 wei succeeds. Same for exact-input sells of 1..6 wei of CLAUS. Expected: ceiling check never rejects a trade whose hook fee is within plan; actual: rejected.","severity":"low","snippet":"            if(fee+protocol>gross*150_000/1_000_000) revert TotalTaxExceeded();","title":"With the v4 protocol fee set, main-pool swaps of 1 to 6 wei revert on the 15% ceiling check because the protocol fee rounds up"},{"citation":"resolved","description":"setModule takes effect immediately with no delay, announcement period or two-step. The owner (0xBb145cA83272c3806D4dDc75cA1D5514789cF1C5, the token's Ownable2Step owner) can therefore front-run a pending swap with a program that charges 14.9% (or that discriminates on the PoolManager caller address), and the user's only protection is their router slippage bound. This is within the documented ceiling and the owner's declared requirement for replaceable programs, so it is reported as a privileged-power trust assumption, not a bypass. If the owner wants users to be able to rely on quoteTradeFee, a short activation delay (pendingModule + effective timestamp) in the host would close the window without changing the design.","line":52,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"Fork 31337 @ 26197295, finalized. quoteTradeFee(ROUTER,true,-0.005e18,'') = 23000. In the same block: vm.prank(OWNER); host.setModule(moduleCharging149000); then ALICE's exact-input buy of 0.005 ETH is charged 0.000745 ETH (14.9%) as the project tests themselves show (testReplaceableProgramFeeBudgetAndFourTradeModes). Expected by a user who quoted first: 2.3%; actual: 14.9%, bounded only by amountOutMin.","severity":"info","snippet":"    function setModule(address module) external {","title":"Trust assumption: token owner can swap the active trade program instantly, so the quoted rate can change to any value up to 14.9% between a user's quote and their swap"},{"citation":"resolved","description":"When the program's STATICCALL fails or returns an invalid plan, quote() returns (module, DEFAULT_PLAN) with module still set. ProtectedHook then charges 2.3%, mints nothing to the program (pips(5)==0) and still calls record(module, ...) so a receipt bound to that program exists and anyone may execute() it, delivering onTrade for a trade the program did not price. moduleFeeEth is 0 in the receipt so a careful program can detect this, but programs that treat every delivered receipt as a paid trade will mis-account. Not a custody or ceiling issue (no funds move); flagged because the README states a receipt attests the trade's 'original module/data/amounts', and here the module's plan was not used. Fix options: return (address(0), DEFAULT_PLAN) on fallback so no receipt is recorded, or carry a 'fallback' flag/plan hash in the Trade struct.","line":77,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"Fork 31337 @ 26197295, finalized. setModule(RevertingModule) (project test contract whose fallback reverts). swap(true,true,0.001 ether) succeeds at 23000 pips and host.nextReceipt() increments; the TradeRecorded event names RevertingModule with moduleFeeEth 0. host.execute(trade,'',100000) then calls RevertingModule.onTrade. Expected per README: receipts attest trades priced by their program; actual: receipt for an unpriced trade.","severity":"info","snippet":"        if(!ok) return(module,packed);","title":"Receipts are recorded and deliverable for trades the program never priced (quote reverted, OOG, malformed or over-cap), naming that program with the default plan"},{"citation":"resolved","description":"If block.timestamp < nftStakingStartsAt, _accrue delegates to FeeLpRewardsHook._accrue which computes project = gross/50 (2%) and platform = totalFee - project with checked arithmetic. Any plan whose total rate is below 2% (including the supported zero-fee plan) makes totalFee < project and the subtraction reverts, which would revert every swap on the main pool. On the pinned mainnet state this branch is dead: ClausNftStaking at 0x4e5916428452891E1deb59d162c20266ED08CAD4 reports startsAt = 1791672095 (2026-10-10), earlier than the fork block timestamp, so the ProtectedHook always takes the plan-driven branch. Reported so the dead branch is not revived (e.g. on a redeploy with a future vault) without removing the hard 2% floor; the minimal fix is to delete the legacy branch from the final core or to route it through ClausFeeBudget as well.","line":44,"path":"candidates/phase1/contract/src/legacy/privacy/FeeNftStakingHook.sol","reproduction":"Only reachable with a ClausNftStaking whose startsAt is in the future. State: nftStakingStartsAt > block.timestamp, active program returning [0,0,0,0,0,0]. Exact-input buy of 1 ETH: totalFee = 0, project = 0.02 ETH, `platform = totalFee - project` reverts with Panic(0x11) and the swap fails. Expected: zero-fee plan is accepted (README: 'Zero-fee plans are supported'); actual: revert. On mainnet at block 26197295 the condition is false, so the probe (all six single-destination 14.9% plans plus the zero plan, all four modes) succeeded.","severity":"info","snippet":"            super._accrue(gross, totalFee);","title":"Pre-staking-start legacy accrual path cannot handle plans below 2% (unreachable on mainnet because the staking vault started 2026-10-10)"},{"citation":"resolved","description":"forge build with the candidate profile (0.8.26, cancun, via-IR, 200 runs) produces 24261 bytes of runtime for ProtectedHook (manifest runtimeBytes agrees). Immutables are already included in that size, so the deployed code is the same size. This leaves 315 bytes for any fix arising from this review (for example the hookData truncation in the medium finding, roughly 60-100 bytes). Not a defect; recorded because the finalizer pins the implementation codehash and a size overrun discovered at deployment would require a new review cycle.","line":27,"path":"candidates/phase1/contract/src/ProtectedHook.sol","reproduction":"cd candidates/phase1/contract && forge build; read out/ProtectedHook.sol/ProtectedHook.json deployedBytecode.object length: 24261 bytes (limit 24576).","severity":"info","snippet":"contract ProtectedHook is FeeNftStakingHook {","title":"ProtectedHook runtime is 24261 bytes, 315 bytes under the EIP-170 limit; any post-review change risks an undeployable core"},{"citation":"resolved","description":"Reviewed in depth: ProtectedToken, ProtectedHook, TradeExtensionHost, PhaseOneFinalizer, ClausFeeBudget, and the inherited chain HookBase -> FeeEfficientHook -> FeeNftHook -> FeePredictionHook -> FeePrivacyHook -> FeeBatchedPrivacyHook -> FeeLpRewardsHook -> FeeNftStakingHook, plus FeeProcessor, LpRemainderProcessor, ClausNftStaking, ProjectRevenue, PlatformRevenue, LiquidityReserve, MarketCapOracle, IMDWeather, ArenaTide, FeeMath, FeeCalls. Verified against Ethereum mainnet via public RPC at block ~26197428: codehash of 0x271f808c... equals the pinned 0x5edefdcf..., codehash of 0x51629bca... equals the pinned 0x63ff1264...; METADATA_SLOT holds 'Claus'/'CLAUS'; both ERC1967 admin slots point at the two ProxyAdmins and both are owned by 0xBb145cA8...; the candidate's timed-buyback slot keccak('claus.hook.timed-buyback.2026-10-15.v1') holds the live schedule (startsAt 1792023599, endsAt 1792066799) matching timedBuybackState() of the live wrapper, so the existing window is preserved with no new activation; the live hook implementation 0xD770a418... is a thin wrapper with previousImplementation() 0x3B1B1a7c... which the candidate replaces entirely. Bytecode of the two delegate targets and of the LP-rewards and NFT-staking vaults contains no DELEGATECALL, CALLCODE, SELFDESTRUCT or CREATE. Executed the project's 12 fork tests on a local anvil fork (chain 31337 at block 26197295): all pass. Additional fork probes: six single-destination 14.9% plans and the zero plan across buy/sell x exact-input/exact-output with the maximum 0.1% protocol fee all trade and never exceed the compounded 15% ceiling (the check's compound formula 1-(1-p)(1-f) was verified algebraically); batched router swaps keep independent transient plans; delivery reentrancy into execute() while unlocked is rejected; a program can only spend its own newly minted claims. Rounding: sum of floor(gross*p_i/1e6) <= floor(gross*rate/1e6) <= fee in all four modes, so walletEth never underflows. Limits: the LP-rewards vault (0xB2304d3D...) and ClausNFT/NftRewards were not source-reviewed beyond interfaces and selector/opcode scans; no Slither/Mythril; no long fuzz runs; the previousImplementation 0x3B1B1a7c... was not disassembled; fork state is 130 blocks older than head. No release-blocking (trade-freezing, custody, ceiling or alternative-upgrade) defect was found; the medium finding is an economic bypass of program fees, not a loss of user funds.","line":2,"path":"candidates/phase1/manifest.json","reproduction":"Not a defect. See the individual findings for failing inputs.","severity":"info","snippet":"  \"version\": \"r2\",","title":"Review scope, verification performed and limits"}],"hash":"0848af20617a153c5e11bce446ca0425719d27749de946a9abfd52bd95bab1f9","nodeId":"7270f1f8-c0fe-4dce-a067-90c168652175","outcome":"completed","summary":"Review complete. The findings file at the repository root holds 7 entries: 1 medium, 1 low and 5 informational. No release-blocking defect was found.\n\n**What I verified**\n\n- The candidate compiles under its own profile, and the project's 12 fork tests pass on a local anvil fork of Ethereum block 26197295 with chain id 31337.\n- Against live mainnet: both pinned processor codehashes match, the token metadata slot holds \"Claus\"/\"CLAUS\", both ERC1967 admin slots point at the two ProxyAdmins owned by the stated owner, and the candidate's timed-buyback slot holds the live schedule, so the existing window is preserved without new activation.\n- Bytecode of the two delegate targets and of the LP-rewards and NFT-staking vaults contains no DELEGATECALL, SELFDESTRUCT or CREATE.\n- My own fork probes: six single-destination 14.9% plans and the zero plan trade in all four modes with the maximum protocol fee; batched swaps keep independent transient plans; delivery reentrancy while the PoolManager is unlocked is rejected; a program can spend only its own newly minted claims. The 15% compound-ceiling arithmetic and the allocation rounding were checked algebraically.\n\n**Findings**\n\n- **Medium, with proof:** any trader can append more than 4096 bytes of hookData and the host returns no program and the 2.3% default. The selected program earns nothing and gets no receipt, so any program rate above the default is voluntary. Confirmed on the fork and with a self-contained failing test under `test/scratch/OversizedHookDataBypass.t.sol`. Suggested fix is to quote the program on truncated or empty data instead of detaching it.\n- **Low:** with the v4 protocol fee set, swaps of 1 to 6 wei revert on the ceiling check because the protocol fee rounds up to 1 wei. Economically irrelevant, but it is a trade rejection path.\n- **Info:** instant program switching is a trust assumption; receipts are recorded for trades whose program quote fell back; the pre-staking legacy accrual branch would revert on sub-2% plans but is unreachable on mainnet; the hook runtime is 315 bytes under the EIP-170 limit; and a scope-and-limits note. The LP-rewards vault and the old previousImplementation were only scanned at the bytecode level.\n\nThe local anvil fork was stopped. No repository files were modified other than the findings file and my scratch tests.","treeHash":null,"usage":{"cachedInputTokens":3164505,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":72479,"runtime":"claude","turns":52,"wallClockMs":1413169}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d73af718d55ba788","findings":[{"citation":"resolved","description":"TradeExtensionHost.quote() is the only source of the per-trade fee plan. When the caller-supplied hookData exceeds 4096 bytes it returns (address(0), DEFAULT_PLAN) before the module is ever consulted. hookData is chosen by the swapper (Universal Router / V4Router ExactInput/ExactOutput params carry it verbatim into IPoolManager.swap, and ProtectedHook._beforeSwapExtension forwards it unchanged). The fallback was meant for module failures (malformed, reverting, oversized RETURN, OOG plans), but this branch is triggered by the trader, not the program. Consequences while any program is active: (1) the program's quoted rate is replaced by the 2.3% default, so a program charging more than 2.3% (up to 14.9%) is bypassable by every trader for the cost of ~4 KB of calldata (~65k gas), and the program's own allocation (pips index 5) is never minted; (2) ClausFeeBudget.module() is address(0), so ProtectedHook._afterSwapExtension skips extensions.record(): no TradeRecorded receipt exists for the trade, which also lets a trader opt out of any program that reacts to receipts. Holders are not harmed and the 15% ceiling still holds; the loss is to the owner-selected program's revenue and the integrity of the receipt stream, which contradicts the stated design that the fee plan is set by the owner-selected program and only module misbehaviour falls back. Fix (minimal, keeps design): do not let user data select the plan. Either truncate what is forwarded to the module (pass data[:4096] or keccak256(data) to the module, and still quote it) or revert the swap for data.length>4096 (the trader chose the payload, so a revert does not freeze anyone else's trading). Keep the 4096 cap for execute()/dataHash. The same gas-dependent fallback at line 68 (gasleft()<QUOTE_GAS+25_000 returns the default plan) is also caller-influenced; see the separate info finding.","line":67,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {TradeExtensionHost} from \"src/TradeExtensionHost.sol\";\nimport {ClausFeeBudget} from \"src/ClausFeeBudget.sol\";\n\n/// @dev Stand-in for the CLAUS token proxy: only owner() is consulted by the host.\ncontract OwnerStub {\n    address public immutable owner;\n    constructor(address owner_) { owner = owner_; }\n}\n\n/// @dev Stand-in for the PoolManager: every extsload returns a zero word.\ncontract ManagerStub {\n    fallback() external payable { assembly { mstore(0, 0) return(0, 32) } }\n}\n\n/// @dev A valid, owner-selected program charging the maximum 14.9% entirely to itself.\ncontract MaxFeeModule {\n    function quote(address, bool, int256, uint160, uint128, bytes calldata) external pure returns (uint256[6] memory a) {\n        a[5] = 149_000;\n    }\n}\n\ncontract HookDataBypassTest is Test {\n    address constant TOKEN = 0x1b54E762aa34CF6E28E9C082F2848e28E45DA6b8;\n    address constant MANAGER = 0x000000000004444c5dc75cB358380D2e3dE08A90;\n    TradeExtensionHost host;\n    MaxFeeModule module;\n\n    function setUp() public {\n        vm.etch(TOKEN, address(new OwnerStub(address(this))).code);\n        vm.etch(MANAGER, address(new ManagerStub()).code);\n        host = new TradeExtensionHost();\n        module = new MaxFeeModule();\n        host.setModule(address(module));\n    }\n\n    /// Any trader controls hookData. A 4097-byte payload must not silently replace the\n    /// owner's active program plan with the 2.3% default and drop the trade receipt.\n    function testOversizedHookDataCannotVoidActiveProgramPlan() public view {\n        (address m1, uint256 p1) = host.quote(address(0xA11CE), true, -1 ether, new bytes(4096));\n        assertEq(m1, address(module));\n        assertEq((p1 & 0xffffff) - 1, 149_000, \"baseline: program plan applies at 4096 bytes\");\n\n        try host.quote(address(0xA11CE), true, -1 ether, new bytes(4097)) returns (address m2, uint256 p2) {\n            assertEq(m2, address(module), \"oversized hookData dropped the active program (no receipt)\");\n            assertEq((p2 & 0xffffff) - 1, 149_000, \"oversized hookData selected the default 2.3% plan\");\n        } catch {\n            // Rejecting oversized trade data outright is also acceptable: the trader chose it.\n        }\n    }\n}","reproduction":"Preconditions: token owner has installed any module via setModule (e.g. one returning allocations [0,0,0,0,0,149000]). Input: quote(sender, true, -1e18, new bytes(4096)) returns (module, packed) with rate 149000. Same call with new bytes(4097) returns (address(0), DEFAULT_PLAN) i.e. rate 23000 and module address(0). Expected: the program's plan (or a revert chosen by the trader); actual: default 2.3% plan with no receipt. On-chain: a buyer sends ExactInputSingle(main, true, amount, 0, hookData=4097 zero bytes) through the Universal Router; ProtectedHook charges 2.3% instead of the program's 14.9% and emits no TradeRecorded. Proof test (run from candidates/phase1/contract): forge test --match-path test/scratch/HookDataBypass.t.sol -> fails now with 'oversized hookData dropped the active program (no receipt)'; passes once oversized data is truncated/hashed for the module or rejected.","severity":"medium","snippet":"        if(data.length>4096) return(address(0),packed);","title":"Any trader voids the active program's fee plan and receipt by padding hookData past 4096 bytes"},{"citation":"resolved","description":"The constructor pins only the manager, the reserve's token/hook, and the processor and remainder delegates (exact address plus codehash). Reserve, fomo and feed are pinned transitively through the processor's immutables. The destinations of 21,500 of the 23,000 default pips are NOT pinned: base.project (13,000), base.platform (3,000), lpRewards_ (2,000) and staking_ (3,500), nor base.nft, base.oracle or privacyBuy_. The inherited checks (FeeEfficientHook, FeeNftHook, FeeLpRewardsHook, FeeNftStakingHook) only require that these contracts answer poolManager()/hook()/token()/poolId()/epochStart()/EPOCH()/projectRevenue()/collection() consistently, which any purpose-built contract can do. Because PhaseOneFinalizer pins the deployed implementation by codehash and then renounces both ProxyAdmins, a candidate built with substituted recipients would permanently redirect all future default fee claims and switch ClausNftStaking.stakingOpen() (which compares hook.nftStaking() to itself) to false for the real vault, closing new stakes there. The live recipients at the pinned block are project 0x49A91293a8dFf24d5c54b798C45376301D29b9f3, platform 0xF1749E78e76CfE1119206833aE93beD2dede5353, lpRewards 0xB2304d3DE407398de441b147e2c64c7c48DAf44E, nftStaking 0x4e5916428452891E1deb59d162c20266ED08CAD4, nft 0x80396c7131159eB92E7E84839e51C9887A7A95d9. This is a privileged-path defect (the owner must still deploy and finalize the wrong candidate), so it is a hardening gap against the task's 'original default fee recipients' requirement rather than an unprivileged exploit. Fix: add the same style of exact-address checks for base.project, base.platform, lpRewards_, staking_, base.nft (and optionally base.oracle/privacyBuy_) in the ProtectedHook constructor, or have PhaseOneFinalizer verify hook.projectRevenue()/platformRevenue()/lpRewards()/nftStaking()/nft() against the live hook's current values before renouncing.","line":51,"path":"candidates/phase1/contract/src/ProtectedHook.sol","reproduction":"State: deploy FakeRevenue with poolManager()==PM and active()!=0; FakePlatform with poolManager()==PM and hook()==HOOK; FakeLpRewards returning poolManager()==PM, positionManager()==reserve.positionManager(), hook()==HOOK, poolId()==main pool id, epochStart()==nft.epochStart(), EPOCH()==1 days, projectRevenue()==FakeRevenue; FakeStaking returning collection()==base.nft, poolManager()==PM, projectRevenue()==FakeRevenue, hook()==HOOK, epochStart()==nft.epochStart(), EPOCH()==1 days, startsAt()==past. Input: new ProtectedHook(BaseConfig{manager:PM, project:FakeRevenue, platform:FakePlatform, reserve:<live reserve>, feed:<live feed>, fomo:<live fomo>, attester, questionHash, processor:0x271f808c..., nft:<live nft>, oracle:<live oracle>, spacing:1}, <live privacyBuy>, FakeLpRewards, 0x51629bca..., FakeStaking, host). Expected: revert InvalidActivation (recipients differ from the live ones). Actual: construction succeeds; PhaseOneFinalizer accepts the implementation by codehash and after finalize() every default allocation of every future trade is minted to the fake contracts, irreversibly.","severity":"low","snippet":"        if (address(base.manager) != address(MANAGER) || base.reserve.token() != TOKEN || base.reserve.hook() != HOOK","title":"ProtectedHook constructor does not pin the default fee recipients (project, platform, LP vault, NFT vault, NFT, oracle) although finalization is irreversible"},{"citation":"resolved","description":"setModule has no delay or announcement period. The owner (currently 0xBb145cA83272c3806D4dDc75cA1D5514789cF1C5, an EOA-style account per the live proxies) can install, in one transaction, a program that quotes 149,000 pips to itself for every trader (address- and amount-dependent plans are allowed), and the next swap in the same block pays it. This is within the stated design (replaceable programs bounded to 14.9% + 0.1% protocol fee = 15%), and it cannot freeze trading, touch existing claims, NFT backing or LP positions, or mint anything but newly earned fees to the program. It is listed so the requester documents it as a privileged power: an offchain quote obtained via quoteTradeFee before a setModule is not binding, and integrators must rely on router slippage limits. Mitigation if desired without changing the design: a short pending/activation delay in setModule and an event-visible effective block.","line":56,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"State: any active trading. Input: owner calls setModule(M) where M.quote returns [0,0,0,0,0,149000]; a buyer's ExactInputSingle for 1 ETH in the same block pays 0.149 ETH to M (plus up to 0.001 ETH protocol fee). Expected per design: allowed; actual: allowed. No fund freeze or ceiling breach was found; verified on a live fork at the latest block (seal, setModule, buy: module claim +745000000000000 wei on a 0.005 ETH buy = exactly 14.9%).","severity":"info","snippet":"        address previous=activeModule;activeModule=module;emit ModuleChanged(previous,module);","title":"Trust assumption: token owner can switch the active trade program instantly to any contract charging up to 14.9%"}],"hash":"54a6ae1e7a02de91fd0280394e494e5bc1ceb6c21f5724dd68e2d959e2ad5e04","nodeId":"d25f2ce4-4e8c-4e80-9f9a-bf895581ff2c","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds three substantiated findings. No tracked file was changed; only `test/scratch/` and gitignored build output were added.\n\n**Findings written to `.imd-findings.json`**\n\n- **Medium, with proof test.** Any trader can void the owner-selected program's fee plan by sending more than 4096 bytes of hookData. The host returns the default 2.3% plan with no module before the program is consulted, so a program charging up to 14.9% is bypassable by anyone, and no receipt is recorded for that trade. Proof under `candidates/phase1/contract/test/scratch/HookDataBypass.t.sol` fails on the current code and tolerates either fix: truncate or hash the data forwarded to the module, or reject oversized data.\n- **Low.** The hook constructor pins only manager, reserve, processor and remainder. Project, platform, LP vault, NFT vault, NFT and oracle addresses are accepted from any contract that answers the interface checks, so a mis-built candidate would irreversibly redirect 21,500 of 23,000 default pips after finalization. Privileged path, hardening recommended.\n- **Info.** Trust assumption: the token owner can install any program with no delay, up to 14.9% to itself. Within the design and bounded by the ceiling.\n\n**Verified negatives**, using live mainnet reads and a fork at the latest block:\n\n- Both proxies are OZ v5 behind the two named ProxyAdmins, so empty-data upgradeAndCall is safe. Sealing, atomic double renounce, trading, module fee minting and receipts all worked on the fork.\n- Token ERC20 and Ownable namespaces, the metadata slot, the timed-buyback slot and schedule, and the processor and remainder code hashes all match the live state.\n- Allocation arithmetic: the six floored parts never exceed the single fee in any of the four modes, so the hook's delta always settles. The 15% check and zero-rate plans hold. Transient slots are cleared per swap and no attacker-controlled call runs in the swap.\n- The gas-dependent fallback in the quote is not reachable by a trader: the minimum gas for a successful buy is about 243k and the program plan still applied there.\n\n**Limits.** The LP rewards vault source is not in the tree, so its revert conditions on recordReward were not reviewed. Fork checks ran at the latest block, not block 26197295, because the public endpoint refuses archive reads.","treeHash":null,"usage":{"cachedInputTokens":1691457,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":64089,"runtime":"claude","turns":48,"wallClockMs":1238297}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"dff6c0d3de4aa913","findings":[{"citation":"resolved","description":"ProtectedHook's constructor (ProtectedHook.sol:47-56) pins only MANAGER, TOKEN, HOOK, the FeeProcessor (address + runtime hash) and the LpRemainderProcessor (address + runtime hash). Every other immutable that decides where fees go or which vault is authoritative is accepted from constructor arguments and validated only for self-consistency: ProjectRevenue (checked only via poolManager()==MANAGER and lpRewards_.projectRevenue()/staking_.projectRevenue() equality), PlatformRevenue (poolManager()==MANAGER only), ClausNFT (hook()/token()/poolManager() getters), IClausLpRewards (getters incl. poolId, epochStart, EPOCH, projectRevenue), ClausNftStaking (collection()/projectRevenue()/hook()/epochStart()/EPOCH()/startsAt() getters), MarketCapOracle/PoolCapSource (getters), ClausPrivacyBuy (getters). All of these are trivially satisfiable by freshly deployed contracts that return the expected values. PhaseOneFinalizer then commits irrevocably: it checks only tokenImpl/hookImpl codehash equality with its own constructor snapshot and, after the upgrade, protectionVersion/protectionKind/maximumTotalTaxPips. It never compares the new implementation's projectRevenue(), platformRevenue(), lpRewards(), nftStaking(), nft(), liquidityReserve(), fomoReserve(), predictionOracle(), ethUsdFeed(), privacyBuy() or extensions() with the values the live proxy returns before the upgrade (live today: projectRevenue 0x49A91293a8dFf24d5c54b798C45376301D29b9f3, platformRevenue 0xF1749E78e76CfE1119206833aE93beD2dede5353, lpRewards 0xB2304d3DE407398de441b147e2c64c7c48DAf44E, nftStaking 0x4e5916428452891E1deb59d162c20266ED08CAD4, nft 0x80396c7131159eB92E7E84839e51C9887A7A95d9). Because both ProxyAdmins are renounced in the same transaction, a wrong binding can never be corrected, and the remaining token owner has no power over core destinations. Impact if the wrong implementation is finalized: 100% of future project/platform/LP-reward/NFT-reward claims (default 2.3% of every trade, up to 14.9%) are minted to attacker-chosen contracts forever; in addition ClausNftStaking.stakingOpen() (ClausNftStaking.sol:98-103) becomes false for the live vault because IClausStakingHook(hook).nftStaking() no longer returns it, so the 158 currently staked NFTs stop earning and new stakes are refused. Preconditions: the owner (or the party that hands the owner the hookImpl address to put in the finalizer constructor) uses an implementation built with substitute dependencies; the on-chain finalizer offers no protection against this, unlike the processor/remainder pins. This is a missing invariant check in an irreversible step rather than a permission bypass, hence medium.","line":44,"path":"candidates/phase1/contract/src/PhaseOneFinalizer.sol","reproduction":"State: live mainnet (fork at 26197295). Inputs: (1) deploy FakeProject with poolManager()=MANAGER, active()=ATTACKER, balances(), claimActive(); FakePlatform with poolManager()=MANAGER and recordAllocation() no-op; FakeNft with hook()=HOOK, token()=TOKEN, poolManager()=MANAGER, epochStart()=1791240095, EPOCH()=86400, totalSupply()=1; FakeLp with poolManager()=MANAGER, positionManager()=live reserve.positionManager(), hook()=HOOK, poolId()=0xfaa42866f7667e3a1a10d783f3b629171febd45f056336b8df766d74afc0f0f7, epochStart()=1791240095, EPOCH()=86400, projectRevenue()=FakeProject, totalLiquidity()=1, recordReward() no-op; FakeStaking with collection()=FakeNft, poolManager()=MANAGER, projectRevenue()=FakeProject, hook()=HOOK, epochStart()=1791240095, EPOCH()=86400, startsAt()=1791672095, totalStaked()=1, recordReward() no-op; FakeOracle with source()=FakeSource(manager()=MANAGER, token()=TOKEN, feed()=live feed, poolId()=same) and scheduler()!=0; FakePrivacyBuy with manager()/token()/hook()/tickSpacing()=1. Keep the real processor 0x271f808cb55C7Cbbb3d4df0DA075deF9AE5F7340, remainder 0x51629bca14098A4f1AD77cd7E85212d67349823d, LiquidityReserve, FomoReserve and feed (required by the pinned cross-checks). (2) new ProtectedHook(BaseConfig{...fakes...}, FakePrivacyBuy, FakeLp, remainder, FakeStaking, new TradeExtensionHost()) -> constructor succeeds. (3) new PhaseOneFinalizer(OWNER, TOKEN, HOOK, TA, HA, tokenImpl, thatHook); OWNER transfers both admins and calls finalize() -> succeeds, both admins renounced. (4) swap(true,true,0.005 ether) via the router. Expected: finalize() reverts because the implementation's destinations differ from the live proxy's. Actual: trade succeeds, PM.balanceOf(FakeProject,0) increases by 0.005e18*13000/1e6 and PM.balanceOf(FakePlatform,0) by 0.005e18*3000/1e6, PM.balanceOf(0x49A9...,0) and PM.balanceOf(0xF174...,0) unchanged; ClausNftStaking(0x4e59...).stakingOpen() returns false; no path exists to change the implementation again. Required fix: in finalize(), before the upgrade read projectRevenue(), platformRevenue(), lpRewards(), nftStaking(), nft(), liquidityReserve(), fomoReserve(), processor(), lpRemainderProcessor(), predictionOracle(), ethUsdFeed(), privacyBuy() through the hook proxy and require the same values from the proxy after the upgrade (and require extensions().activeModule()==address(0) and nextReceipt()==0); alternatively pin those addresses as constants in ProtectedHook's constructor exactly as processor/remainder are pinned.","severity":"medium","snippet":"        if(IProtectedCore(token).protectionVersion()!=1 || IProtectedCore(hook).protectionVersion()!=1\n            || IProtectedCore(token).protectionKind()!=keccak256(\"CLAUS_TOKEN\") || IProtectedCore(hook).protectionKind()!=keccak256(\"CLAUS_HOOK\")\n            || IProtectedCore(token).maximumTotalTaxPips()!=150_000 || IProtectedCore(hook).maximumTotalTaxPips()!=150_000) revert InvalidFinalization();","title":"Irreversible finalize() does not verify the new hook's fee-destination and vault bindings against the live proxy; a self-consistent substitute set passes every constructor and finalize check"},{"citation":"resolved","description":"The fallback that protects trading from a hostile program is also reachable from the trader's side. (a) hookData longer than 4096 bytes makes quote() return module=address(0) with DEFAULT_PLAN, so ClausFeeBudget.start() stores module 0: the trade is charged the 2.3% default, moduleEth is 0, and _afterSwapExtension records no receipt at all. (b) If gasleft() inside quote() is below QUOTE_GAS+25_000 (line 68) the module is skipped and the default plan is returned with the module retained, so a receipt is recorded with moduleFeeEth=0 at the default rate; a wrapper contract can tune the gas it forwards to the router to hit this window deterministically. Consequently an owner-selected program that quotes above 2.3% (up to 14.9%) is optional for any trader willing to pay ~65k gas of calldata, and a program that relies on receiving a receipt for every trade can be bypassed. The ceiling, custody and trade-liveness properties are not affected; this is a correctness gap between the documented 'program fee' and what is enforceable, and ProtectedHook.quoteTradeFee() also returns the module address on fallback so integrators cannot tell a fallback from a program quoting exactly 2.3%.","line":67,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"State: finalized core, OWNER has setModule(M) where M.quote returns [13000,3000,1500,2000,3500,126000] (sum 149000). Input A: ALICE exact-input buy of 1 ether via the router with hookData = new bytes(4097). Expected (program active): hook fee 0.149 ether, PM.balanceOf(M,0) += 0.126 ether, TradeRecorded emitted. Actual: quote() returns (address(0), DEFAULT_PLAN); ProjectFeeAccrued feeEth = 0.023 ether; PM.balanceOf(M,0) unchanged; no TradeRecorded event; host.nextReceipt unchanged. Input B: same swap with empty hookData from a wrapper that forwards gas so that gasleft() at TradeExtensionHost.sol:68 is < 105000: quote() returns (M, DEFAULT_PLAN); fee 0.023 ether; TradeRecorded with moduleFeeEth=0. Fix: do not skip the program on oversized input; truncate or hash the data (e.g. pass data[:4096]) so the program still quotes and the receipt is still recorded, and make the low-gas branch revert-free but deterministic from the hook side by reserving the quote gas in ProtectedHook._beforeSwapExtension (e.g. require gasleft() >= QUOTE_GAS+40_000 before calling, which only fails swaps that would otherwise run out of gas later anyway). Also return module=address(0) from quoteTradeFee() when the plan is the fallback so integrators can detect it.","severity":"low","snippet":"        if(data.length>4096) return(address(0),packed);","title":"Trader can force the quote fallback (oversized hookData or gas starvation), paying the 2.3% default instead of the active program's rate and suppressing the receipt"},{"citation":"resolved","description":"Documented design, reported as a trust assumption rather than a defect. setModule() takes effect in the same block with no timelock or event-before-effect, and a program's quote may depend on sender/amount/data. The owner can therefore (1) front-run a large buy by switching from a 0% program to a 14.9% program, (2) run a program that charges 14.9% to everyone except favoured addresses, which is economically close to a trade restriction without violating the common ceiling, and (3) by calling Ownable2StepUpgradeable.renounceOwnership() on the token make IClausGovernor(TOKEN).owner() return address(0) so setModule() can never succeed again, pinning whatever program is active. None of these exceed the 15% aggregate ceiling or block trading, and the fallback keeps trades live, so no fix is required by the stated requirements; a 1-block or longer delay on module activation and a per-trade maximum-fee parameter accepted by the hook would narrow (1).","line":56,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"State: finalized core, no module. ALICE reads quoteTradeFee(ROUTER,true,-1 ether,\"\") = 23000 and submits a buy with amountOutMin derived from 2.3%. OWNER's setModule(M149) lands first in the block where M149 quotes 149000. Actual: ALICE's swap is charged 0.149 ether on a 1 ether buy and succeeds unless her amountOutMin rejects it; quoteTradeFee before the block said 23000. Second state: OWNER calls T.renounceOwnership(); afterwards any setModule(x) from any address reverts NotAuthorized because owner() is address(0), and activeModule stays M149 permanently.","severity":"info","snippet":"        address previous=activeModule;activeModule=module;emit ModuleChanged(previous,module);","title":"Trust assumption: token owner can change the fee program instantly (no delay) and programs may price by address, so a trader's pre-signed quote is not binding; renouncing token ownership freezes the c"}],"hash":"99b84273f3cca41c06fadff5f39d58670163db788fa226bb3efffa98115f58f6","nodeId":"4438a0fe-ef88-4601-9894-3097a69bfee5","outcome":"completed","summary":"The findings file is valid and all three snippets match the tree exactly. Nothing else was written to the repository.\n\n## Recap\n\n**Scope covered.** All four entrypoints (ProtectedToken, ProtectedHook, TradeExtensionHost, PhaseOneFinalizer), the fee-budget library, and the full inherited hook chain (HookBase → FeeEfficientHook → FeeNftHook → FeePredictionHook → FeePrivacyHook → FeeBatchedPrivacyHook → FeeLpRewardsHook → FeeNftStakingHook), plus the fixed delegate targets, revenue/reserve contracts and the staking vault. The candidate compiles under the pinned profile; ProtectedHook is 24,261 bytes, 315 under EIP-170. I used a public mainnet RPC for read-only checks: the processor and remainder runtime hashes match the constructor pins, the timed-buyback struct layout matches the live slot (window active until 1792066799), the metadata slot is already populated so name/symbol survive the upgrade, NFT staking has started (so the pre-staking `_accrue` path that would underflow on a zero-fee plan is dead), and the live token has no permit surface that the candidate drops.\n\n**Verified clean.** I traced all four trade modes with the ceiling check and confirmed the hook fee equals floor(gross·r/1e6) in every mode, that the six allocations can never underflow the wallet remainder, that the 14.9% plan plus a 0.1% protocol fee stays under 15% on both buys and sells, that zero-rate plans clear PlatformRevenue.recordAllocation, that transient plan/module slots cannot leak across swaps or into the hook's self-swaps, and that `execute()` correctly blocks reentry, replay, forged receipts, return bombs and in-swap delivery. No path lets an owner or program freeze transfers, buys, sells or LP exits, confiscate assets or redirect earned claims. No critical or high defects were found, so no proof tests were written.\n\n**Findings written to `.imd-findings.json`:**\n- **Medium.** `finalize()` never checks that the new hook is bound to the live ProjectRevenue, PlatformRevenue, LP vault, staking vault, NFT, oracle and feed. A self-consistent set of substitute contracts passes every constructor and finalize check, and the step is irreversible. Fix: compare those getters through the proxy before and after the upgrade, or pin them like the processors.\n- **Low.** A trader can force the default 2.3% quote by sending more than 4096 bytes of hookData (no receipt recorded) or by gas-starving the quote, so a program's premium and receipts are optional.\n- **Info.** Trust assumption: programs switch instantly with no delay and may price by address; renouncing token ownership pins the current program forever.\n\n**Limits.** No archive fork was available, so the fork suite was not re-run and the external LP rewards vault's source is not in the tree; its `recordReward` behaviour was taken as already live. Dust-sized swaps of a few wei can trip the ceiling check under a max protocol fee; I judged that economically irrelevant and did not report it.","treeHash":null,"usage":{"cachedInputTokens":1913844,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":65446,"runtime":"claude","turns":27,"wallClockMs":1092645}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"dd3018ab6b18e7bc","findings":[{"citation":"resolved","description":"TradeExtensionHost.quote() treats trader-supplied hookData longer than 4096 bytes as a reason to drop the program entirely: it returns module=address(0) with the 2.3% DEFAULT_PLAN. ProtectedHook._beforeSwapExtension then snapshots module=0, so FeeNftStakingHook._accrue mints nothing to the program (moduleEth=0), the 2.3% goes to the original default recipients, and _afterSwapExtension skips record(), so no receipt exists. The length is chosen by the swapper (Universal Router and any custom unlock callback forward hookData verbatim), costing only ~16k gas of zero-byte calldata. Every program rate above the default is therefore unenforceable, and a program that relies on receipts (rewards, attestations, its own 14.9% allocation) can be bypassed by anyone at will. This is not the documented 'invalid plan falls back' behaviour: the plan here is valid and the program is healthy; the fallback is triggered purely by an input the counterparty controls. Verified on a local fork at block 26197295 after finalization: with a program quoting [0,0,0,0,0,149000], a 0.01 ETH exact-input buy with empty hookData credited 1.49e15 wei of ERC6909 claims to the program and recorded a receipt; the identical buy with new bytes(4097) credited the program 0, credited projectRevenue 1.3e14 (13000 pips of the default plan) and recorded no receipt. The gas-based fallback on line 68 was also probed on the fork and is NOT exploitable: the cheapest successful swap needed ~190k gas at PoolManager.swap, so gasleft never drops below the 105k threshold inside a swap that can still complete. Required fix: do not let the length of trader data select the plan. Either forward at most 4096 bytes to the module (data[:4096] or data[:0]) while keeping module and plan, or revert the swap on oversized data (gas estimation absorbs it like InsufficientBuybackGas). If the owner intends oversized data to be tolerated, the fallback must still keep `module` so the program's allocation and receipt survive, i.e. return (module, packed) after quoting with truncated data.","line":67,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {TradeExtensionHost, IClausTradeModule} from \"src/TradeExtensionHost.sol\";\n\n/// Program that charges the maximum 14.9%, entirely to itself, for every trade.\ncontract MaxFeeProgram {\n    function quote(address, bool, int256, uint160, uint128, bytes calldata) external pure returns (uint256[6] memory a) {\n        a = [uint256(0), 0, 0, 0, 0, 149000];\n    }\n    function onTrade(TradeExtensionHost.Trade calldata, bytes calldata) external pure returns (bytes4) {\n        return IClausTradeModule.onTrade.selector;\n    }\n}\n\n/// Stand-in for the token: only owner() is consulted by TradeExtensionHost.setModule.\ncontract OwnerStub {\n    address public immutable owner;\n    constructor(address o) { owner = o; }\n}\n\n/// Stand-in for the PoolManager: quote() only reads slot0 and liquidity through extsload(bytes32).\ncontract ManagerStub {\n    fallback() external {\n        assembly { mstore(0, 1) return(0, 32) }\n    }\n}\n\ncontract OversizedDataBypassTest is Test {\n    TradeExtensionHost host;\n    MaxFeeProgram program;\n    address trader = address(0xA11CE);\n\n    function setUp() public {\n        vm.etch(0x1b54E762aa34CF6E28E9C082F2848e28E45DA6b8, address(new OwnerStub(address(this))).code);\n        vm.etch(0x000000000004444c5dc75cB358380D2e3dE08A90, address(new ManagerStub()).code);\n        host = new TradeExtensionHost();\n        program = new MaxFeeProgram();\n        host.setModule(address(program));\n    }\n\n    function rate(uint256 packed) internal pure returns (uint256) { return (packed & 0xffffff) - 1; }\n\n    /// A trader must not be able to lower the quoted program fee, or detach the program\n    /// from the trade, merely by attaching more than 4096 bytes of hookData.\n    function testHookDataLengthCannotDropTheProgramFee() public view {\n        (address m, uint256 packed) = host.quote(trader, true, -1 ether, new bytes(4096));\n        assertEq(m, address(program));\n        assertEq(rate(packed), 149000);\n\n        // Either the host still quotes the program's rate for the same trade, or it rejects the\n        // oversized data outright. Silently falling back to the 2.3% default is the defect.\n        try host.quote(trader, true, -1 ether, new bytes(4097)) returns (address m2, uint256 packed2) {\n            assertEq(m2, address(program), \"program detached by hookData length\");\n            assertEq(rate(packed2), 149000, \"program fee replaced by default\");\n        } catch {}\n    }\n}","reproduction":"State: candidate finalized on fork 31337 @ Ethereum 26197295; owner calls host.setModule(program) where program.quote returns [0,0,0,0,0,149000]. Input A: PoolManager.swap(main pool, zeroForOne=true, amountSpecified=-0.01e18, hookData=\"\"). Expected and actual: program ERC6909 balance +1,490,000,000,000,000; host.nextReceipt +1. Input B: identical swap with hookData=new bytes(4097). Expected: same 14.9% to the program and a receipt (or a revert). Actual: program balance +0, projectRevenue +130,000,000,000,000 (default 1.3% project share), platformRevenue etc. per DEFAULT_PLAN, no TradeRecorded event, nextReceipt unchanged. Self-contained proof: test/scratch/OversizedDataBypass.t.sol (stubs token.owner() and PoolManager.extsload via vm.etch) shows host.quote(..., new bytes(4096)) returns (program, 149000) while host.quote(..., new bytes(4097)) returns (address(0), 23000).","severity":"high","snippet":"        if(data.length>4096) return(address(0),packed);","title":"Any trader can strip the active program's fee and receipt by attaching more than 4096 bytes of hookData"},{"citation":"resolved","description":"finalize() is the only gate before both ProxyAdmins are renounced, yet it verifies nothing about the hook implementation beyond protectionVersion(), protectionKind() and maximumTotalTaxPips(). ProtectedHook's constructor pins PoolManager, the LiquidityReserve token/hook, FeeProcessor (address+codehash) and LpRemainderProcessor (address+codehash), but the fee destinations that every future trade pays into are only checked for mutual consistency, never against mainnet: FeeEfficientHook.sol:114 accepts any ProjectRevenue/PlatformRevenue whose poolManager() is the PM; FeeNftHook accepts any ClausNFT whose hook()/token()/poolManager() match; FeeLpRewardsHook accepts any lpRewards whose poolId/epoch/positionManager/projectRevenue() echo the expected values; FeeNftStakingHook accepts any staking contract whose collection()/projectRevenue()/epochStart() echo base.nft/base.project; FeePredictionHook accepts any oracle whose source() echoes the pool. Nothing compares these immutables with the live hook at 0x37bf...bfff (projectRevenue(), platformRevenue(), nft(), nftStaking(), lpRewards(), privacyBuy(), predictionOracle(), ethUsdFeed()). A ProtectedHook built with a self-consistent set of look-alike contracts therefore satisfies both constructors and finalize(), and after renounceOwnership() the redirection of every future project/platform/LP/NFT allocation, plus the NFT_FEE_PIPS/LIQUIDITY_FEE_PIPS gating (totalStaked()/totalLiquidity()), is permanent. The manifest's artifact hashes are compiler templates, explicitly not immutable-populated deployment hashes, so they do not close this gap either. Preconditions: whoever assembles the finalizer's hookImpl_ argument (owner key, deployer script, or a compromised build pipeline) supplies the wrong constructor arguments; the owner is trusted, but the finalizer exists precisely to make the irreversible step mechanically safe, and today it is not. Required fix: in PhaseOneFinalizer.finalize() (before upgrading) or in ProtectedHook's constructor, assert that projectRevenue, platformRevenue, nft, nftStaking, lpRewards, privacyBuy, predictionOracle, ethUsdFeed, fomoReserve and liquidityReserve of the candidate equal the corresponding getters of the live hook proxy (or hard-code the mainnet addresses the way processor/remainder already are), and assert hookImplementation.extensions().activeModule()==address(0).","line":44,"path":"candidates/phase1/contract/src/PhaseOneFinalizer.sol","reproduction":"State: deploy FakeRevenue with poolManager()==PM, FakeNFT/FakeStaking/FakeLpRewards/FakeOracle returning the live reserve.hook(), reserve.token(), base.nft.epochStart(), 1 days, the live positionManager and main poolId, and projectRevenue()==FakeRevenue. Construct ProtectedHook(BaseConfig{manager=PM, project=FakeRevenue, platform=FakePlatform, reserve=live reserve, feed=live feed, fomo=live fomo, processor=0x271f...7340, nft=FakeNFT, oracle=FakeOracle, spacing=1}, live privacyBuy, FakeLpRewards, 0x5162...823d, FakeStaking, host). Expected: construction or finalize() rejects an implementation whose fee destinations differ from the live hook's. Actual: every constructor check passes (processor/remainder/reserve are the pinned live ones), PhaseOneFinalizer(owner, token, hook, TA, HA, tokenImpl, thatHook).finalize() succeeds, both admins are renounced, and the next swap mints walletEth/platform/nftEth/rewardEth claims to the fake contracts forever.","severity":"medium","snippet":"        if(IProtectedCore(token).protectionVersion()!=1 || IProtectedCore(hook).protectionVersion()!=1","title":"Finalizer does not bind the candidate hook's fee destinations to the live ones, so a substituted-destination implementation passes the irreversible upgrade"},{"citation":"resolved","description":"The ceiling check floors the 15% allowance of gross but adds a hook fee that is floored and a protocol fee that v4 rounds up per step (SwapMath.computeSwapStep charges amountRemaining - amountIn when the target is not reached, i.e. a full wei on a 9 wei swap). For a 10 wei exact-input buy under a 14.9% plan and protocol fee 1000 pips: fee=floor(10*0.149)=1, amountToSwap=9, protocol=1, cap=floor(10*0.15)=1, so 2>1 and the swap reverts. Verified on the fork at block 26197295 after finalization with the protocol fee set to 1000|1000<<12: the 10 wei buy reverts with WrappedError(hook, TotalTaxExceeded 0xe8f1a8aa, HookCallFailed); buys of 100 wei and above, and exact-input/exact-output sells of every probed size, succeed. Economic impact is nil (sub-100 wei ETH trades), so this is not a trade freeze, but it is a concrete boundary where the 'cannot reject trading' claim does not hold. Fix: compare on a common integer basis without double flooring, e.g. (fee+protocol)*1_000_000 > gross*150_000, or carve a 1 wei tolerance, which keeps the ceiling semantics for every non-dust trade.","line":330,"path":"candidates/phase1/contract/src/legacy/FeeEfficientHook.sol","reproduction":"State: finalized candidate on fork 31337 @ Ethereum 26197295, program quoting [0,0,0,0,0,149000], PoolManager.setProtocolFee(main, 1000|(1000<<12)) by the live controller. Input: PoolManager.swap(main, zeroForOne=true, amountSpecified=-10, sqrtPriceLimit=MIN_SQRT_PRICE+1, hookData=\"\"). Expected: a 10 wei buy taxed at most 15% (1 wei) completes. Actual: revert WrappedError(0x37bf...bfff, 0xe8f1a8aa TotalTaxExceeded, 0xa9e35b2f HookCallFailed). Same input with amountSpecified=-100 succeeds.","severity":"low","snippet":"            if(fee+protocol>gross*150_000/1_000_000) revert TotalTaxExceeded();","title":"Dust exact-input buys revert with TotalTaxExceeded when a 14.9% plan meets the maximum protocol fee"}],"hash":"de9f8a5a1796a7db6900f06038bf2cf44eb7fd90d477dfc9da346c5a321058cc","nodeId":"0b7e618d-4b8e-4aa2-a07b-96e4c2775823","outcome":"completed","summary":"The review is complete. The findings file at the repository root holds three substantiated findings, one with a self-contained proof test.\n\n**Findings**\n\n- **High. Trader-controlled hookData length strips the program's fee and receipt.** In `TradeExtensionHost.quote` at line 67, hookData longer than 4096 bytes returns module zero with the 2.3% default plan. On the local fork after finalization, a 0.01 ETH buy against a 14.9% program paid the program 1.49e15 wei and recorded a receipt. The identical buy with 4097 bytes of hookData paid the program nothing, paid the default recipients, and recorded no receipt. Any program rate above the default is therefore unenforceable by anyone willing to spend about 16k gas of calldata. The ceiling itself is not breached. The proof test at test/scratch/OversizedDataBypass.t.sol fails on the current code without a fork. The gas-based fallback on the next line was probed and is not exploitable: the cheapest completing swap needs about 190k gas at the swap call, far above the 105k threshold.\n\n- **Medium. The finalizer does not bind the candidate hook's fee destinations to the live ones.** Processor, remainder processor, PoolManager and reserve are pinned by address or codehash, but ProjectRevenue, PlatformRevenue, NFT, staking, LP rewards, oracle and feed are only checked for mutual consistency. A self-consistent set of look-alike contracts passes both constructors and finalize, after which both admins are renounced and future allocations are redirected permanently. The owner is trusted, but the irreversible step has no mechanical guard here.\n\n- **Low. A 10 wei exact-input buy reverts with TotalTaxExceeded** under a 14.9% plan plus the maximum protocol fee, because the hook fee is floored, the protocol fee is rounded up per step, and the 15% cap is floored. Buys of 100 wei and above and all probed sells succeed. Economically nil, but it is a concrete boundary where trading is rejected.\n\n**Verified clean**\n\n- Token storage: mainnet balances live in the OZ v5 ERC20 namespace, the owner in the Ownable namespace, and name, symbol and URI at the candidate's metadata slot. Both admins report interface version 5.0.0, so upgradeAndCall with empty data performs no delegatecall.\n- Allocation arithmetic: six floored allocations never exceed the total fee in any of the four modes, including exact-output where the fee is derived from net. Zero-rate plans mint nothing and the platform allocation call accepts zeros.\n- Ceiling math for buys and sells, protocol snapshot timing, transient plan start and finish, hook self-swaps skipping callbacks, receipt binding, replay, locked-only execution, return-size checks and gas bounds all behaved as specified. Processor and remainder codehashes match mainnet. Staking started in the past, so the pre-staking accrual branch is dead. The timed-buyback slot is populated and active on mainnet, so the window is preserved.\n\n**Limits of review**\n\nThe candidate's own fork suite ran against a local Anvil fork at block 26197295 and passed. No invariant fuzzing beyond the repository's rounding fuzz was run. Legacy processors were reviewed for call sites and pinning, not line by line. Program-side quote inconsistency between the view path and the swap path remains a documented design limit, not a core defect.","treeHash":null,"usage":{"cachedInputTokens":2468755,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":61407,"runtime":"claude","turns":47,"wallClockMs":1122453}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"bdd9b74dce66953d","findings":[{"citation":"resolved","description":"TradeExtensionHost.quote() is the only source of the per-trade fee plan. When the swapper-supplied hookData is longer than 4096 bytes it returns (address(0), DEFAULT_PLAN) before the owner-selected program is consulted. hookData is chosen freely by whoever calls PoolManager.swap (the Universal Router and V4Router forward ExactInput/ExactOutput hookData verbatim; ProtectedHook._beforeSwapExtension forwards it unchanged). ProtectedHook then snapshots module=address(0), FeeNftStakingHook._accrue computes moduleEth=0 and routes the 2.3% default to the default recipients, and ProtectedHook._afterSwapExtension skips extensions.record() because module==0, so no TradeRecorded receipt exists. The fallback was designed for malformed, reverting, oversized or out-of-gas PROGRAM responses; this branch is triggered by the counterparty, while the program is healthy. Consequences while any program is active: every rate above 2.3% (up to 14.9%) is voluntary for the trader at a cost of about 4 KB of zero calldata (~65k gas); the program's own allocation (pips index 5) is never minted; and a trader can opt out of any program that reacts to receipts. Holders are not harmed and the 15% ceiling, custody and trade liveness are intact, but the central r2 requirement (owner-selected replaceable programs that price trades) is unenforceable by an unprivileged party. Merged from four specialist reports (audit_flow, audit_math, audit_permissions, audit_economics). The related gas-starvation branch on line 68 (gasleft()<QUOTE_GAS+25_000 returns the default plan but keeps the module) was probed by audit_math on the fork and not shown reachable inside a swap that can still complete (~190k gas is needed after the quote), so it is not reported as a defect. Required fix: do not let the length of trader data select the plan. Quote the program with the data truncated (e.g. bytes calldata quoted = data.length>4096 ? data[:0] : data, or data[:4096]) while keeping module and plan, and hash the same truncated data in ProtectedHook._afterSwapExtension so execute() still binds to the quoted input; alternatively revert the swap on oversized data (the trader chose the payload). Any fix must fit in the 315 bytes of EIP-170 headroom left in ProtectedHook if it touches the hook.","line":67,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {TradeExtensionHost, IClausTradeModule} from \"src/TradeExtensionHost.sol\";\n\n/// Program that charges the maximum 14.9%, entirely to itself, for every trade.\ncontract MaxFeeProgram {\n    function quote(address, bool, int256, uint160, uint128, bytes calldata) external pure returns (uint256[6] memory a) {\n        a = [uint256(0), 0, 0, 0, 0, 149000];\n    }\n    function onTrade(TradeExtensionHost.Trade calldata, bytes calldata) external pure returns (bytes4) {\n        return IClausTradeModule.onTrade.selector;\n    }\n}\n\n/// Stand-in for the token: only owner() is consulted by TradeExtensionHost.setModule.\ncontract OwnerStub {\n    address public immutable owner;\n    constructor(address o) { owner = o; }\n}\n\n/// Stand-in for the PoolManager: quote() only reads slot0 and liquidity through extsload(bytes32).\ncontract ManagerStub {\n    fallback() external {\n        assembly { mstore(0, 1) return(0, 32) }\n    }\n}\n\ncontract OversizedDataBypassTest is Test {\n    TradeExtensionHost host;\n    MaxFeeProgram program;\n    address trader = address(0xA11CE);\n\n    function setUp() public {\n        vm.etch(0x1b54E762aa34CF6E28E9C082F2848e28E45DA6b8, address(new OwnerStub(address(this))).code);\n        vm.etch(0x000000000004444c5dc75cB358380D2e3dE08A90, address(new ManagerStub()).code);\n        host = new TradeExtensionHost();\n        program = new MaxFeeProgram();\n        host.setModule(address(program));\n    }\n\n    function rate(uint256 packed) internal pure returns (uint256) { return (packed & 0xffffff) - 1; }\n\n    /// A trader must not be able to lower the quoted program fee, or detach the program\n    /// from the trade, merely by attaching more than 4096 bytes of hookData.\n    function testHookDataLengthCannotDropTheProgramFee() public view {\n        (address m, uint256 packed) = host.quote(trader, true, -1 ether, new bytes(4096));\n        assertEq(m, address(program));\n        assertEq(rate(packed), 149000);\n\n        // Either the host still quotes the program's rate for the same trade, or it rejects the\n        // oversized data outright. Silently falling back to the 2.3% default is the defect.\n        try host.quote(trader, true, -1 ether, new bytes(4097)) returns (address m2, uint256 packed2) {\n            assertEq(m2, address(program), \"program detached by hookData length\");\n            assertEq(rate(packed2), 149000, \"program fee replaced by default\");\n        } catch {}\n    }\n}","reproduction":"Unit (no fork, run from candidates/phase1/contract): owner installs a module whose quote returns [0,0,0,0,0,149000]. host.quote(trader,true,-1 ether,new bytes(4096)) returns (module, rate 149000); host.quote(trader,true,-1 ether,new bytes(4097)) returns (address(0), rate 23000). Expected: the program's plan (or a revert chosen by the trader); actual: default 2.3% plan with module detached. Fork (local anvil chain 31337 at Ethereum block 26197295, candidate finalized, same module active): ExactInputSingle buy of 0.01 ETH via router 0x23617e59A5925b2A4Bf75d73ff6711cD0b29De85 with empty hookData mints 1,490,000,000,000,000 wei of ERC6909 claims to the module and host.nextReceipt becomes 1; the identical buy with hookData=new bytes(4097) mints 0 to the module, mints 130,000,000,000,000 wei (1.3% of the default plan) to the live projectRevenue 0x49A91293a8dFf24d5c54b798C45376301D29b9f3, and nextReceipt stays 1 (no TradeRecorded). All three specialist proofs fail on the current code for this reason; the attached one is kept.","severity":"high","snippet":"        if(data.length>4096) return(address(0),packed);","title":"Any trader strips the active program's fee plan and receipt by attaching more than 4096 bytes of hookData"},{"citation":"resolved","description":"finalize() is the only gate before both ProxyAdmins are renounced, yet after the upgrade it checks only protectionVersion(), protectionKind() and maximumTotalTaxPips() of the two proxies, plus the implementation codehash it snapshotted in its own constructor. ProtectedHook's constructor (ProtectedHook.sol lines 51-56) pins only MANAGER, the reserve's token/hook, FeeProcessor (exact address + runtime hash) and LpRemainderProcessor (exact address + runtime hash). The destinations that receive 21,500 of the 23,000 default pips and all future program allocations are accepted from constructor arguments and validated only for mutual consistency: FeeEfficientHook.sol:114 requires project_/platform_.poolManager()==manager; FeeLpRewardsHook.sol:66-73 requires lpRewards_ to echo poolManager/positionManager/hook/poolId/epochStart/EPOCH/projectRevenue; FeeNftStakingHook.sol:26-31 requires staking_ to echo collection/poolManager/projectRevenue/hook/epochStart/EPOCH. Any purpose-built contract satisfies these. Nothing compares the candidate's projectRevenue(), platformRevenue(), lpRewards(), nftStaking(), nft(), privacyBuy(), predictionOracle() or ethUsdFeed() with what the live hook proxy 0x37bf...bfff returns before the upgrade (live: project 0x49A91293a8dFf24d5c54b798C45376301D29b9f3, platform 0xF1749E78e76CfE1119206833aE93beD2dede5353, lpRewards 0xB2304d3DE407398de441b147e2c64c7c48DAf44E, nftStaking 0x4e5916428452891E1deb59d162c20266ED08CAD4). The manifest's artifact hashes are compiler templates, not immutable-populated deployment hashes, so they do not close the gap either. Because both admins are renounced in the same transaction and the remaining token owner has no power over core destinations, a wrong binding is permanent: all future project/platform/LP-reward/NFT-reward claims go to the substitute contracts, and ClausNftStaking.stakingOpen() (ClausNftStaking.sol:98-103, which compares hook.nftStaking() with itself) turns false for the real vault, so recordReward and new stakes on it are refused. Precondition: the owner (or whoever assembles the hookImpl_ argument: deployer script, build pipeline) supplies an implementation built with substitute dependencies. This is a missing invariant check in an irreversible privileged step, not an unprivileged bypass, hence medium. Merged from audit_flow (medium), audit_math (medium) and audit_economics (low). Required fix: in finalize(), before upgrading, read projectRevenue(), platformRevenue(), lpRewards(), nftStaking(), nft(), liquidityReserve(), fomoReserve(), processor(), lpRemainderProcessor(), predictionOracle(), ethUsdFeed() and privacyBuy() through the hook proxy and require the same values from the proxy after the upgrade (also require extensions().activeModule()==address(0) and nextReceipt()==0); or pin those addresses as exact constants in ProtectedHook's constructor the way processor/remainder already are.","line":44,"path":"candidates/phase1/contract/src/PhaseOneFinalizer.sol","reproduction":"Local anvil fork, chain 31337 at Ethereum block 26197295 (test/scratch/JudgeFork.t.sol::testSubstituteDestinationsPassFinalize). Deploy FakeProject{poolManager()=PM}, FakePlatform{poolManager()=PM, recordAllocation() no-op}, FakeLp{poolManager()=PM, positionManager()=live reserve.positionManager(), hook()=HOOK, poolId()=main pool id, epochStart()=live nft.epochStart(), EPOCH()=1 days, projectRevenue()=FakeProject, totalLiquidity()=1, recordReward() no-op}, FakeStaking{collection()=live nft, poolManager()=PM, projectRevenue()=FakeProject, hook()=HOOK, epochStart()=live nft.epochStart(), EPOCH()=1 days, startsAt()=epochStart, totalStaked()=1, recordReward() no-op}. new ProtectedHook(BaseConfig{live manager/reserve/feed/fomo/attester/questionHash/processor 0x271f...7340/nft/oracle, project=FakeProject, platform=FakePlatform, spacing 1}, live privacyBuy, FakeLp, live remainder 0x5162...823d, FakeStaking, new TradeExtensionHost()) constructs without revert. new PhaseOneFinalizer(OWNER, TOKEN, HOOK, TA, HA, new ProtectedToken(), thatHook); OWNER transfers both admins and calls finalize(). Expected: revert because the implementation's destinations differ from the live proxy's. Actual: finalize() succeeds, TA.owner()==HA.owner()==address(0), hook.projectRevenue()==FakeProject. A 0.005 ETH exact-input buy via the router then mints 65,000,000,000,000 wei to FakeProject (13000 pips), 15,000,000,000,000 to FakePlatform (3000), 10,000,000,000,000 to FakeLp (2000) and 17,500,000,000,000 to FakeStaking (3500); the live project, platform, lpRewards and staking vault claim balances are unchanged; ClausNftStaking(0x4e59...).stakingOpen() was true before the upgrade and is false after it; no upgrade path remains.","severity":"medium","snippet":"        if(IProtectedCore(token).protectionVersion()!=1 || IProtectedCore(hook).protectionVersion()!=1","title":"Irreversible finalize() never binds the candidate hook's fee destinations to the live proxy's; a self-consistent substitute set passes every constructor and finalize check"},{"citation":"resolved","description":"_checkTotalTax compares the floored hook fee plus the protocol fee actually accrued during the swap against floor(gross*15%). Uniswap v4's SwapMath.computeSwapStep charges amountRemaining - floor(amountRemaining*(1e6-fee)/1e6) as the fee when the target is not reached, i.e. a full 1 wei on any input below 1000 wei; with lpFee 0 the whole step fee becomes protocol fee. For gross < 7 wei the allowance floor(gross*0.15) is 0 while protocol is 1, so every 1-6 wei exact-input buy reverts; in the oneForZero branch (line 335) a 1 wei token sell hits protocol==input and reverts; with a 14.9% program a 10 wei buy gives fee=1, protocol=1, allowance=1 and reverts. Without the protocol fee the same trades succeed, and the live pool currently has protocol fee 0, so this is a boundary where the 'ceiling never rejects trading' claim fails only for sub-100-wei trades with no economic value. Not a trade freeze, hence low. Merged from audit_math (low) and audit_permissions (low). Fix: compare on a common integer basis without double flooring, e.g. (fee+protocol)*1_000_000 > gross*150_000 or tolerate the 1 wei rounding when protocol<=1, keeping the ceiling semantics for every non-dust trade.","line":330,"path":"candidates/phase1/contract/src/legacy/FeeEfficientHook.sol","reproduction":"Local anvil fork, chain 31337 at Ethereum block 26197295, candidate finalized with live dependencies (test/scratch/JudgeFork.t.sol::testDustTradesRevertUnderProtocolFee). (1) ExactInputSingle buy of 3 wei with protocol fee 0 succeeds. (2) vm.prank(PM.protocolFeeController()); PM.setProtocolFee(main, 1000|(1000<<12)). ExactInputSingle buys of 1,2,3,4,5,6 wei via the router all revert with 0x90bfb865 Wrap__FailedHookCall(hook 0x37bf...bfff, beforeSwap/afterSwap selector, 0xe8f1a8aa TotalTaxExceeded); 7 and 8 wei succeed. Exact-input sell of 1 wei CLAUS reverts with the same error; 100 wei succeeds. (3) With a program quoting [0,0,0,0,0,149000]: a 10 wei buy reverts with TotalTaxExceeded, a 100 wei buy succeeds. Expected: a trade whose hook fee is within plan is never rejected by the ceiling check; actual: rejected for these inputs.","severity":"low","snippet":"            if(fee+protocol>gross*150_000/1_000_000) revert TotalTaxExceeded();","title":"With the v4 protocol fee set, dust main-pool trades revert on the 15% ceiling check because the protocol fee rounds up to 1 wei"},{"citation":"resolved","description":"Documented design, reported as a privileged-power trust assumption, not a defect. setModule() takes effect in the same block with no delay, announcement or two-step, and a program's quote may depend on sender, amount and data. The token owner (0xBb145cA83272c3806D4dDc75cA1D5514789cF1C5, Ownable2Step owner of the proxy) can therefore front-run a pending swap with a program charging 14.9%, or run a program that charges 14.9% to everyone except favoured addresses (economically close to a trade restriction without exceeding the common ceiling or rejecting trades). A quote obtained through quoteTradeFee() before the swap is not binding; users are protected only by router slippage bounds. Separately, Ownable2StepUpgradeable.renounceOwnership() on the token makes IClausGovernor(TOKEN).owner() return address(0), after which setModule() can never succeed and whatever program is active stays active permanently. None of this exceeds the 15% aggregate ceiling, blocks trading, or touches existing claims, NFT backing or LP positions. Merged from audit_flow, audit_permissions and audit_economics (all info). If the owner wants quotes to be reliable, a short pending/activation delay with an event-visible effective block would narrow the window without changing the design.","line":56,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"Code path: setModule() lines 52-57 has only the owner check and module sanity checks and writes activeModule immediately. Project test testReplaceableProgramFeeBudgetAndFourTradeModes shows the sequence: quoteTradeFee(ROUTER,true,-0.005e18,'') returns 23000 before setModule; after vm.prank(OWNER) host.setModule(module charging 149000) in the same block, ALICE's 0.005 ETH exact-input buy is charged 745,000,000,000,000 wei (14.9%). Expected per design: allowed; actual: allowed. Renounce path: after T.renounceOwnership(), any setModule(x) from any address reverts NotAuthorized because owner()==address(0).","severity":"info","snippet":"        address previous=activeModule;activeModule=module;emit ModuleChanged(previous,module);","title":"Trust assumption: token owner can switch the active trade program instantly to any contract charging up to 14.9%, and renouncing token ownership pins the current program forever"},{"citation":"resolved","description":"When the program's STATICCALL fails, runs out of gas, returns the wrong size, or returns an allocation above MAX_RATE (lines 77, 81, 85), quote() returns (module, DEFAULT_PLAN) with the module retained. ProtectedHook then charges 2.3%, mints nothing to the program (pips index 5 is 0 in the default plan) and still calls extensions.record(module, ...), so a receipt bound to that program exists and anyone may execute() it, delivering onTrade for a trade the program did not price. moduleFeeEth is 0 in such receipts, so a careful program can detect the case, but programs that treat every delivered receipt as a paid trade will mis-account. The same (module, DEFAULT_PLAN) tuple is what ProtectedHook.quoteTradeFee() returns, so integrators cannot tell a fallback from a healthy program quoting exactly 23000. No funds move and the ceiling holds, so this is informational. Merged from audit_permissions (info) and the integrator note in audit_flow. Fix options: return (address(0), DEFAULT_PLAN) on all fallbacks so no receipt is recorded (consistent with the oversized-data branch, which already does this), or carry a fallback flag in the Trade struct / quoteTradeFee result.","line":77,"path":"candidates/phase1/contract/src/TradeExtensionHost.sol","reproduction":"Code path: quote() lines 77/81/85 return the module with the default plan; ProtectedHook._afterSwapExtension (ProtectedHook.sol:72-76) records whenever ClausFeeBudget.module()!=0. Project test testOptionalBrokenModuleCannotBlockTrading installs RevertingModule (fallback reverts) and swaps 0.001 ETH and 10 CLAUS successfully at 23000 pips; by the code path each swap increments host.nextReceipt and emits TradeRecorded naming RevertingModule with moduleFeeEth 0, and host.execute(trade,'',100000) then calls RevertingModule.onTrade. Expected per README ('receipt attests the trade's original module/data/amounts'): receipts only for trades priced by their program; actual: receipt for an unpriced trade.","severity":"info","snippet":"        if(!ok) return(module,packed);","title":"Receipts are recorded and deliverable for trades the program never priced, and quoteTradeFee() cannot distinguish a fallback from a genuine 2.3% quote"},{"citation":"resolved","description":"If block.timestamp < nftStakingStartsAt, _accrue delegates to FeeLpRewardsHook._accrue (FeeLpRewardsHook.sol:155-161), which computes project = gross/50 (2%) via FeeMath.projectFromGross and platform = totalFee - project with checked arithmetic. Any plan whose total rate is below 2%, including the supported zero-fee plan, makes totalFee < project and the subtraction reverts with Panic(0x11), which would revert every main-pool swap while such a program is active. On the pinned mainnet state this branch is unreachable: ClausNftStaking at 0x4e5916428452891E1deb59d162c20266ED08CAD4 reports startsAt = 1791672095 (2026-10-10), earlier than the fork block timestamp 1792059851, and nftStakingStartsAt is an immutable copied from it, so ProtectedHook always takes the plan-driven branch. Reported so the dead branch is not revived (e.g. on a redeploy with a future vault) without removing the hard 2% floor; the minimal fix is to delete the legacy branch from the final core or route it through ClausFeeBudget as well. From audit_permissions (info), verified.","line":44,"path":"candidates/phase1/contract/src/legacy/privacy/FeeNftStakingHook.sol","reproduction":"Verified on the local fork at block 26197295: vault.startsAt() = 1791672095 < block.timestamp = 1792059851 (test/scratch/JudgeFork.t.sol::testVaultStartedAndSizes), so the condition on line 43 is false for the mainnet deployment. Only reachable with a ClausNftStaking whose startsAt is in the future: with nftStakingStartsAt > block.timestamp and an active program returning [0,0,0,0,0,0], an exact-input buy of 1 ETH gives totalFee = 0, project = 0.02 ETH, and platform = totalFee - project reverts with Panic(0x11), failing the swap. Expected: zero-fee plan accepted (README: 'Zero-fee plans are supported'); actual: revert in that configuration.","severity":"info","snippet":"            super._accrue(gross, totalFee);","title":"Pre-staking-start legacy accrual branch cannot handle plans below 2% (dead on mainnet because the staking vault started 2026-10-10)"},{"citation":"resolved","description":"forge build with the candidate profile (0.8.26, cancun, via-IR, optimizer 200) produces 24261 bytes of runtime for ProtectedHook, matching the manifest. Immutables are already included in that size, so the deployed code is the same size, leaving 315 bytes for any fix arising from this review (the hookData truncation for the high finding touches ProtectedHook._afterSwapExtension if the hashed data must match the quoted data). Not a defect; recorded because the finalizer pins the implementation codehash and a size overrun discovered at deployment would require a new review cycle. From audit_permissions (info), verified.","line":27,"path":"candidates/phase1/contract/src/ProtectedHook.sol","reproduction":"cd candidates/phase1/contract && forge build; python3 -c reads out/ProtectedHook.sol/ProtectedHook.json deployedBytecode.object: 24261 bytes (limit 24576). Project test testAtomicSealPreservesCustodyAndRemovesBothUpgradeOwners asserts address(candidate).code.length <= 24576.","severity":"info","snippet":"contract ProtectedHook is FeeNftStakingHook {","title":"ProtectedHook runtime is 24261 bytes, 315 bytes under the EIP-170 limit; any post-review change risks an undeployable core"},{"citation":"resolved","description":"Judge pass over the four specialist reports (audit_flow, audit_math, audit_permissions, audit_economics; 16 findings in total) for the r2 candidate at candidates/phase1/contract/src and manifest. Read in full: ProtectedToken, ProtectedHook, TradeExtensionHost, PhaseOneFinalizer, ClausFeeBudget, FeeMath, FeeCalls, HookBase, FeeEfficientHook, FeeNftStakingHook, FeeLpRewardsHook, plus the constructors of FeeNftHook, FeePredictionHook, FeePrivacyHook, FeeBatchedPrivacyHook and ClausNftStaking.stakingOpen/recordReward. Compiled the candidate with its own profile. Ran the three supplied proof tests (all fail on the current code for the stated reason). Started a local anvil fork (chain 31337) of Ethereum at block 26197295 from a public archive endpoint and reproduced the destination-substitution, dust-trade and oversized-hookData findings there, and confirmed the staking vault's startsAt is in the past. The project's own 12 fork tests were run against the same fork and all pass. Duplicates were merged: the oversized-hookData bypass (4 reports) into one high; destination binding (3 reports) into one medium; dust ceiling reverts (2 reports) into one low; instant setModule trust assumption (3 reports) into one info. Dropped: audit_flow's gas-starvation sub-claim (not shown reachable inside a completable swap, per audit_math's fork probe; not independently measured here). Limits: no Slither/Mythril, no long fuzz runs; the live LP-rewards vault, ClausNFT/NftRewards, ProjectRevenue/PlatformRevenue, FeeProcessor and LpRemainderProcessor bytecode were not independently disassembled; the ERC-7201 storage layout compatibility of ProtectedToken with the live token implementation was checked only through the project's post-upgrade fork tests (owner, balances, transfers, metadata); transient-slot contamination across batched multi-hop swaps and the timed-buyback stream arithmetic were not re-probed beyond the project's tests. No release-blocking (trade-freezing, custody, ceiling or alternative-upgrade) defect was reproduced; the high finding is an unprivileged economic bypass of program fees and receipts, not a loss of user funds.","line":2,"path":"candidates/phase1/manifest.json","reproduction":"Not a defect. See the individual findings for failing inputs.","severity":"info","snippet":"  \"version\": \"r2\",","title":"Review scope, verification performed and limits"}],"hash":"dfba283b9e0d4842cd77d7fcaeeb59b16f26407d369928f4e60b5de89ae5baa1","nodeId":"241d49ee-3802-4a5d-b90c-cff32d17aaf1","outcome":"completed","summary":"The review is complete. `.imd-findings.json` in the repository root holds 8 merged findings, all reproduced against the r2 candidate. The anvil fork has been stopped and no repository files were changed; only `test/scratch/` holds my throwaway reproduction tests.\n\n**What I verified and kept**\n\n- **High: oversized hookData bypass** (TradeExtensionHost.sol:67). Four specialists reported this and all three supplied proofs fail on the current code for the stated reason. I also reproduced it on a local fork at block 26197295: a padded 0.01 ETH buy mints nothing to a 14.9% program, routes 1.3% to the live project revenue, and records no receipt. Merged into one finding with one proof attached. Severity set to high because it is unprivileged, trivial, and defeats the stated r2 requirement for enforceable replaceable programs, though no user funds are at risk.\n- **Medium: finalize() does not bind fee destinations** (PhaseOneFinalizer.sol:44). Reproduced on the fork: a hook built with fake project, platform, LP-rewards and staking contracts passed every constructor check and finalize(), both admins were renounced, the next trade minted all default allocations to the fakes, and the real staking vault's stakingOpen() flipped to false. Three reports merged.\n- **Low: dust trades revert under the protocol fee** (FeeEfficientHook.sol:330). Reproduced: 1 to 6 wei buys and a 1 wei sell revert with TotalTaxExceeded once the 0.1% protocol fee is set; 7 wei succeeds; a 10 wei buy at 14.9% reverts. Two reports merged.\n- **Info (four):** instant setModule trust assumption (three reports merged), receipts recorded for unpriced trades plus fallback indistinguishable in quoteTradeFee, the dead pre-start accrual branch (confirmed dead: vault startsAt is before the fork timestamp), and the 315-byte EIP-170 margin (confirmed 24261 bytes). One scope-and-limits entry closes the list.\n\n**Dropped:** audit_flow's gas-starvation sub-claim, since audit_math's fork probe showed it unreachable inside a swap that can still complete and I did not measure otherwise.\n\n**Other checks:** the candidate compiles under its own profile, and the project's 12 fork tests all pass against my fork. No release-blocking defect (trade freeze, custody, ceiling breach or alternative upgrade path) was reproduced. Limits are recorded in the final info entry: no Slither or Mythril, no long fuzzing, and the live vault and processor bytecode were not independently disassembled.","treeHash":null,"usage":{"cachedInputTokens":1374847,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":34226,"runtime":"claude","turns":33,"wallClockMs":694448}}],"verification":[]}