{"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":"bacfa675-c77b-4b76-8422-bd19f6951889","kind":"skill:adversarial-review","nodes":[{"acceptedSubmissionHash":"06e9e2a6571b217b33ef4537aaeabec5953530b8222018ba07816e7908f22442","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"c26edc76dc7a76e09a42c3634054429c1679c6e247747d647a03f77e5b332d7e","skillId":"adversarial-review","tools":[]},"key":"adversarial_review","kind":"code","role":"review","skillHash":"c26edc76dc7a76e09a42c3634054429c1679c6e247747d647a03f77e5b332d7e","skillId":"adversarial-review","state":"accepted"}],"objective":"Review launch 170's MedallionHook and FareToken (this commit) as an attacker. Read-only: report findings, change nothing. The pool is a Sepolia test deployment with no real value; the petition text in the source is fiction.\n\nLive facts (Sepolia, 2026-09-27; the reviewer has no network, take them as given): MedallionHook at 0xe8ba92e07c2e2dab545f3d3a4dfdd1f00e4f40cc with poolManager() = Sepolia's PoolManager 0xE03A1074c86CFeDd5C142C4F04F1a1536e203543; FareToken at 0x4ced114edef2d3b0a46d0a8c206edea3729d5a50; pool id 0x1e6f0b2bf47787c51443a89aef85ca34670eda0314a31870f129ec0720dc72bb initialised in block 11780004 with one-sided FARE447 liquidity; totalFees() is 0 and no swap has happened. The mainnet constants MEDALLION_NFT, IMD and POOL4_HOOK have no code on Sepolia, so there retire() reverts MedallionUnavailable and burnIMD cannot find its burn pool, as README.md says.\n\nFocus:\n1. Return-delta accounting in all four swap modes: the fee minted as claims in beforeSwap before afterSwap verifies the fill; the positive specified BeforeSwapDelta and the positive unspecified afterSwap delta; the `specified == 0` and PartialFill checks; _magnitude at type(int256).min; and whether any mode lets a swapper pay less than 2% of the ETH leg or take out more than the pool gave.\n2. LPs: the hook has no liquidity callbacks. Confirm LPs can always add and remove, never pay the hook, and that fees never come out of LP principal.\n3. retire(): anyone can trigger it once the NFT holder has approved the hook. Check the ownerOf/transferFrom handling, the exact 1.64 ETH burn-and-take of claims, the pending-unlock flag in transient slots 1 and 2, the manager-locked requirement, and totalFees crossing the cap through several pools that attach the same hook.\n4. burnIMD as it would run on mainnet: manipulation of the plain-pool anchor (it walks at most 200 ticks per elapsed block), the one-sided PriceOffReference check, MAX_SLIPPAGE_BPS = 400 against a sandwich, trust in POOL4_HOOK's refTick() and marketOpen() including return-data size, and the 0.05 / 0.01 ETH batch caps.\n5. Any path that strands claims forever (for example on Sepolia) and whether the disclosure is loud enough.\n\nReport each finding with a severity, file and line, and the exact call sequence that triggers it.","parentJobId":null,"planHash":"9ff712c0689ff036a5e1a1df6298218ae23c0b17b8d6ce588a1b97d7acde6a99","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"bacfa675-c77b-4b76-8422-bd19f6951889","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":"51046","feedbackHash":"040d3c85edd047d77d2bfba4d1494e5b89ab1b2f18845e7277dba48fca5e1ef6","nodeKey":"adversarial_review","submissionHash":"06e9e2a6571b217b33ef4537aaeabec5953530b8222018ba07816e7908f22442","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"39d79d78f2a96ad93436e918fdb2ef5affde35fa32d1cfa21043d454ff6ffe92","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"02ae6543274731ab","findings":[{"description":"Fallback updates its reference from the current plain-pool spot immediately before deriving minimum output. The accumulated elapsed-block allowance in _walkAnchor (lines 351-358) lets a newly manipulated spot become the reference without having persisted for those blocks. Requiring the manager to be locked does not prevent an attacker from completing separate manager unlocks before and after burnIMD in one transaction. In a plain pool with no other liquidity, an attacker can move the empty pool price, supply liquidity at that price, trigger the burn, and withdraw almost the entire ETH batch in exchange for negligible IMD. The first burn necessarily uses this fallback even when POOL4 supplies a valid, open reference. This is a conditional mainnet economic vulnerability, limited to 0.01 ETH per fallback burn and the cooldown; it does not consume the creator reserve or affect the supplied zero-fee Sepolia state. The documented accumulated spot-anchor limitation explains the behavior, but the 4% floor consequently provides no independent protection against this atomic repricing. Removing this exposure requires revisiting that reference-design assumption. Existing anchor tests check movement bounds and mocked outputs rather than this settled manipulation sequence.","line":202,"path":"src/MedallionHook.sol","reproduction":"Use a real v4 PoolManager with protocol fees zero and ordinary IMD ERC-20 behavior. Start with hasBurned=false, anchorTick=0, anchorBlock=lastBurnBlock=B, totalFees=1.65 ether, creatorPaid=burnSpent=0, and 1.65 ether of backed native claims owned by the hook. POOL4 may honestly return marketOpen=true and refTick=0. Initialize burnPoolKey(false) at sqrtPriceX96=2^96 with no liquidity. At block B+1150, execute the following from an attacker contract in one transaction: (1) Open a manager unlock and call swap(plainKey, SwapParams(true, -1, 803050292342716926398368), emptyBytes). This moves the empty pool to tick -230000 with zero balance deltas; finish the unlock. (2) Open another unlock and add liquidity 100000000000000 over ticks [-230200,-229800], settling 98162491063329411 wei ETH and 10084907 raw IMD units; finish the unlock. (3) Call hook.burnIMD(false,0). Since hasBurned is false, normal reference mode is skipped. The allowance is 1150*200=230000 ticks, so the anchor immediately becomes -230000 and PriceOffReference passes. quoteAtTick(-230000,0.01 ether)=1027368 raw IMD units and minOut=986274. The fully filled swap outputs 1016075 units, so it passes Slippage and spends 0.01 ether of claims. (4) Open another unlock, remove all the attacker's liquidity, collect its LP fees, settle, and finish. Principal withdrawal returns 108062491063329410 wei ETH and 9068831 IMD units; LP fees return another 100000000000000 wei ETH. Net attacker proceeds are 9999999999999999 wei ETH for 1016076 raw IMD units. At the unchanged POOL4 reference of tick 0, those IMD units have only the same number of wei of ETH value. Expected protection would reject this attacker-created execution price relative to the independent reference; actual execution accepts it because the manipulated spot also supplies the minimum-output reference. These quantities were checked using integer TickMath, SqrtPriceMath, and SwapMath calculations from the vendored v4 implementation; this is a constructed mainnet-capable state, not an assertion about current mainnet liquidity or a fork-test result.","severity":"medium","title":"An atomic spot-price manipulation can extract the fallback burn batch"}],"hash":"06e9e2a6571b217b33ef4537aaeabec5953530b8222018ba07816e7908f22442","nodeId":"d7c0a20d-00a2-42e5-8ff0-899cc3511a5e","outcome":"completed","summary":"Saved [`.imd-findings.json`](.imd-findings.json) with one medium finding: atomic manipulation of the fallback reference can extract nearly a 0.01 ETH burn batch.\n\nIncludes exact state, calls, and integer accounting. No fork test performed. Contracts and tests unchanged.","treeHash":null,"usage":{"cachedInputTokens":536832,"inputTokens":81413,"model":"gpt-6-astra","outputTokens":9596,"runtime":"codex","turns":5,"wallClockMs":344490}}],"verification":[]}