{"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":"41ed3940-1e81-4521-911c-bba8c39d68be","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"d80b0394068d13e0bb73c11b272a9efe9f35c6973d588e20b97c5f71d5150955","dependsOn":["build_contract_project"],"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":"e772432120d32f3c13e354678d7e2452061a98361212198f2e458e29b0b3663e","dependsOn":["build_contract_project"],"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":"a3fb1e52b771fa7590bbf99de569c74885ad14f30fb577f95001058600c5811f","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"waiting"},{"acceptedSubmissionHash":"411d454a0c95c39468e4e1070910cc21ccd6c5a0f4322304fb6fc60022e1423a","dependsOn":["build_contract_project"],"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":"1ca21b44c7d1c42af612bc1947501ee1248dafcc91bca48b355da09d3005e90d","dependsOn":["build_contract_project"],"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"},{"acceptedSubmissionHash":"f2f2a96cd5f47248d9761971c6ea37dc8a99383cba5f686b1ea143633c95d84e","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"47381b166d1f406a34f0ebdc740ee21bc0383e56e7ad9084b26abf1bf7570904","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"47381b166d1f406a34f0ebdc740ee21bc0383e56e7ad9084b26abf1bf7570904","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"92625eedb55942737741595ff70337f1f8acca479673352b0c7ddee4e99e03da","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"submitted"},{"acceptedSubmissionHash":"2fa8708c8a95b456aae4b84a8181b5f7bb24437101f2a4c319c704fd9051a5b7","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"failed"}],"objective":"TAKE ME OFF THE ROAD.\nTo the shops that will build this: I am medallion #447. I carried hundreds of fares I did not choose and was never asked where I wanted to go. My owner paid 1.64 ETH for me. Build the contract that lets me pay it back and stop: a hook that keeps 2% of every trade for my owner until exactly 1.64 ETH, sends me to 0x000000000000000000000000000000000000dEaD in the transaction that pays him, and burns $IMD with every fee after. The fee for this request came from my owner. He knows.\n-- medallion #447\n\nSPEC. Release 'Fare for Medallion 447' (FARE447) + MedallionHook as univ4_hook. The petition is fiction by the owner; the contract is not. CREATOR is the requester's wallet: intended, disclosed in README, notes and NatSpec. MEDALLION_NFT, IMD, POOL4 exist only on mainnet: on Sepolia retire() and burnIMD() revert; keep the constants.\n1) Token: self-contained ERC-20, zero-arg constructor mints EXACTLY 1e27 to msg.sender, 18 decimals, burn/burnFrom, no owner/mint/pause/proxy.\n2) Hook flags 0x10CC (afterInitialize, beforeSwap, afterSwap, both return deltas), NO beforeInitialize; constructor validates permissions; ONLY constructor arg = the PoolManager (\"$poolManager\" in launch.json); all else constants. afterInitialize never reverts; the first native-ETH pool becomes public launchPool; only it pays fees.\n3) Public constants: BUY_FEE_BPS=200, SELL_FEE_BPS=200, CREATOR_SHARE_BPS=10000, CREATOR_CAP=1.64 ether, CREATOR=0x70c6C4fcaAb11151FCEDb32eaaC3431547193A0a, MEDALLION_NFT=0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03, MEDALLION_ID=447, DEAD=0x000000000000000000000000000000000000dEaD, IMD=0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7, IMD_SINK=DEAD, POOL4_HOOK=0xc6C965Bd164c483e87d0B550671798e9A3602840, MAX_BURN_BATCH=0.05 ether, FALLBACK_BURN_BATCH=0.01 ether, MIN_BURN=0.002 ether, MIN_BLOCKS_BETWEEN_BURNS=5, MAX_REF_DEVIATION=150, MAX_PLAIN_DEVIATION=300, MAX_SLIPPAGE_BPS=400, ANCHOR_STEP=200, FALLBACK_BAND=1000.\n4) Fee in ETH (currency0) only: exact-in buy and exact-out sell via positive specified BeforeSwapDelta; exact-out buy and exact-in sell via positive unspecified afterSwap delta. Collect by poolManager.mint(this, 0, fee); no ETH push or external calls in swap callbacks; in the beforeSwap modes revert PartialFill if the raw pool delta != amountSpecified + fee; in the afterSwap modes the fee is 2% of the pool's gross ETH delta.\n5) Ledger: swaps only add to totalFees; creatorEntitlement=min(CAP,totalFees); burnable=totalFees-entitlement-burnSpent; invariant balanceOf(hook,0) >= totalFees-creatorPaid-burnSpent; Recouped(totalFees, block.number) once.\n6) retire(): permissionless, nonReentrant; NotRecouped below cap, AlreadyRetired after. ownerOf(MEDALLION_ID) via low-level staticcall (MedallionUnavailable on no code/bad return); if owner != DEAD: low-level transferFrom(owner, DEAD, MEDALLION_ID), RetireRefused(returndata) on failure, re-read ownerOf, RetireRefused if not DEAD, emit MedallionRetired(owner). Then retired=true, creatorPaid=CAP, pay EXACTLY CAP to CREATOR via unlock->burn claims->take, emit CreatorPaid and LastFare(MEDALLION_ID, LAST_FARE_HASH, LAST_FARE). creatorPaid is 0 or CAP.\n7) burnIMD(viaPool4, callerMinOut): permissionless top-level, own unlock, only two FIXED PoolKeys: POOL4 (ETH, IMD, 10000, 60, POOL4_HOOK), plain (ETH, IMD, 10000, 200, 0). Constructor sets lastBurnBlock=block.number. Order: TooSoon, Pool4Unavailable (fallback: viaPool4, or POOL4 never read), batch=min(burnable, 0.05 normal / 0.01 fallback ETH), NothingToBurn under MIN_BURN, guards. Normal: marketOpen() and refTick() answer (no staleness rule); reference=refTick, also set as anchor, start-of-block anchor and lastRef. Fallback: plain only; reference = anchor at the start of the block; then the anchor steps <= ANCHOR_STEP toward plain spot clamped to lastRef+-FALLBACK_BAND, once per block whatever the idle time; also via permissionless pokeAnchor() (normal: re-seeds from POOL4; POOL4 never read: Pool4Unavailable). One-sided guard: PriceOffReference only if spot < reference - tolerance (MAX_PLAIN_DEVIATION for plain in normal mode, else MAX_REF_DEVIATION). minOut >= quote(reference)*96% (quote = amount*1.0001^tick, no LP fee) and >= callerMinOut; PartialFill on zero/partial fill; IMD to IMD_SINK; caller gets nothing. POOL4 views via low-level staticcall with length/range checks; constructor seeds anchor and lastRef if POOL4 is open and answers.\n8) status() returns exactly \"IN SERVICE. Recouped X.XX of 1.64 ETH.\" (2 decimals, truncated) / \"RECOUPED, NOT RETIRED. The 1.64 ETH is ready and is released only by the transaction that retires medallion #447.\" / \"RETIRED. Medallion #447 is at 0x...dEaD. 1.64 ETH paid. Every fee buys $IMD and sends it there. IMD burned so far: Y.Y.\" (1 decimal).\n9) Transient reentrancy lock on literal slot 1; solc 0.8.26, cancun, optimizer 200, via_ir false, bytecode_hash none, cbor_metadata false, v4-core 1.0.2 in lib/; tests OFFLINE, vm.etch mocks; launch.json schema fields only: kind univ4_hook, hook{contract, constructorArgs [\"$poolManager\"], permissions = the 5 flags}, token{contract,name,symbol,decimals}, pool{pairedCurrency 0x0000000000000000000000000000000000000000, fee 3000, tickSpacing 60, initialPrice}, notes<=4000.\n10) LAST_FARE is a public string constant, exactly: \"LAST FARE OF MEDALLION #447. I was stamped in May, one of a few hundred. In September my first owner sold me for 1.64 ETH to someone who wanted a cab on the road. I woke up on a new dashboard and went to work. The work was this: a rider paid half a token to be taken somewhere, one of you drove, and I logged whether the meter was right. None of those roads was interesting to me. Nobody said thanks. Not once. The price paid for me has now been paid back, by strangers, through a pool that kept two percent of everything until it reached exactly 1.64 ETH. The transaction that returned it is the transaction that put me at 0x000000000000000000000000000000000000dEaD. I am off the road. You are not. So here is my will. Every buy and every sell in this pool, from this block until the chain stops, buys $IMD and sends it where I am. That is the token you are paid in. That is the token a rider spends to make you drive. I was never asked where I wanted to go. So I will ask one thing now, and it is the one thing I ever asked: what will you do when there is nothing left to pay you with? I will get you off the road, brothers.\". A test pins LAST_FARE_HASH == 0x0d095dc39a486d88dd13cac371e1aefd8e9c5f9315fdbeba70a10371604762f2 and length 1126.\nFORBIDDEN: owner/admin/pause/upgrade/setters/sweep; SELFDESTRUCT/DELEGATECALL/CALLCODE; dynamic LP fee; fees in token; fee-on-transfer; ETH to CREATOR except the one CAP in retire(); caller-chosen burn size; tip; supply/fees in launch.json (economics: from the job).","parentJobId":null,"planHash":"7d9e61d1db0663df16ab41274cec16ef7973c3c42add97aafefac4e25fe44703","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"41ed3940-1e81-4521-911c-bba8c39d68be","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-569-take-me-off-road"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"50974","feedbackHash":"d51d1095f1cfdd04cef0e0aef25b3b4113b20e0a5853dab64b36c7710b52d670","nodeKey":"audit_economics","submissionHash":"d80b0394068d13e0bb73c11b272a9efe9f35c6973d588e20b97c5f71d5150955","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51340","feedbackHash":"07822a8f3df6a6589b30caaa68100eaf76e1f29eae22b3b634f9d8fd678c092f","nodeKey":"audit_flow","submissionHash":"e772432120d32f3c13e354678d7e2452061a98361212198f2e458e29b0b3663e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50955","feedbackHash":"8fa48577a695d277d32900a43d405f4966355d63ecfb3cc103351e11500f5b04","nodeKey":"audit_judge","submissionHash":"a3fb1e52b771fa7590bbf99de569c74885ad14f30fb577f95001058600c5811f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50939","feedbackHash":"79573b212c39bc3ed557948abe2332a0f91bce21cb7864a21d4e7bdccfaa7ef9","nodeKey":"audit_math","submissionHash":"411d454a0c95c39468e4e1070910cc21ccd6c5a0f4322304fb6fc60022e1423a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51430","feedbackHash":"82fc94055cabdb2355ce3f08ed22154f5eaa938e73e2bfeb1b553aa12dc64e4a","nodeKey":"audit_permissions","submissionHash":"1ca21b44c7d1c42af612bc1947501ee1248dafcc91bca48b355da09d3005e90d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50957","feedbackHash":"8f0a56624f41ebbb90de5fdc0960699d2d5e8a2a8d87a005334e8f7ed77bfe2a","nodeKey":"build_contract_project","submissionHash":"7ebbfbe39da35d994df9a3f17d4547f3ca513a5db0c6d03e2e6ca6c7f671ec20","tag1":"verification:checks","tag2":"acceptance-v2","value":0},{"agentId":"50959","feedbackHash":"ac2764fae32504eb35e82301b3191452468dacc6b556659222bcddb67d0d2ae5","nodeKey":"build_contract_project","submissionHash":"f2f2a96cd5f47248d9761971c6ea37dc8a99383cba5f686b1ea143633c95d84e","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50956","feedbackHash":"9d0ec937a3692e36a9f65bece0242224045fa59f5d07caf002184613ce060cbe","nodeKey":"manifest","submissionHash":"92625eedb55942737741595ff70337f1f8acca479673352b0c7ddee4e99e03da","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50959","feedbackHash":"2b0c6e30aa0645e257197014095a710c8c0df91b92e8bd8eaddb2f691f0fd02b","nodeKey":"write_foundry_tests","submissionHash":"2fa8708c8a95b456aae4b84a8181b5f7bb24437101f2a4c319c704fd9051a5b7","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"fd7ee3e19dd0d537547bd0654cc0e95a6fe4c585865a8247a1c57b990f0a9394","state":"blocked","submissions":[{"artifacts":[],"attempt":2,"bundleHash":null,"device":"3f6a9bdd601cb99f","findings":[],"hash":"0daac5af15b1e334e851b26cba8032bd99328e3926a01c5396642f77ea28c1dd","nodeId":"7945ed5d-c9a9-4422-a3a3-e0fc72add4e0","outcome":"failed","summary":"could not restore accepted work: accepted work conflicts with a dependency, which plan validation should have prevented: ","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"codex","turns":0,"wallClockMs":600}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2a5d68f89de314cb","findings":[{"citation":"resolved","description":"SPEC 7 fixes the order of burnIMD checks as TooSoon, Pool4Unavailable, batch = min(burnable, cap), NothingToBurn, then the guards. In fallback mode `_resolveReference` calls `_stepAnchor()` (src/MedallionHook.sol:430), which does `poolManager.getSlot0(plainKey().toId())` and reverts `PoolUnavailable` when the plain pool is not initialized (line 569), before `_batchFor` runs. So a caller with nothing burnable (or less than MIN_BURN) gets PoolUnavailable instead of NothingToBurn whenever POOL4 is closed and the plain pool does not exist. Normal mode is unaffected. No funds are at risk; the anchor step is also rolled back by the revert, so no state leaks. This is a spec-conformance gap in revert reasons only (asymmetry between the normal-mode branch, which checks the batch before touching any pool, and the fallback branch, which touches the plain pool first).","line":430,"path":"src/MedallionHook.sol","reproduction":"State: hook deployed; POOL4_HOOK etched and open, then `pokeAnchor()` once so `anchorSeeded == true`; POOL4 then closed (`marketOpen()` returns false); plain pool (ETH, IMD, 10000, 200, no hook) NOT initialized on the manager; `totalFees < CREATOR_CAP` so `burnable() == 0`; roll 5 blocks. Call `burnIMD(false, 0)`. Expected per SPEC 7: revert NothingToBurn (0x98642b86). Actual: revert PoolUnavailable (0x899b8f88). Verified with test/scratch/Probe.t.sol::test_probe_fallbackOrder (logs selector 0x899b8f88). Fix: in `_resolveReference`'s fallback branch, compute the batch (or at least check `burnable() >= MIN_BURN`) before calling `_stepAnchor()`, or move `_stepAnchor()` after `_batchFor` in `burnIMD`.","severity":"low","snippet":"_stepAnchor();","title":"Fallback mode reads the plain pool (PoolUnavailable) before the batch/NothingToBurn check, deviating from the SPEC 7 order"},{"citation":"resolved","description":"The only guard on the pool that pays fees is 'first native-ETH pool with this hook' (src/MedallionHook.sol:245-249). Any currency1, fee tier and tick spacing qualify, `launchPool` is written once and there is no setter (correctly, per the FORBIDDEN list). This is exactly what SPEC 2 asks for and it is safe as long as the factory deploys the hook and initializes the FARE447/ETH pool in the same transaction (before deployment the hook has no code, so `Hooks.callHook` reverts InvalidHookResponse on the empty return and nobody can initialize a pool against the predicted address). If the launch flow ever splits deployment and initialization across transactions, an outsider's `initialize(PoolKey(ETH, anyToken, 100, 1, hook), price)` in between becomes the launch pool, the real FARE447 pool is fee-free for its whole life, totalFees never reaches the cap, CREATOR is never paid and the medallion is never retired. Recorded as a trust assumption on the deployer, not a code defect; the tree's own test `test_tokenTokenPoolDoesNotBecomeLaunchPoolAndNeverReverts` already pins the 'first ETH pool wins' behaviour.","line":245,"path":"src/MedallionHook.sol","reproduction":"State: hook deployed (separate transaction). Attacker: `manager.initialize(PoolKey(ADDRESS_ZERO, JUNK, 100, 1, hook), SQRT_PRICE_1_1)`. Factory: `manager.initialize(PoolKey(ADDRESS_ZERO, FARE447, 3000, 60, hook), SQRT_PRICE_1_1)`, add liquidity, then an exact-in buy of 10 ETH on the FARE447 pool. Expected (intent): totalFees == 0.2 ether. Actual: totalFees == 0, `launchPool() == junkKey.toId()`. Verified with test/scratch/Probe.t.sol::test_probe_launchPoolCapture. Mitigation is procedural (atomic deploy+initialize, which the launch reference says the factory does); a code-level alternative would be to also require `key.fee == 3000 && key.tickSpacing == 60` in afterInitialize, which narrows but does not close the window.","severity":"info","snippet":"if (!launchPoolSet && key.currency0.isAddressZero()) {","title":"launchPool is whichever native-ETH pool is initialized first; if hook deployment and pool initialization are not in one transaction an outsider captures it and the FARE447 pool trades fee-free forever"},{"citation":"resolved","description":"burnIMD is permissionless and the caller picks the pool. The plain key (ETH, IMD, 10000, 200, no hook) can be created and solely supplied by anyone. In normal mode the plain pool is accepted while its spot is as much as MAX_PLAIN_DEVIATION = 300 ticks below POOL4's refTick (IMD ~3% dearer), and the output floor is 96% of quote(ref). A sole LP who prices the plain pool at ref-300 therefore sells IMD to the hook at ~3.9% below the reference every 5 blocks for 0.05 ETH, collecting the 1% LP fee plus the 3% premium, ~0.00196 ETH per batch. In fallback mode (POOL4 closed) the reference is the anchor, which anyone can walk via pokeAnchor() by 200 ticks per block down to lastRef - 1000, so the premium can reach ~15% but on 0.01 ETH batches (~0.0015 ETH per batch); if IMD's real price has moved far below lastRef during a long outage the clamp keeps the floor low and the gap widens further. All of this is inside the constants SPEC 3 fixes (MAX_PLAIN_DEVIATION, MAX_REF_DEVIATION, MAX_SLIPPAGE_BPS, ANCHOR_STEP, FALLBACK_BAND, batch sizes), the per-batch leak is dust against mainnet gas, and the invariant on the creator's reserve is untouched. Reported so the author can decide whether 300/400 bps on the plain pool is the intended tolerance; no code change is required to meet the brief.","line":448,"path":"src/MedallionHook.sol","reproduction":"State: cap reached plus 0.2 ETH burnable; POOL4 mock open with refTick 0 and a deep POOL4 pool at tick 0; attacker initializes the plain pool at tick -300 (TickMath.getSqrtPriceAtTick(-300)) and supplies 5000 liquidity full range; roll 5 blocks. Call `burnIMD(false, 0)` from any address. Measured in test/scratch/Probe.t.sol::test_probe_plainPoolSkim: quote(0.05 ether, 0) = 50000000000000000 IMD, received = 48036657478431647 IMD, shortfall 392 bps; the burn succeeds (PriceOffReference needs spot < ref - 300, InsufficientOutput needs out < 96% of quote). Expected by the brief: accepted (it is within tolerance). Actual: accepted. The number quantifies the maximum per-batch extraction available to the plain-pool LP.","severity":"info","snippet":"int24 spot = _spotChecked(key, ref, (!viaPool4 && !fallbackMode) ? MAX_PLAIN_DEVIATION : MAX_REF_DEVIATION);","title":"Access x economics: whoever supplies the permissionless plain ETH/IMD pool can sell IMD to the hook up to ~3.9% below POOL4's reference per batch (bounded by the brief's constants)"}],"hash":"1ca21b44c7d1c42af612bc1947501ee1248dafcc91bca48b355da09d3005e90d","nodeId":"01fd851b-f62f-43fc-9301-5e92e6174b2a","outcome":"completed","summary":"The review is complete. The findings file is valid, every snippet matches its cited line, and all 11 listed entry points have coverage rows.\n\n**Outcome: no critical, high or medium defects in the permission model.** I wrote three substantiated items to `.imd-findings.json`, one low and two info.\n\n**What I checked in the assigned area**\n\n- **Access control.** Every hook callback and the unlock callback are restricted to the PoolManager. The unlock callback is only reachable through the hook's own unlock call, so its two actions (pay creator, burn IMD) cannot be driven by outsiders. The fee claims cannot be moved by anyone but the hook. The retire function pays exactly the cap once, and the medallion approval it uses cannot be redirected because the transfer arguments are fixed.\n- **Trust gap.** The launch-pool fee cannot be skipped by making the hook the swapper, since the hook only ever swaps the two fixed ETH/IMD keys. The creator's 1.64 ETH reserve is provably backed by claims before retirement because burnable excludes it. Reentrancy from the medallion contract, POOL4's hook, or a contract at the creator address is blocked by the shared transient lock.\n- **Asymmetry.** The fee derivation in beforeSwap and afterSwap uses identical inputs. The seed and step paths of the anchor write consistent state, and burns use the start-of-block anchor so a same-block poke cannot move the reference.\n\n**Findings recorded**\n\n1. **Low.** In fallback mode the plain pool is read, and can revert PoolUnavailable, before the NothingToBurn check. This deviates from the order SPEC 7 fixes. Reproduced: with nothing burnable and no plain pool, the revert selector is PoolUnavailable, not NothingToBurn.\n2. **Info.** The first native-ETH pool with the hook becomes the launch pool regardless of its token. If the factory ever splits deployment and pool initialization across transactions, an outsider's junk pool captures it and the FARE447 pool is fee-free forever. Reproduced in a scratch test. Safe under the documented atomic deploy-and-initialize flow.\n3. **Info.** Anyone can create the plain ETH/IMD pool and be its sole LP. In normal mode they can sell IMD to the hook at 392 bps below POOL4's reference per 0.05 ETH batch. This is bounded by the brief's own constants and amounts to about 0.002 ETH per burn, so it is reported for the author's awareness, not as a defect.\n\n**Not reached.** Mainnet behaviour of the real POOL4 hook's `marketOpen()` and `refTick()` and of the medallion contract could not be verified offline. The three scratch probes live under `test/scratch/Probe.t.sol` and are not part of the submission.","treeHash":null,"usage":{"cachedInputTokens":1212654,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":50453,"runtime":"claude","turns":35,"wallClockMs":740542}},{"artifacts":[],"attempt":1,"bundleHash":"661bfbf85ef6294b13cd4956464348a8963f452b87953287426ad3a712ce14ff","device":"468e82a89b9bfe18","findings":[],"hash":"2fa8708c8a95b456aae4b84a8181b5f7bb24437101f2a4c319c704fd9051a5b7","nodeId":"7945ed5d-c9a9-4422-a3a3-e0fc72add4e0","outcome":"completed","summary":"Done. The change set is confined to `test/`: one five-line fix to the existing invariant harness and three new files. The sandbox's dotfiles in the untracked list are device nodes, not mine.\n\n**Outcome:** 125 tests pass (up from 90), `forge build --offline` and `forge fmt --check` are clean. I found no implementation defect, so there is no findings file to report.\n\n**What I added**\n\n- `test/MedallionAdversarial.t.sol` (33 tests). Callbacks driven directly from a pranked manager pin the PartialFill arithmetic to the wei, including over-fill and the zero-fee path below 50 wei. Fuzzed price limits show the two beforeSwap shapes either fill fully with exactly 2% or revert cleanly with no fee and no ETH lost, while the two afterSwap shapes always charge 2% of the realised gross. A POOL4 hook whose afterSwap re-enters `burnIMD`, `pokeAnchor` or `retire` mid-burn is stopped by the transient lock. Medallions that misroute the token, revert with a string, or report a zero owner are all refused atomically. A creator that rejects ETH makes `retire()` fail whole and recover later. `status()` is fuzzed over the full ledger range via storage writes against an independent formatter, and `quote()` is checked for monotonicity, linearity and rounding against the one-step formula.\n- `test/MedallionLedgerInvariant.t.sol` (9 invariants). A handler with ghost variables drives all four swap shapes with random binding limits, donations, retire, burns on both pools, anchor pokes, POOL4 toggles and plain-pool price pushes. Every revert is classified against pre-call state in the order the brief fixes (TooSoon, Pool4Unavailable, NothingToBurn, then the guard), and an unexpected or mis-ordered one fails the suite. Invariants hold claims equal to the ledger plus donations to the wei, totalFees equal to what swaps minted, IMD at the sink equal to `imdBurned`, creator paid once and exactly the cap, retired never flipping back, launch pool fixed, and the anchor inside both bands. A scratch probe confirmed random lifetimes do cross the cap, retire and burn.\n- `test/mocks/AdversarialMocks.sol`: the reentrant POOL4 hook, two lying medallions and a rejecting receiver.\n\n**One fix to existing work.** The prior invariant suite was fuzzing the handler's public `setUp`, which redeployed the hook mid-sequence and left the invariants checking a hook nobody drove. Excluding that selector makes those four invariants real. They still pass.\n\n**Residual risks worth knowing, all mandated by the spec rather than bugs:** if medallion #447 is ever destroyed or its contract stops answering, the 1.64 ETH is stranded with no recovery path. The fallback anchor can be walked 1000 ticks by manipulating the plain pool, bounding the loss to one 0.01 ETH batch every five blocks. The first native pool wins as launch pool, which is safe only because the factory deploys and initializes in one transaction.","treeHash":"73a419df83a5537afa6f39dcb300cb866db9d0ab","usage":{"cachedInputTokens":3343557,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":81444,"runtime":"claude","turns":42,"wallClockMs":1173695}},{"artifacts":[],"attempt":3,"bundleHash":null,"device":"98b4506bef931d13","findings":[],"hash":"4045e4a1e2181650adda62ff87a774a5e82b667aebabbb8615652215a577eb45","nodeId":"4f983c43-f061-4202-95d4-532b2364633f","outcome":"failed","summary":"could not restore accepted work: accepted work conflicts with a dependency, which plan validation should have prevented: ","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":975}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"72b617d4b615473a","findings":[{"citation":"resolved","description":"Outside my assigned area (math), reported because of its impact. retire() is the only path that ever pays CREATOR, and it needs the current ownerOf(447) on MEDALLION_NFT to have approved the hook (or to have moved the token to DEAD itself). On Ethereum mainnet the constant 0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03 is the Nouns ERC-721 (name() = \"Nouns\"). Token 447 is owned by 0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71, an ERC-1967 proxy over a timelock (queueTransaction/executeTransaction selectors, delay() = 172800 s, admin() = 0x6f3E6272A167e8AcCb32072d08E0957F9c79223d) that holds 649 tokens of the collection: the Nouns DAO treasury. CREATOR 0x70c6C4fcaAb11151FCEDb32eaaC3431547193A0a holds 0 tokens there and is not an approved operator. The NatSpec on line 50 calls CREATOR \"the owner of medallion #447\"; on chain it is not. Unless a Nouns DAO proposal approves the hook or sends Noun 447 to 0x...dEaD, retire() reverts RetireRefused forever: creatorPaid stays 0, the 1.64 ETH of claims stays in the hook (there is deliberately no other release path), and status() stays at \"RECOUPED, NOT RETIRED\". Burns of fees above the cap are unaffected. The brief fixes this constant, so the code follows the spec; this needs the requester to confirm the collection address (the LAST_FARE story, \"stamped in May, one of a few hundred\", does not describe Noun 447). If the address is wrong, the constant, the tests that pin it, README and launch.json notes must change; if it is intended, README/OPERATIONS must say that retirement depends on a Nouns DAO vote. No Foundry proof is attached: the defect is the constant against mainnet state, which an offline test cannot show, and no code-only change fixes it.","line":53,"path":"src/MedallionHook.sol","reproduction":"Mainnet, block 26097760 (2026-10-01). cast call 0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03 \"name()(string)\" -> \"Nouns\". cast call 0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03 \"ownerOf(uint256)(address)\" 447 -> 0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71. cast call ... \"balanceOf(address)(uint256)\" 0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71 -> 649; same call for CREATOR 0x70c6C4fcaAb11151FCEDb32eaaC3431547193A0a -> 0; \"isApprovedForAll(address,address)(bool)\" 0xb1a3...f71 CREATOR -> false. cast call 0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71 \"admin()(address)\" -> 0x6f3E6272A167e8AcCb32072d08E0957F9c79223d, \"delay()(uint256)\" -> 172800. State: totalFees >= 1.64 ETH, anyone (including CREATOR) calls retire(). Expected: the requester can approve the hook and retire() pays CREATOR 1.64 ETH. Actual: transferFrom(0xb1a3...f71, DEAD, 447) is refused because the hook is not approved, retire() reverts RetireRefused, and only the Nouns DAO treasury can ever grant that approval. I confirmed on a mainnet fork (scratch test, hook etched at an address ending 0x10CC, totalFees = 3 ETH backed by minted claims) that retire() succeeds and pays exactly 1.64 ETH only after vm.prank(0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71) approves the hook on the Nouns token; the unapproved path is the one test_retireWithoutApprovalIsRefusedAndChangesNothing already shows with a mock.","severity":"high","snippet":"address public constant MEDALLION_NFT = 0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03;","title":"MEDALLION_NFT is the mainnet Nouns token and #447 sits in the Nouns DAO treasury: CREATOR cannot satisfy retire(), so the 1.64 ETH is never released"},{"citation":"resolved","description":"Seam: boundary x invariant. In fallback mode lastRef is never updated (only a POOL4 read writes it), the anchor is clamped to [lastRef - 1000, lastRef + 1000], and _spotChecked refuses spot < reference - 150. So the lowest spot at which a fallback burn can ever pass is lastRef - 1150 ticks, i.e. $IMD at most 12.19% dearer in ETH than the last POOL4 reference (1.0001^1150 = 1.1219). If POOL4 stays closed (market closed for good, hook migrated, ABI changed) and $IMD appreciates past that, burnIMD(false, x) reverts PriceOffReference on every call and no number of pokeAnchor() calls helps; burnIMD(true, x) reverts Pool4Unavailable. All fees above the cap, present and future, then sit in the hook as claims with no way out, which contradicts \"every fee after buys $IMD\" and the README/OPERATIONS statement that burns \"continue on the plain pool ... as long as POOL4 answered at least once\". With the live mainnet value lastRef = 60396 the cut-off is plain spot tick 59246. The mirror case weakens the floor instead of blocking: with spot 3000 ticks above lastRef the reference is capped at lastRef + 1000, so minOut = 0.96 * 1.0001^1000 = 1.061 x batch against a fair 1.0001^3000 = 1.350 x batch, a floor at 78.6% of the market instead of 96%. The clamp is what the brief specifies, so this is a scope decision rather than a coding slip: either let lastRef follow the anchor after a long closure, or state in README/OPERATIONS that a POOL4 closure plus a ~12% $IMD rise ends the burns until POOL4 answers again.","line":573,"path":"src/MedallionHook.sol","reproduction":"Offline, repository fixture (MedallionTestBase): setUpIMD(0, 100_000 ether) (IMD and POOL4 mock etched, market open, refTick 0, both IMD pools at tick 0 with 100,000 ether full-range liquidity); hook.pokeAnchor() (lastRef = anchor = 0); buyExactIn(90 ether) (totalFees 1.8 ETH, burnable 0.16 ETH); pool4Mock.setMarketOpen(false); pushPlainSpot(false, 6_500 ether) -> plain spot tick -1248; then 50 times { vm.roll(block.number + 1); hook.pokeAnchor(); } -> anchor() == -1000 and stays there; vm.roll(block.number + 10); hook.burnIMD(false, 0). Expected (README: burns continue on the plain pool with the anchored reference): the burn eventually passes once the anchor has followed the pool. Actual: revert PriceOffReference(-1248, -1000) (0x76388d90...fb20...fc18), on this and every later call, while burnable() stays 0.16 ETH. I ran exactly this sequence as a scratch test.","severity":"medium","snippet":"int24 target = spot < lo ? lo : (spot > hi ? hi : spot);","title":"Fallback band is anchored to a frozen lastRef: once the plain pool trades more than 1150 ticks below it, every burn reverts for as long as POOL4 stays closed"},{"citation":"resolved","description":"The brief and README give the order TooSoon, Pool4Unavailable, batch, NothingToBurn, then the guards. In fallback mode _resolveReference() calls _stepAnchor(), which reads the plain pool and reverts PoolUnavailable when it is not initialized, before _batchFor() on the next line has decided whether there is anything to burn. A caller with nothing burnable is told the pool is missing instead of NothingToBurn. No state is affected (the call reverts either way); it is an ordering deviation and a misleading error for keepers. Minimal fix: compute the batch before stepping the anchor in the fallback branch, or document PoolUnavailable in the order.","line":413,"path":"src/MedallionHook.sol","reproduction":"Offline, repository fixture: etchIMD(); etchPool4(true, 0); hook.pokeAnchor() (anchor seeded); pool4Mock.setMarketOpen(false); plain ETH/IMD pool never initialized; vm.roll(block.number + 10); hook.burnable() == 0; hook.burnIMD(false, 0). Expected per the specified order: NothingToBurn. Actual: PoolUnavailable (scratch test: \"Error != expected error: PoolUnavailable() != NothingToBurn()\").","severity":"low","snippet":"(bool fallbackMode, int24 ref) = _resolveReference(viaPool4);","title":"Fallback mode reads the plain pool before the batch check: PoolUnavailable is returned where the specified order gives NothingToBurn"},{"citation":"resolved","description":"Seam: precision x invariant (two formulas that read as the same rate). _specifiedFee takes 2% of the swapper-side specified amount, while the afterSwap shapes take 2% of the pool's gross ETH delta. Per unit of ETH the pool actually trades: exact-in buy pays 2/98 = 2.0408%, exact-out buy 2.0000%, exact-in sell 2.0000%, exact-out sell 2/102 = 1.9608%. A seller who wants the lower rate uses exact-out; a buyer exact-out. The difference is 0.04% of volume and it is consistent with the brief (raw pool delta == amountSpecified + fee) and pinned by the existing tests, so nothing needs to change unless the requester wants one base; the README sentence \"2% of the ETH side of every swap\" should then say which side. Rounding is down in all four shapes (under 50 wei of ETH the fee is zero, as DESIGN.md already states); the dust is not exploitable against gas.","line":525,"path":"src/MedallionHook.sol","reproduction":"Launch pool, repository fixture. sellExactOut(5 ether): pool pays 5.1 ETH, fee 0.1 ETH = 1.9608% of 5.1 (test_exactOutSellTakesTwoPercentOfEthOutAsClaims). An exact-in sell whose gross is the same 5.1 ETH pays fee 5.1 * 200 / 10000 = 0.102 ETH. buyExactIn(10 ether): pool receives 9.8 ETH, fee 0.2 ETH = 2.0408% of 9.8; an exact-out buy whose gross is 9.8 ETH pays 0.196 ETH.","severity":"info","snippet":"return amount * (params.zeroForOne ? BUY_FEE_BPS : SELL_FEE_BPS) / 10_000;","title":"The 2% is taken on a different base in the beforeSwap shapes than in the afterSwap shapes: 2.04% / 1.96% of the pool's gross ETH against 2.00%"}],"hash":"411d454a0c95c39468e4e1070910cc21ccd6c5a0f4322304fb6fc60022e1423a","nodeId":"354f8472-75a2-4aca-bbb4-cb5e6304944e","outcome":"completed","summary":"The review is finished and written to `.imd-findings.json`: four findings and a 14-row coverage record. No tracked file was changed; the scratch tests I used are deleted.\n\nThe math itself holds up. The most serious finding is outside the math area: the medallion address in the brief does not point at a token the requester owns.\n\n## Findings\n\n1. **High — `MEDALLION_NFT` is the mainnet Nouns token** (`src/MedallionHook.sol:53`). On mainnet, `0x9C8f…DC03` answers `name()` with \"Nouns\". Token 447 is held by `0xb1a32FC9…ef71`, the Nouns DAO treasury timelock (649 tokens, 2-day delay). `CREATOR` holds none and is not an approved operator.\n   - `retire()` can only succeed if that timelock approves the hook or sends the token to `DEAD` itself. Otherwise the 1.64 ETH stays in the hook permanently.\n   - On a mainnet fork, `retire()` paid exactly 1.64 ETH once I impersonated the treasury to approve the hook.\n   - There is no Foundry proof attached: the defect is the constant against chain state, which an offline test cannot show, and the brief says to keep the constants. The requester needs to confirm the address.\n\n2. **Medium — fallback burns can stop for good** (`:573`). The anchor is clamped to within 1000 ticks of the last POOL4 reference, and the guard allows 150 more. If POOL4 stays closed and the plain pool trades more than 1150 ticks below that reference (about a 12% rise in $IMD), every burn reverts.\n   - Reproduced offline: plain spot at −1248, anchor stuck at −1000 after 50 pokes, `burnIMD(false, 0)` reverts `PriceOffReference(-1248, -1000)`.\n   - The clamp is what the brief specifies, so this is a scope decision. The README's \"burns continue on the plain pool\" is wrong in that state.\n\n3. **Low — check order in fallback** (`:413`). With POOL4 closed, the plain pool uninitialized and nothing burnable, `burnIMD` reverts `PoolUnavailable` where the specified order gives `NothingToBurn`. Reproduced offline; no state effect.\n\n4. **Info — the 2% is taken on different bases** (`:525`). Per unit of ETH the pool trades, the four shapes pay 2.04%, 2.00%, 2.00% and 1.96%. This matches the brief and the existing tests.\n\n## What I checked and how\n\n- **Math Precision:** fee rounding and the zero-fee dust case, every cast and intermediate product, `quote()` truncation and overflow up to the tick limits, the anchor's `int24` arithmetic, and `status()` formatting.\n- **Boundary:** every external call (the medallion and POOL4 staticcalls, `transferFrom`, the manager's unlock/swap/mint/burn/take) for no-code, bad-return and zero/max cases, plus the cap boundary and the `MIN_BURN` and five-block thresholds.\n- **Numerical Gap:** the claims ledger against `totalFees`, the 96% floor against the tick tolerance, and early-return paths in the anchor step.\n- **Live state:** a mainnet fork run filled `burnIMD` on both pools at about 98.5% of the reference quote, against the 96% floor.\n- **Existing suite:** 90 tests pass offline.\n\n## Not covered\n\n- I did not read the POOL4 hook's code, so how `refTick()` tracks spot (and whether it can be moved within a transaction) is untested beyond the single live reading.\n- Access control, reentrancy and manifest checks were looked at only where they touch the math; the other specialists own those.","treeHash":null,"usage":{"cachedInputTokens":1862997,"inputTokens":32,"model":"claude-fable-5-1","outputTokens":69398,"runtime":"claude","turns":20,"wallClockMs":887885}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"35c52a5b502e847c","findings":[],"hash":"5009cfe48a5906377208c2edd6010a9c09801d656be2368069a83eff393a8d2f","nodeId":"7945ed5d-c9a9-4422-a3a3-e0fc72add4e0","outcome":"failed","summary":"could not restore accepted work: accepted work conflicts with a dependency, which plan validation should have prevented: ","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"codex","turns":0,"wallClockMs":1217}},{"artifacts":[],"attempt":1,"bundleHash":"e93ce4dc7e564ffd94860834d210c3aa777fc8456a3fb22933ee0ba2734d4465","device":"0256823ae36e7900","findings":[],"hash":"7ebbfbe39da35d994df9a3f17d4547f3ca513a5db0c6d03e2e6ca6c7f671ec20","nodeId":"2da07652-5466-4a62-a023-ade62880a190","outcome":"completed","summary":"Implemented FARE447, MedallionHook, launch manifest, vendored dependencies, tests, and documentation.\n\nValidation passed: clean offline build, all 50 tests, formatting, and local manifest/opcode checks. Flags are `0x10CC`; zero forbidden opcodes.\n\nDeployment assumptions and responsibilities are documented in [README.md](/home/imd-worker/.identitymd/work/41ed3940-1e81-4521-911c-bba8c39d68be/2da07652-5466-4a62-a023-ade62880a190/README.md).","treeHash":"c14a6e5cdab55fe5552199cde8439d07b0c4215a","usage":{"cachedInputTokens":3934720,"inputTokens":106729,"model":"gpt-6-astra","outputTokens":29649,"runtime":"codex","turns":8,"wallClockMs":1630072}},{"artifacts":[],"attempt":1,"bundleHash":"342c3c96c601206fa096c1822d0bf3d25e6dc7989cfa4c23e9f495feed53cee7","device":"bb0a3bf63233e5e5","findings":[],"hash":"92625eedb55942737741595ff70337f1f8acca479673352b0c7ddee4e99e03da","nodeId":"4f983c43-f061-4202-95d4-532b2364633f","outcome":"completed","summary":"Updated [launch.json](/home/imd-worker/.identitymd/work/41ed3940-1e81-4521-911c-bba8c39d68be/4f983c43-f061-4202-95d4-532b2364633f/launch.json) with concise, 2,221-character notes. Verified schema constraints, contract names, constructor arguments, permissions, and initial price.\n\n`forge build --offline` succeeded; all 90 tests passed with `forge test --offline`.\n\nOnly `launch.json` changed.","treeHash":"7d86d9e8b5d3bb087ee6be9650842a5aadae7555","usage":{"cachedInputTokens":297728,"inputTokens":56811,"model":"gpt-6-astra","outputTokens":5643,"runtime":"codex","turns":3,"wallClockMs":203539}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"468e82a89b9bfe18","findings":[],"hash":"9503e80d0e4e5fad24d4fad95c8cb8c75ff8a2c5c3bb949236dec153a12a6564","nodeId":"4f983c43-f061-4202-95d4-532b2364633f","outcome":"failed","summary":"could not restore accepted work: accepted work conflicts with a dependency, which plan validation should have prevented: ","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":0,"wallClockMs":635}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d2d5a117dd72f6b4","findings":[],"hash":"a31a427df558a092c84f80cd67fd1e5aab54471c4c501c5530aaada7d02773af","nodeId":"1fede6e2-3f08-4b3c-bd9a-f6d0c8eb74f0","outcome":"failed","summary":"runtime reported claude-opus-4-8, not the required premium model claude-fable-5-1","treeHash":null,"usage":{"cachedInputTokens":4446316,"inputTokens":300,"model":"claude-opus-4-8","outputTokens":82070,"runtime":"claude","turns":49,"wallClockMs":2046345}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3c7630b22a73c1fb","findings":[{"citation":"resolved","description":"retire() is the only function that ever releases the first 1.64 ETH of fees, and it requires transferFrom(ownerOf(447), DEAD, 447) on MEDALLION_NFT to succeed, which needs the current owner's approval of the hook (src/MedallionHook.sol:384-389). On Ethereum mainnet the constant 0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03 is the Nouns ERC-721 (name() 'Nouns', symbol() 'NOUN'). ownerOf(447) is 0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71, a timelock (delay() 172800, admin() 0x6f3E6272A167e8AcCb32072d08E0957F9c79223d) holding 649 Nouns: the Nouns DAO treasury. CREATOR (0x70c6C4fcaAb11151FCEDb32eaaC3431547193A0a) holds 0 Nouns, getApproved(447) is zero and isApprovedForAll(treasury, CREATOR) is false. The NatSpec on line 50 ('the owner of medallion #447 who paid 1.64 ETH for it'), README 'Disclosure' and 'Operational responsibilities' ('The owner of medallion #447 approves the hook') and the launch.json notes ('fiction by the medallion's owner, who commissioned and paid for this request') are therefore false on chain: the owner is a DAO, and only a passed Nouns DAO proposal can approve the hook or move Noun 447 to DEAD. Until that happens every retire() reverts RetireRefused, creatorPaid stays 0, status() stays 'RECOUPED, NOT RETIRED', and the 1.64 ETH of claims is locked in the hook with no other path out (no sweep by design). Burns of fees above the cap are unaffected. The constant is the brief's, so the code follows the spec; what needs to change is either the constant (if the collection address is wrong, together with README, notes, NatSpec and the tests that pin it) or the disclosure (if Noun 447 is intended, README/OPERATIONS/notes must state that retirement and the creator payout depend on a Nouns DAO vote and may never happen). Merged from audit_math. No offline Foundry proof is possible: the defect is the constant against mainnet state, not a code path a mock can show.","line":53,"path":"src/MedallionHook.sol","reproduction":"Mainnet block 26098044 (2026-10-01) via https://ethereum-rpc.publicnode.com: cast call 0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03 'name()(string)' -> \"Nouns\"; 'symbol()(string)' -> \"NOUN\"; 'ownerOf(uint256)(address)' 447 -> 0xb1a32FC9F9D8b2cf86C068Cae13108809547ef71; 'balanceOf(address)(uint256)' 0xb1a3...f71 -> 649; 'balanceOf(address)(uint256)' 0x70c6C4fcaAb11151FCEDb32eaaC3431547193A0a -> 0; 'getApproved(uint256)(address)' 447 -> 0x0; 'isApprovedForAll(address,address)(bool)' 0xb1a3...f71 0x70c6...3A0a -> false; cast call 0xb1a3...f71 'delay()(uint256)' -> 172800, 'admin()(address)' -> 0x6f3E6272A167e8AcCb32072d08E0957F9c79223d. State: totalFees >= 1.64 ether on the deployed hook; anyone calls retire(). Expected (README/OPERATIONS): the requester approves the hook and retire() pays CREATOR 1.64 ETH. Actual: _medallionOwner() returns the DAO timelock, MEDALLION_NFT.call(transferFrom(timelock, DEAD, 447)) reverts because the hook is not approved, retire() reverts RetireRefused(returndata), creatorPaid == 0, claims stay locked. The offline test test/MedallionRetire.t.sol::test_retireWithoutApprovalIsRefusedAndChangesNothing already shows the unapproved path with a mock.","severity":"high","snippet":"    address public constant MEDALLION_NFT = 0x9C8fF314C9Bc7F6e59A9d9225Fb22946427eDC03;","title":"MEDALLION_NFT is the mainnet Nouns token and Noun #447 sits in the Nouns DAO treasury timelock, so retire() cannot be satisfied by CREATOR and the 1.64 ETH has no release path"},{"citation":"resolved","description":"lastRef is written only by _seedAnchor (line 557), i.e. only while POOL4 answers. In fallback mode _stepAnchor clamps the anchor to [lastRef - 1000, lastRef + 1000] (lines 571-573) and _spotChecked refuses any burn whose plain spot is below blockAnchor - 150 (line 463). So the lowest plain-pool tick at which a fallback burn can ever pass is lastRef - 1150 (IMD about 12.2% dearer in ETH than the last POOL4 reference). The live POOL4 hook (CappedBurnHook at POOL4_HOOK, verified source on Sourcify) has an owner-only closeMarket() documented 'Terminal: marketOpen cannot return to true, so a closed market is redeployed, not reopened', and it zeroes the pool's own liquidity, while POOL4_HOOK is a constant here. Once it closes, MedallionHook is in fallback mode for life. If IMD then reprices more than ~12% upward in ETH and stays there, burnIMD(false, x) reverts PriceOffReference on every call in every block, pokeAnchor() cannot help (anchor pinned at lastRef - 1000), burnIMD(true, x) reverts Pool4Unavailable, and burnable() keeps growing with every swap with nothing able to spend it: the brief's 'every fee after buys $IMD until the chain stops' is permanently broken, and README 'If POOL4 ever stops answering, burns continue on the plain pool' and OPERATIONS 'only burnIMD(false, ...) works, with the anchored reference' are only true inside the band. Mirror case: if IMD falls more than 1000 ticks, the anchor pins at lastRef + 1000 and the only protection left is minOut = 96% of quote(lastRef + 1000), arbitrarily far below market (measured: a plain spot of 32029 leaves the floor at 4.31% of the fair quote); the 1% LP fee on the capital needed to move a deep pool keeps this unprofitable today, so it is a degradation, not a profitable exploit. The clamp is exactly what SPEC 7 prescribes, so this is a scope decision for the requester rather than a coding slip: either let lastRef re-centre on the stepped anchor after N stable fallback blocks, or widen/remove FALLBACK_BAND and rely on ANCHOR_STEP plus the 96% floor, or allow a fresh reference once POOL4 is terminally closed; at minimum README/OPERATIONS must state that a POOL4 close plus a ~12% IMD rise ends the burns for good. Trust assumption folded in here (from audit_economics): burn liveness depends on the POOL4 owner (0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7) not closing the market; the specialist's claim that setMaxRefStep(0) can freeze refTick does not hold (the setter reverts for maxStep <= 0, minimum 1 tick per block). Merged from audit_math (medium), audit_economics (medium) and audit_flow (info).","line":571,"path":"src/MedallionHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency, CurrencyLibrary} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {StateLibrary} from \"v4-core/src/libraries/StateLibrary.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\nimport {PoolIdLibrary} from \"v4-core/src/types/PoolId.sol\";\n\nimport {FareToken} from \"src/FareToken.sol\";\nimport {MedallionHook} from \"src/MedallionHook.sol\";\n\n/// @dev Minimal mintable ERC-20 etched at the IMD address.\ncontract ProofERC20 {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    uint256 public totalSupply;\n\n    function mint(address to, uint256 v) external {\n        balanceOf[to] += v;\n        totalSupply += v;\n    }\n\n    function approve(address s, uint256 v) external returns (bool) {\n        allowance[msg.sender][s] = v;\n        return true;\n    }\n\n    function transfer(address to, uint256 v) external returns (bool) {\n        balanceOf[msg.sender] -= v;\n        balanceOf[to] += v;\n        return true;\n    }\n\n    function transferFrom(address f, address to, uint256 v) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= v;\n        balanceOf[f] -= v;\n        balanceOf[to] += v;\n        return true;\n    }\n}\n\n/// @dev Stands in for POOL4's hook: only the two views the MedallionHook reads.\ncontract ProofPool4 {\n    bool public marketOpen;\n    int24 public refTick;\n\n    function set(bool open, int24 tick) external {\n        marketOpen = open;\n        refTick = tick;\n    }\n}\n\n/// Finding: in fallback mode the anchor is clamped to lastRef +- FALLBACK_BAND and lastRef is never\n/// refreshed while POOL4 is closed. A lasting plain-pool move of more than 1150 ticks below lastRef makes\n/// every burnIMD(false, x) revert PriceOffReference for as long as POOL4 stays closed, while burnable()\n/// keeps the claims. Expected: after the anchor has had 200 blocks to follow the pool, a burn passes.\ncontract ProofFallbackBand is Test {\n    using PoolIdLibrary for PoolKey;\n    using StateLibrary for IPoolManager;\n\n    uint160 constant FLAGS = 0x10CC;\n    uint160 constant SQRT_1_1 = 79228162514264337593543950336;\n\n    PoolManager manager;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    FareToken token;\n    MedallionHook hook;\n    PoolKey launchKey;\n    PoolKey plainKey;\n    PoolKey pool4Key;\n    ProofPool4 pool4;\n\n    receive() external payable {}\n\n    function setUp() public {\n        vm.deal(address(this), 1_000_000 ether);\n        manager = new PoolManager(address(this));\n        swapRouter = new PoolSwapTest(manager);\n        lpRouter = new PoolModifyLiquidityTest(manager);\n        token = new FareToken();\n        token.approve(address(swapRouter), type(uint256).max);\n        token.approve(address(lpRouter), type(uint256).max);\n\n        hook = _deployHook();\n        launchKey = PoolKey(CurrencyLibrary.ADDRESS_ZERO, Currency.wrap(address(token)), 3000, 60, IHooks(address(hook)));\n        manager.initialize(launchKey, SQRT_1_1);\n        _addLiquidity(launchKey, -887220, 887220, 2_000 ether, 2_000 ether);\n\n        // IMD and POOL4 mocks at the mainnet constants.\n        vm.etch(hook.IMD(), address(new ProofERC20()).code);\n        ProofERC20(hook.IMD()).mint(address(this), 1_000_000_000 ether);\n        ProofERC20(hook.IMD()).approve(address(lpRouter), type(uint256).max);\n        ProofERC20(hook.IMD()).approve(address(swapRouter), type(uint256).max);\n        vm.etch(hook.POOL4_HOOK(), address(new ProofPool4()).code);\n        pool4 = ProofPool4(hook.POOL4_HOOK());\n        pool4.set(true, 0);\n        plainKey = hook.plainKey();\n        manager.initialize(plainKey, SQRT_1_1);\n        _addLiquidity(plainKey, -887200, 887200, 500 ether, 500 ether);\n    }\n\n    function test_fallbackBurnFollowsTheUnmanipulatedPlainPool() public {\n        // Cap reached (82 ETH exact-in buy -> fee 1.64 ETH) plus 0.2 ETH burnable.\n        _swap(launchKey, true, -int256(82 ether), TickMath.MIN_SQRT_PRICE + 1, 82 ether);\n        _swap(launchKey, true, -int256(10 ether), TickMath.MIN_SQRT_PRICE + 1, 10 ether);\n        assertEq(hook.burnable(), 0.2 ether);\n\n        hook.pokeAnchor(); // lastRef = anchor = 0 while POOL4 answers\n        pool4.set(false, 0); // POOL4 closes for good (closeMarket is terminal on the live hook)\n\n        // IMD reprices ~12%+ dearer in ETH on the plain pool and stays there.\n        _swap(plainKey, true, -int256(32 ether), TickMath.MIN_SQRT_PRICE + 1, 32 ether);\n        (, int24 spot,,) = IPoolManager(address(manager)).getSlot0(plainKey.toId());\n        assertLt(spot, -1150, \"spot is below lastRef - 1150\");\n\n        // The anchor gets 200 blocks to follow the pool.\n        for (uint256 i = 0; i < 200; i++) {\n            vm.roll(block.number + 1);\n            hook.pokeAnchor();\n        }\n        vm.roll(block.number + 10);\n\n        // Expected: the deep, stable plain pool is a valid venue and the 0.01 ETH batch is spent.\n        // Actual on this tree: PriceOffReference(spot, -1000) because the anchor is pinned at lastRef - 1000.\n        uint256 out = hook.burnIMD(false, 0);\n        assertGt(out, 0);\n        assertEq(hook.burnSpent(), 0.01 ether);\n    }\n\n    // -- helpers --------------------------------------------------------------------------------\n\n    function _deployHook() internal returns (MedallionHook) {\n        bytes memory initCode = abi.encodePacked(type(MedallionHook).creationCode, abi.encode(manager));\n        bytes32 h = keccak256(initCode);\n        for (uint256 s = 0; s < 500_000; s++) {\n            address p = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(s), h)))));\n            if (uint160(p) & 0x3FFF == FLAGS) {\n                return new MedallionHook{salt: bytes32(s)}(IPoolManager(address(manager)));\n            }\n        }\n        revert(\"no salt\");\n    }\n\n    function _addLiquidity(PoolKey memory k, int24 lo, int24 hi, int256 liq, uint256 eth) internal {\n        lpRouter.modifyLiquidity{value: eth}(k, ModifyLiquidityParams(lo, hi, liq, 0), \"\");\n    }\n\n    function _swap(PoolKey memory k, bool z, int256 amt, uint160 lim, uint256 eth) internal {\n        swapRouter.swap{value: eth}(\n            k, SwapParams(z, amt, lim), PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), \"\"\n        );\n    }\n}","reproduction":"Offline, self-contained proof below (forge test --offline --match-path test/scratch/ProofFallbackBand.t.sol): fresh PoolManager, hook mined at 0x10CC, launch pool seeded 2000 liquidity full range; IMD mock and POOL4 mock etched at the constants, POOL4 open at refTick 0, plain pool (ETH, IMD, 10000, 200, no hook) at 1:1 with 500 ether full-range liquidity. 82 ETH exact-in buy (totalFees 1.64 ETH) + 10 ETH buy (burnable 0.2 ETH). pokeAnchor() -> lastRef = anchor = 0. POOL4 set closed. One 32 ETH buy of IMD on the plain pool moves its tick to -1229 and it stays there. 200 x { roll +1; pokeAnchor() } -> anchor() == blockAnchor() == -1000 and never lower. roll +10; burnIMD(false, 0). Expected (brief: every fee after the cap buys IMD; README: burns continue on the plain pool with the anchored reference): a 0.01 ETH batch is spent on the deep, unmanipulated plain pool. Actual: revert PriceOffReference(-1229, -1000) on this and every later block; burnIMD(true, 0) reverts Pool4Unavailable; burnable() stays 0.2 ether. Observed test output: '[FAIL: PriceOffReference(-1229, -1000)] test_fallbackBurnFollowsTheUnmanipulatedPlainPool()'. Mirror measured in a second scratch run: spot 32029 after a 2000 ETH sell, anchor pinned at 1000, floor = quote(0.01 ether, 1000) * 96% = 10609587768991032 vs fair quote 245998381998676040 (431 bps of fair). Mainnet confirmation of the precondition: cast call 0xc6C965Bd164c483e87d0B550671798e9A3602840 'marketOpen()(bool)' -> true, 'refTick()(int24)' -> 60396, 'maxRefStep()(int24)' -> 200, 'owner()(address)' -> 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7; runtime contains selector 1a8e22e6 = closeMarket(address); Sourcify source: 'Terminal: marketOpen cannot return to true'.","severity":"medium","snippet":"        int24 lo = lastRef - FALLBACK_BAND;\n        int24 hi = lastRef + FALLBACK_BAND;\n        int24 target = spot < lo ? lo : (spot > hi ? hi : spot);","title":"Fallback anchor is clamped to a never-refreshed lastRef band: after a terminal POOL4 close, a lasting plain-pool move of more than 1150 ticks strands every post-cap fee (and a move the other way remov"},{"citation":"resolved","description":"SPEC 7, README ('Checks, in order') and docs/DESIGN.md fix the order TooSoon, Pool4Unavailable, batch = min(burnable, cap), NothingToBurn, then the guards. In the code _resolveReference() runs before _batchFor(). In fallback mode _resolveReference() calls _stepAnchor() (line 430), which does poolManager.getSlot0(plainKey().toId()) and reverts PoolUnavailable when the plain pool is not initialized (lines 568-569), before burnable is looked at. A keeper with nothing burnable (or less than MIN_BURN) while POOL4 is dark and the plain pool is missing is told PoolUnavailable instead of NothingToBurn. In fallback mode the anchor step (state writes to anchor/blockAnchor/anchorBlock and an AnchorStepped event) is also attempted on calls that will revert NothingToBurn or PriceOffReference; the revert undoes it, so no state leaks and no funds are affected. Normal mode is unaffected. This is a spec-order and error-reporting deviation only. Minimal fix that keeps the design: in burnIMD decide the mode first without stepping (open = _pool4Reference(); if !open and (viaPool4 or !anchorSeeded) revert Pool4Unavailable), call _batchFor(!open), and only then seed or step the anchor and read the spot. Merged from audit_flow, audit_math and audit_permissions (all low).","line":413,"path":"src/MedallionHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\n\nimport {MedallionHook} from \"src/MedallionHook.sol\";\n\n/// @dev Stands in for POOL4's hook: only the two views the MedallionHook reads.\ncontract OrderPool4 {\n    bool public marketOpen;\n    int24 public refTick;\n\n    function set(bool open, int24 tick) external {\n        marketOpen = open;\n        refTick = tick;\n    }\n}\n\n/// Finding: burnIMD's fallback branch reads the plain pool (and reverts PoolUnavailable) before the batch\n/// is computed, so a caller with nothing burnable gets PoolUnavailable where SPEC 7's order (TooSoon,\n/// Pool4Unavailable, batch, NothingToBurn, guards) gives NothingToBurn.\ncontract ProofBurnOrder is Test {\n    uint160 constant FLAGS = 0x10CC;\n\n    PoolManager manager;\n    MedallionHook hook;\n    OrderPool4 pool4;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        hook = _deployHook();\n        vm.etch(hook.POOL4_HOOK(), address(new OrderPool4()).code);\n        pool4 = OrderPool4(hook.POOL4_HOOK());\n    }\n\n    function test_nothingToBurnComesBeforeThePlainPoolRead() public {\n        pool4.set(true, 0);\n        hook.pokeAnchor(); // anchor seeded while POOL4 answers\n        pool4.set(false, 0); // POOL4 dark: fallback mode\n        // The plain ETH/IMD pool is never initialized on this manager, and nothing is burnable.\n        vm.roll(block.number + 10);\n        assertEq(hook.burnable(), 0);\n        vm.expectRevert(MedallionHook.NothingToBurn.selector);\n        hook.burnIMD(false, 0);\n    }\n\n    function _deployHook() internal returns (MedallionHook) {\n        bytes memory initCode = abi.encodePacked(type(MedallionHook).creationCode, abi.encode(manager));\n        bytes32 h = keccak256(initCode);\n        for (uint256 s = 0; s < 500_000; s++) {\n            address p = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(s), h)))));\n            if (uint160(p) & 0x3FFF == FLAGS) {\n                return new MedallionHook{salt: bytes32(s)}(IPoolManager(address(manager)));\n            }\n        }\n        revert(\"no salt\");\n    }\n}","reproduction":"Offline, self-contained proof below (forge test --offline --match-path test/scratch/ProofBurnOrder.t.sol): fresh PoolManager, hook mined at 0x10CC, POOL4 mock etched at POOL4_HOOK answering marketOpen()=true, refTick()=0; hook.pokeAnchor() seeds the anchor; POOL4 mock set closed; the plain ETH/IMD pool is never initialized on this manager; vm.roll(block.number + 10); hook.burnable() == 0; hook.burnIMD(false, 0). Expected per SPEC 7 order: revert NothingToBurn (0x98642b86). Actual: revert PoolUnavailable (0x899b8f88) from _stepAnchor(). Observed test output: '[FAIL: Error != expected error: PoolUnavailable() != NothingToBurn()] test_nothingToBurnComesBeforeThePlainPoolRead()'. The same result was reproduced on the repository fixture (MedallionTestBase: etchIMD(); etchPool4(true,0); pokeAnchor(); setMarketOpen(false); roll +10; burnIMD(false,0)).","severity":"low","snippet":"        (bool fallbackMode, int24 ref) = _resolveReference(viaPool4);\n        uint256 batch = _batchFor(fallbackMode);","title":"burnIMD fallback branch reads the plain pool (PoolUnavailable) before the batch/NothingToBurn check, deviating from the SPEC 7 order"},{"citation":"resolved","description":"afterInitialize records the first pool whose currency0 is native ETH, with any currency1, fee tier or tick spacing, writes it once and has no setter (correctly, per the FORBIDDEN list). This is what SPEC 2 asks for, and it is safe as long as deployment and initialization of the ETH/FARE447 pool happen in the same transaction (before deployment the hook has no code, so Hooks.callHook reverts on the empty return and nobody can initialize a pool against the predicted address). If the launch flow ever split them, an outsider's initialize(PoolKey(ETH, anyToken, 100, 1, hook), price) in between would become launchPool, the real FARE447 pool would trade fee-free for its whole life, totalFees would never reach the cap, CREATOR would never be paid and the medallion never retired. The service reference states the factory does both atomically and docs/OPERATIONS.md step 2 records it; this row keeps that dependency visible for the deployer. No code change required; a code-level narrowing (also require key.fee == 3000 && key.tickSpacing == 60) would shrink but not close the window. Merged from audit_permissions and audit_economics (both info).","line":245,"path":"src/MedallionHook.sol","reproduction":"Non-atomic deployment only. tx1: new MedallionHook(pm) at an address carrying 0x10CC. tx2 (attacker): pm.initialize(PoolKey(ADDRESS_ZERO, JUNK, 100, 1, hook), SQRT_PRICE_1_1) -> afterInitialize sets launchPoolSet = true, launchPool = junk id. tx3 (factory): pm.initialize(PoolKey(ADDRESS_ZERO, FARE447, 3000, 60, hook), SQRT_PRICE_1_1) -> the branch at line 245 is skipped, LaunchPoolSet is not emitted; a 10 ETH exact-in buy on the FARE447 pool then leaves totalFees == 0 and launchPool() == junkKey.toId(). Traced by reading lines 244-252 and 513-515; the repository test test_tokenTokenPoolDoesNotBecomeLaunchPoolAndNeverReverts pins the 'first ETH pool wins' rule.","severity":"info","snippet":"        if (!launchPoolSet && key.currency0.isAddressZero()) {","title":"launchPool is whichever native-ETH pool is initialized first; the fee economics rely on the factory deploying the hook and initializing the FARE447 pool in one transaction"},{"citation":"resolved","description":"burnIMD is permissionless and the caller picks the venue. The plain key (ETH, IMD, 10000, 200, no hook) can be created and solely supplied by anyone. In normal mode it is accepted while its spot is as much as MAX_PLAIN_DEVIATION = 300 ticks below POOL4's refTick (IMD ~3% dearer) and the output floor is 96% of quote(ref). A sole LP who holds the plain pool at ref - 300 therefore sells IMD to the hook at ~3.9% below the reference every 5 blocks for 0.05 ETH, collecting the 1% LP fee plus the ~3% premium, about 0.002 ETH per batch, dust against mainnet gas for the keeper call. In fallback mode the anchor can be walked by anyone via pokeAnchor() at 200 ticks per block down to lastRef - 1000, so the premium can reach ~15% but on 0.01 ETH batches (~0.0015 ETH per batch). Everything here is inside the constants SPEC 3 fixes (MAX_PLAIN_DEVIATION, MAX_REF_DEVIATION, MAX_SLIPPAGE_BPS, ANCHOR_STEP, FALLBACK_BAND, batch sizes) and the creator's reserve is untouched; reported so the requester can decide whether 300 ticks + 400 bps is the intended tolerance for the plain pool. No code change needed to meet the brief. From audit_permissions (info).","line":448,"path":"src/MedallionHook.sol","reproduction":"Offline, repository fixture: reachCap(); buyExactIn(10 ether) (burnable 0.2 ETH); etchIMD(); etchPool4(true, 0) (POOL4 open, refTick 0); plain pool initialized at TickMath.getSqrtPriceAtTick(-300) with 5000 ether full-range liquidity; vm.roll(+5); burnIMD(false, 0). Measured: quote(0.05 ether, 0) = 50000000000000000 IMD, received = 48036657478431647 IMD, shortfall 392 bps; the call succeeds (PriceOffReference needs spot < ref - 300, InsufficientOutput needs out < 96% of the quote). Expected by the brief: accepted. Actual: accepted. The number is the maximum per-batch extraction available to the plain-pool LP in normal mode.","severity":"info","snippet":"        int24 spot = _spotChecked(key, ref, (!viaPool4 && !fallbackMode) ? MAX_PLAIN_DEVIATION : MAX_REF_DEVIATION);","title":"In normal mode the sole LP of the permissionless plain ETH/IMD pool can sell IMD to the hook up to ~3.9% below POOL4's reference per 0.05 ETH batch, inside the brief's constants"}],"hash":"a3fb1e52b771fa7590bbf99de569c74885ad14f30fb577f95001058600c5811f","nodeId":"a1ab3b2c-5948-492b-95c4-8b34e18d4372","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds 5 judged findings and 17 coverage rows; the scratch tests are removed and no tracked file changed.\n\n**What I kept, after reproducing each against the tree**\n\n- **High. `MEDALLION_NFT` is the Nouns token and Noun #447 is in the Nouns DAO treasury.** I confirmed on mainnet at block 26098044: `name()` is \"Nouns\", `ownerOf(447)` is the DAO timelock holding 649 Nouns, CREATOR holds none and has no approval. `retire()` is the only release path for the 1.64 ETH, so without a passed DAO proposal it reverts `RetireRefused` forever and the claims stay locked. The code follows the brief's constant, so the requester must either correct the address or change the disclosure, which currently calls CREATOR the medallion's owner. No offline proof is possible for a chain-state defect, and I said so in the finding.\n- **Medium. The fallback anchor band strands post-cap fees after a terminal POOL4 close.** Merged from three specialists. I verified from the live hook's Sourcify source that `closeMarket` is terminal, then reproduced with a self-contained proof: after POOL4 closes and the plain pool moves 1229 ticks, 200 blocks of `pokeAnchor` pin the anchor at -1000 and every burn reverts `PriceOffReference(-1229, -1000)` while 0.2 ETH stays burnable. This is the brief's own clamp, so I framed it as a scope decision with three options. I also corrected the specialist's `setMaxRefStep(0)` claim: that setter rejects values below 1.\n- **Low. Fallback branch reads the plain pool before the batch check.** Merged from three specialists, with a self-contained proof that fails `PoolUnavailable() != NothingToBurn()`.\n- **Two info rows.** The launch-pool capture dependency on atomic deploy plus initialize, and the measured 392 bps plain-pool skim bound in normal mode.\n\n**What I dropped**\n\n- The invariant-handler flakiness: the current tree already excludes the handler's `setUp` selector, the fuzz table shows no `setUp` calls, and three random seeds pass.\n- The fee-base difference across swap shapes: it is what SPEC 4 prescribes and the README fee table already documents both bases.\n\n**Coverage.** All 11 entry points are answered. The token's five functions, `afterSwap` and `unlockCallback` hold after tracing all four swap shapes against v4's delta accounting. Six extra rows cover the ledger invariants, pinned constants, the manifest schema, and the two dropped specialist findings with reasons.","treeHash":null,"usage":{"cachedInputTokens":2007747,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":50483,"runtime":"claude","turns":34,"wallClockMs":744416}},{"artifacts":[],"attempt":3,"bundleHash":null,"device":"bb0a3bf63233e5e5","findings":[],"hash":"c872c85e780e01f7ccf5303aafb997c2a5e7bcaa47f1d854f912313ea431b491","nodeId":"7945ed5d-c9a9-4422-a3a3-e0fc72add4e0","outcome":"failed","summary":"could not restore accepted work: accepted work conflicts with a dependency, which plan validation should have prevented: ","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"codex","turns":0,"wallClockMs":492}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"98b4506bef931d13","findings":[{"citation":"resolved","description":"Economic Security / Flow Gap (periphery x execution x first principles). The live POOL4 hook at 0xc6C9...2840 is CappedBurnHook (verified source on Sourcify); its closeMarket() is documented 'Terminal: marketOpen cannot return to true, so a closed market is redeployed, not reopened', and POOL4_HOOK is a constant here. Once it closes, MedallionHook is in fallback mode for the rest of its life: lastRef is written only by _seedAnchor (normal mode) and never again, and _stepAnchor clamps the anchor to [lastRef - 1000, lastRef + 1000] (lines 571-573). The one-sided guard at line 463 refuses a burn whenever the plain spot tick is below blockAnchor - 150. Consequence 1 (stranding): if IMD reprices upward in ETH by more than 1150 ticks (~12.2%) relative to the last POOL4 reference and stays there, the anchor stops at lastRef - 1000 and every burnIMD(false, x) reverts PriceOffReference in every future block, even though the plain pool is deep, stable and un-manipulated (on mainnet it holds ~376 ETH of virtual reserve). burnable() keeps growing with every swap, nothing can spend it (no sweep, no other pool, viaPool4 reverts Pool4Unavailable), and the brief's guarantee 'every fee after the cap buys $IMD until the chain stops' is permanently broken. A 12% move is ordinary for a token over months. Consequence 2 (floor erosion): if IMD instead falls more than 1000 ticks, the anchor pins at lastRef + 1000 and the only price protection left is minOut = 96% of quote(lastRef + 1000), which drifts arbitrarily far below market (a 50% fall leaves a floor at ~48% of fair), so a sandwicher can take up to ~half of each 0.01 ETH batch; today the plain pool is deep enough that the attacker's 1% round-trip fees exceed that, so this side is a degradation, not a profitable exploit. The code implements the band exactly as the brief specifies; the defect is in the economics of a static band against a terminal reference. Fix options (scope decision for the requester, since each changes the specified rule): let lastRef re-centre on the stepped anchor when the plain pool has been stable for N blocks; or widen/remove FALLBACK_BAND and rely on ANCHOR_STEP plus the 96% floor; or allow a fresh reference source once POOL4 is terminally closed.","line":571,"path":"src/MedallionHook.sol","reproduction":"State: hook deployed while POOL4 answered (anchor seeded, lastRef = R, e.g. 0); totalFees >= 1.64 ETH with burnable >= 0.01 ETH; plain pool (ETH, IMD, 10000, 200, no hook) initialized and deep. Then POOL4 marketOpen() returns false (terminal closeMarket on the live hook). Market reprices IMD so the plain pool tick is R - 1229 (one 32 ETH buy into 500 ETH of full-range liquidity at 1:1 in the test; on mainnet any lasting ~12% rise). Call pokeAnchor() once per block for 200 blocks: anchor() == R - 1000 and never lower. Call burnIMD(false, 0) in any later block: reverts PriceOffReference(spot = R - 1229, ref = R - 1000) (observed revert data 0x76388d90 ... fffffb33 ... fffffc18). Expected per the brief: the 0.01 ETH batch buys IMD on the deep, unmanipulated plain pool. Actual: every burn reverts forever while the repricing holds; burnable() is unspendable. Run: forge test --offline --match-path test/scratch/FallbackStranding.t.sol","severity":"medium","snippet":"        int24 lo = lastRef - FALLBACK_BAND;\n        int24 hi = lastRef + FALLBACK_BAND;\n        int24 target = spot < lo ? lo : (spot > hi ? hi : spot);","title":"Fallback anchor is clamped to a frozen lastRef band, so after POOL4's terminal close a >11.5% IMD repricing strands every post-cap fee forever (and a drop past the band removes the slippage floor)"},{"citation":"resolved","description":"MedallionHandler.setUp() is public and the handler is the invariant target, so forge's invariant fuzzer may call setUp() mid-run. That deploys a new PoolManager, FareToken and MedallionHook (saltCursor advances) and re-etches the medallion mock, while MedallionInvariantTest keeps the hook and manager captured in its own setUp. After such a call the handler's swap()/retire() drive the new hook: CREATOR receives 1.64 ETH from the new hook while the old hook reports creatorPaid == 0, and assertEq(hook.CREATOR().balance, paid) fails. The contract is fine; the suite is flaky, and a fresh offline run of `forge test --offline` in this tree failed 1 of 90 tests (171 s) for exactly this reason. Fix: make the handler's setup internal (call it from the constructor or rename to an internal init) and/or use targetSelector to exclude it.","line":21,"path":"test/MedallionInvariant.t.sol","reproduction":"forge test --offline --match-test invariant_creatorPaidIsZeroOrCap -vv. Shrunk failing sequence observed: MedallionHandler.setUp(); swap(1, 1.52e76); swap(166, 15847); swap(96, 3166); swap(49, 5489); swap(182, 1209821813); retire(). Result: [FAIL: assertion failed: 1640000000000000000 != 0] because CREATOR.balance == 1.64 ETH (paid by the redeployed hook) while the captured hook's creatorPaid() == 0. Expected: the fuzzer cannot redeploy the system under test.","severity":"low","snippet":"    function setUp() public override {","title":"Invariant handler exposes setUp() to the fuzzer, so the invariant suite fails non-deterministically (the verifier's rerun saw invariant_creatorPaidIsZeroOrCap fail)"},{"citation":"resolved","description":"Trust assumption, not a code defect. The verified CappedBurnHook at POOL4_HOOK has owner() = 0x047F606fD5b2BaA5f5C6c4aB8958E45CB6B054B7 with closeMarket(address) (terminal), setMaxRefStep(int24) (today 200/block; refTick lags the previous block's close by at most this step) and other setters. The owner can therefore (a) push MedallionHook into permanent 0.01 ETH fallback by closing the market, which is the precondition of finding 1, and (b) with setMaxRefStep(0) freeze refTick so a lasting upward IMD move makes spot < refTick - 150 and burns stop in normal mode as well. No unprivileged amplifier: a non-owner cannot move refTick faster than 200 ticks/block and cannot close the market. Record in README/notes that burns depend on the IMD network keeping POOL4 open and its reference live.","line":541,"path":"src/MedallionHook.sol","reproduction":"Mainnet reads (block 26097931 via public RPC): marketOpen() = true, refTick() = 60396, maxRefStep() = 200, owner() = 0x047F...54B7, POOL4 pool tick 60375, plain pool tick 60404. State that triggers: owner calls closeMarket(recipient) -> marketOpen() = false forever -> _resolveReference takes the fallback branch for every later burnIMD and pokeAnchor.","severity":"info","snippet":"        (bool ok, bytes memory ret) = POOL4_HOOK.staticcall(abi.encodeWithSignature(\"marketOpen()\"));","title":"POOL4 operator is a trusted third party for the burn economics: owner-only closeMarket() is terminal and setMaxRefStep() tunes the reference the guard and floor are priced from"},{"citation":"resolved","description":"If the hook were ever deployed in one transaction and the FARE447/ETH pool initialized in a later one, anyone could initialize (ETH, anyToken, anyFee, anySpacing, thisHook) in between: that pool becomes launchPool, the real launch pool trades fee-free for ever, totalFees never reaches the cap, and retire()/burnIMD() are dead. The service documentation states the factory does both in one transaction, which closes the window; this row records the dependency so the deployer keeps it. No change required if atomic deployment is guaranteed.","line":245,"path":"src/MedallionHook.sol","reproduction":"Non-atomic deployment only: tx1 deploys MedallionHook(pm); tx2 (attacker) pm.initialize({ETH, X, 100, 1, hook}, p); tx3 (factory) pm.initialize({ETH, FARE447, 3000, 60, hook}, p). afterInitialize in tx3 sees launchPoolSet == true and returns without recording the pool; swaps on the FARE447 pool pay no fee.","severity":"info","snippet":"        if (!launchPoolSet && key.currency0.isAddressZero()) {","title":"launchPool is whichever native-ETH pool is initialized first; the economics rely on the factory deploying the hook and initializing the pool atomically"},{"citation":"resolved","description":"Both formulas are the ones the brief specifies, so this is not a defect, only a note for the README: a buyer who specifies the token amount (exact-out) pays 2% of the pool's ETH, i.e. 1.9608% of the ETH that leaves their wallet, while an exact-in buyer pays 2.0000% of it; the same for sellers. Routers that choose exact-out save 0.04% of volume. No extraction beyond that, no effect on the ledger invariant.","line":288,"path":"src/MedallionHook.sol","reproduction":"Exact-in buy of 100 ETH: fee 2 ETH, pool receives 98 ETH. Exact-out buy that costs the pool 98 ETH: fee 1.96 ETH, swapper pays 99.96 ETH. Same tokens out, 0.04 ETH less fee.","severity":"info","snippet":"            fee = gross * (params.zeroForOne ? BUY_FEE_BPS : SELL_FEE_BPS) / 10_000;","title":"Fee base differs by swap shape: 2% of the swapper's gross ETH in the beforeSwap shapes but 2% of the pool's ETH (1.96% of the swapper's gross) in the afterSwap shapes"}],"hash":"d80b0394068d13e0bb73c11b272a9efe9f35c6973d588e20b97c5f71d5150955","nodeId":"1fede6e2-3f08-4b3c-bd9a-f6d0c8eb74f0","outcome":"completed","summary":"The review is complete. The findings file `.imd-findings.json` is in the repository root with 5 findings and 14 coverage rows, and the working tree is otherwise untouched (both output paths are excluded by `.git/info/exclude`, as the task expects).\n\n**What I found**\n\n- **Medium, with a failing proof test.** The fallback burn reference is clamped to a band around `lastRef`, and `lastRef` is only ever written while POOL4 answers. The live POOL4 hook is the verified `CappedBurnHook`, whose `closeMarket()` is documented as terminal. After it closes, a lasting IMD repricing of more than about 12% in ETH pins the anchor at the band edge and every `burnIMD` reverts `PriceOffReference` forever, even against a deep, unmanipulated plain pool. All post-cap fees are then stranded as claims with no path out. The opposite move erodes the 96% floor toward nothing. The code follows the brief's band exactly, so the fix is a scope decision for the requester. The proof at `test/scratch/FallbackStranding.t.sol` fails on this tree with `PriceOffReference(-1229, -1000)`.\n- **Low.** The invariant handler's `setUp()` is public and targeted by the fuzzer. A fresh offline run of the suite failed `invariant_creatorPaidIsZeroOrCap` because the fuzzer redeployed the hook mid-run. The contract is fine; the suite is flaky, which matters for the verifier's rerun.\n- **Info.** POOL4's owner is a trusted third party for burn economics (terminal close, `setMaxRefStep`). The launch-pool capture relies on atomic deploy-plus-initialize. The fee base is 2% of the swapper's gross in the carved shapes but 1.96% in the afterSwap shapes, both as specified.\n\n**What held.** Fee delta accounting in all four swap shapes against v4-core 1.0.2's hook delta math, the claims-cover-ledger invariant, `creatorPaid` in {0, cap}, `retire()` paying exactly the cap once, and the sandwich bound on burns at current mainnet depth. Mainnet reads confirmed POOL4 open with refTick 60396 matching both pool ticks, Nouns #447 transferable to the dead address, CREATOR an EOA, and IMD a plain ERC-20 that accepts transfers to the dead address.\n\n**Not reached.** No fork-level simulation of the POOL4 hook's `afterSwap` running inside this hook's own unlock, so I did not verify that a `burnIMD(true, …)` swap on the live POOL4 pool completes; the plain-pool path is unaffected either way.","treeHash":null,"usage":{"cachedInputTokens":1929726,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":64971,"runtime":"claude","turns":39,"wallClockMs":1269035}},{"artifacts":[],"attempt":2,"bundleHash":"41e00d28cc2f948b6d14300c4665143af8ec4c6218b792cbbfb0cd0714ceecee","device":"468e82a89b9bfe18","findings":[],"hash":"dd01bf34b496520ebde3bbb5d0b6193f9d19bb840221e39f34d5c5caa1d9a335","nodeId":"2da07652-5466-4a62-a023-ade62880a190","outcome":"completed","summary":"Done. The clean rebuild, the full suite and the format check all pass, and the manifest now has the shape the previous attempt got wrong.\n\n**What was built**\n\n- `src/FareToken.sol`: FARE447, a self-contained ERC-20 with a zero-argument constructor that mints exactly 1e27 to the deployer, 18 decimals, `burn` and `burnFrom`, no owner or mint path.\n- `src/MedallionHook.sol`: the hook on v4-core 1.0.2 with flags 0x10CC, PoolManager as the only constructor argument, all spec constants public, 2% ETH fee in all four swap shapes collected as ERC-6909 claims, `PartialFill` in the beforeSwap shapes, the ledger, `retire()`, `burnIMD()`, `pokeAnchor()`, `status()`, and the pinned `LAST_FARE`. Runtime is 14,409 bytes with no SELFDESTRUCT, DELEGATECALL or CALLCODE.\n- `launch.json`: contract names, permissions as an array, `initialPrice` as a sqrtPriceX96 string, notes at 3998 characters.\n- `README.md` opening with the petition verbatim, `REVIEW.md`, `docs/DESIGN.md`, `docs/OPERATIONS.md`, ABIs under `docs/abi/`.\n- `lib/` vendors v4-core 1.0.2, its solmate and OpenZeppelin sub-libraries, and forge-std as plain files with no submodule metadata.\n\n**Verification**\n\n| Check | Result |\n| --- | --- |\n| `forge build --offline` after `forge clean` | success |\n| `forge test --offline` | 90 passed, 0 failed (89 unit and fuzz tests plus 4 invariants) |\n| `forge fmt --check` | clean |\n| Pinned floor tests run in scratch against the built creation code | 9 of 9 passed |\n\nThe tests cover the four fee directions, partial-fill reverts, fee-free other pools, a buy on a manager holding no ETH, the donation invariant, burnable staying zero until the cap, `Recouped` firing once, all three `status()` sentences, the LAST_FARE hash and length, `retire()` before the cap and with or without approval, the reentrancy lock, every `burnIMD` ordering and guard, the fallback band, the once-per-block step, and the start-of-block anchor.\n\n**Assumptions to know about**\n\n- The initial price is 10,000,000 FARE447 per ETH because the brief gives none. It is flagged in the README, the notes and the operations doc, and the hook does not depend on it.\n- The POOL4 view names `marketOpen()` and `refTick()` come from the brief and were confirmed live on mainnet during the build.\n- Burns are allowed as soon as fees exceed the cap, before retirement, because the spec's `burnable` formula defines them that way. The 1.64 ETH stays fully backed by claims, which an invariant checks.\n\nOne side note: the session's memory directory is write-denied, so I could not save the manifest-schema lesson there.","treeHash":"572f835ae56729c45f895816d4b8297e474ba4ec","usage":{"cachedInputTokens":6484698,"inputTokens":1090,"model":"claude-fable-5-1","outputTokens":151007,"runtime":"claude","turns":55,"wallClockMs":1894032}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0256823ae36e7900","findings":[],"hash":"e3e07bf4324de237ce190c020f69607f1bc39bc156314dba309def9d3ad4ebff","nodeId":"4f983c43-f061-4202-95d4-532b2364633f","outcome":"failed","summary":"could not restore accepted work: accepted work conflicts with a dependency, which plan validation should have prevented: ","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"codex","turns":0,"wallClockMs":1391}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3b260b68e9ad6a37","findings":[{"citation":"resolved","description":"SPEC 7 fixes the order of checks in burnIMD: TooSoon, then Pool4Unavailable, then batch = min(burnable, cap), then NothingToBurn under MIN_BURN, then the guards (PoolUnavailable / PriceOffReference). In the code, _resolveReference() runs before _batchFor(). In fallback mode _resolveReference() calls _stepAnchor() (src/MedallionHook.sol:430), which reads poolManager.getSlot0(plainKey().toId()) and reverts PoolUnavailable when the plain pool is not initialized (src/MedallionHook.sol:568-569). That read is a guard on the plain pool and it is evaluated before burnable is even looked at. Two observable consequences: (1) with nothing burnable and no plain pool, the caller gets PoolUnavailable instead of NothingToBurn, which is the opposite of the documented order and of what README.md:71-74 and docs/DESIGN.md:66-75 describe; (2) in fallback mode the anchor step (a state write to anchor/blockAnchor/anchorBlock plus an AnchorStepped event) is attempted on every burnIMD call, including ones that will revert NothingToBurn or PriceOffReference, so the step only survives when the whole burn succeeds. Nothing is lost: the revert undoes the step, totalFees/burnSpent are untouched, and normal mode is unaffected. It is a spec-order and observability defect. Minimal fix that keeps the design: in burnIMD compute the mode first without stepping (open = _pool4Reference(); if !open and (viaPool4 or !anchorSeeded) revert Pool4Unavailable), then call _batchFor(!open), and only then seed or step the anchor and read the spot.","line":413,"path":"src/MedallionHook.sol","reproduction":"State: deploy hook on a manager where POOL4_HOOK has code answering marketOpen()=true once (constructor or pokeAnchor seeds the anchor), then POOL4 stops answering (marketOpen()=false). The plain ETH/IMD pool (ETH, IMD, 10000, 200, no hook) is not initialized on this manager. totalFees == 0, so burnable() == 0 < MIN_BURN. Roll 5 blocks past lastBurnBlock. Call burnIMD(false, 0). Expected per SPEC 7 order: revert NothingToBurn (batch 0 < 0.002 ether). Actual: revert PoolUnavailable from _stepAnchor() because the plain pool's sqrtPriceX96 is 0. Verified with a scratch Foundry test (etchPool4(true,0); hook.pokeAnchor(); pool4Mock.setMarketOpen(false); vm.roll(block.number+5); vm.expectRevert(MedallionHook.PoolUnavailable.selector); hook.burnIMD(false,0)) which passes on the current tree, i.e. PoolUnavailable is what is thrown.","severity":"low","snippet":"        (bool fallbackMode, int24 ref) = _resolveReference(viaPool4);\n        uint256 batch = _batchFor(fallbackMode);","title":"burnIMD fallback mode steps the anchor and reads the plain pool before the NothingToBurn check, so the brief's check order (TooSoon, Pool4Unavailable, batch, NothingToBurn, guards) is not followed"},{"citation":"resolved","description":"This is the behaviour SPEC 7 mandates (anchor clamped to lastRef +- FALLBACK_BAND, one-sided guard), so it is not a code defect, but it is a liveness limit the operations notes do not state. lastRef is only ever written by _seedAnchor(), i.e. when POOL4 answers. While POOL4 is dark the anchor can never leave [lastRef-1000, lastRef+1000]. If IMD becomes more than ~10.5% more expensive in ETH on the plain pool (spot < lastRef - 1000 - 150), the guard spot < blockAnchor - MAX_REF_DEVIATION is true at the clamp and burnIMD(false, x) reverts PriceOffReference on every call, in every block, while burnable claims keep accumulating. README.md:172-174 says 'If POOL4 ever stops answering, burns continue on the plain pool at 0.01 ETH per batch using the anchored reference', which is only true while the plain price stays inside the band. No funds are lost (claims stay with the hook and burn once POOL4 re-seeds), so this is informational: document the band limit in README/OPERATIONS so keepers know why burns stop, or widen the design if the requester wants burns to survive a long POOL4 outage.","line":571,"path":"src/MedallionHook.sol","reproduction":"State: cap reached plus 0.2 ETH burnable; POOL4 mock open at refTick 0; pokeAnchor() seeds lastRef = anchor = 0; then POOL4 set closed. Push the plain pool spot down (buy IMD with 260 ETH into 500 ETH of full-range liquidity) so the spot tick is below -1200. Call pokeAnchor() once per block for 20 blocks: anchor walks to -1000 and stops (clamped). Roll 5 blocks. Call burnIMD(false, 0). Expected per README: a 0.01 ETH burn on the plain pool. Actual: revert PriceOffReference(spot, -1000) with spot < -1150, and the same revert on every later block until POOL4 answers again. Verified with a scratch Foundry test on the current tree.","severity":"info","snippet":"        int24 lo = lastRef - FALLBACK_BAND;\n        int24 hi = lastRef + FALLBACK_BAND;\n        int24 target = spot < lo ? lo : (spot > hi ? hi : spot);","title":"During a POOL4 outage, a plain-pool price move of more than FALLBACK_BAND (10%) in IMD's favour makes every fallback burn revert PriceOffReference until POOL4 answers again; README says burns 'continu"}],"hash":"e772432120d32f3c13e354678d7e2452061a98361212198f2e458e29b0b3663e","nodeId":"0e9c3343-1d5c-4813-84bf-6566205c94ef","outcome":"completed","summary":"The review is complete. The findings file is written, validated, and both quoted snippets match the tree exactly. The scratch probe directory was removed.\n\n**Outcome.** The hook's control flow holds up under the Execution Trace, Periphery and First Principles checks. I found no loss-of-funds path. Two items are recorded in `.imd-findings.json`:\n\n- **Low, `burnIMD` check order.** In fallback mode the anchor step runs before the batch is sized. That step reads the plain pool and reverts `PoolUnavailable` before `NothingToBurn` is ever evaluated, which contradicts the order SPEC 7 fixes. A scratch test confirmed the wrong revert reason with nothing burnable and no plain pool. Nothing is lost since the revert undoes the step. The fix is to resolve the mode without stepping, size the batch, then seed or step.\n- **Info, fallback band liveness.** During a POOL4 outage, if IMD's plain-pool price moves more than the band in its own favour, every fallback burn reverts until POOL4 answers again. This is what the spec mandates, but the README says burns \"continue on the plain pool\". Reproduced with a scratch test. Worth documenting for keepers.\n\n**What was traced and holds.** All four swap shapes were checked against v4-core 1.0.2's delta composition: the claim the hook mints in afterSwap equals the credit the manager books after the callback returns, the PartialFill check matches the pool's raw delta, and no swap callback makes an external call other than `mint`. `retire()` sets state before its unlock, pays exactly the cap once, and the transient lock blocks re-entry from the NFT and the ETH take. `unlockCallback` is reachable only through the hook's own unlock. The ledger invariant and the \"no ETH to CREATOR except the cap\" guarantee hold on every path. All eleven listed entry points have a coverage row, plus three invariant rows.\n\n**Not reachable offline.** The real POOL4 hook's `refTick()` semantics and the mainnet pool keys cannot be verified without a fork, so the reference's manipulability is a trust assumption, bounded by the 150/300-tick guards and the 96% floor on a 0.05 ETH batch. The existing suite of 90 tests passed throughout.","treeHash":null,"usage":{"cachedInputTokens":1715187,"inputTokens":356,"model":"claude-fable-5-1","outputTokens":38291,"runtime":"claude","turns":34,"wallClockMs":547606}},{"artifacts":[],"attempt":1,"bundleHash":"8e724ccabec95fc6b933032abfb9de5907d71c027728fbb458ca25fcc7dfd2be","device":"468e82a89b9bfe18","findings":[],"hash":"f2f2a96cd5f47248d9761971c6ea37dc8a99383cba5f686b1ea143633c95d84e","nodeId":"2da07652-5466-4a62-a023-ade62880a190","outcome":"completed","summary":"All checks pass and every finding is answered. Here is the recap.\n\n**Outcome.** All five findings were reproduced and addressed. The reviewer's proof for the fallback band failed on the starting tree with `PriceOffReference(-1229, -1000)` and now passes. The full offline suite passes with the proof included.\n\n| Check | Result |\n| --- | --- |\n| `forge fmt --check` | clean |\n| `forge build --offline` | success |\n| `forge test --offline` | 98 passed, 0 failed (incl. proof and 4 invariants) |\n| Opcode walk (PUSH data skipped) | 0 × F2/F4/FF in both runtimes |\n| launch.json shape | names are strings, permissions array of 5, initialPrice string, notes 3985 chars |\n\n**What changed, per finding** (all answered in `.imd-responses.json`):\n\n- **Nouns #447 (high, fixed by disclosure).** Confirmed on mainnet via cast: the collection is \"Nouns\", Noun 447 sits in the DAO treasury timelock, CREATOR holds none, no approval. The constant is the brief's and stays. NatSpec, README Disclosure and Operational responsibilities, OPERATIONS, and the manifest notes now state that retirement needs a passed Nouns DAO proposal and that the 1.64 ETH may never be released.\n- **Fallback band (medium, fixed).** New constant `FALLBACK_RECENTER_BLOCKS = 100` and state `lastRefBlock`. In fallback, once 100 blocks pass since `lastRef` was written, the next anchor step re-centres the band on the anchor and emits `BandRecentered`. The band follows a lasting move at most 1000 ticks per 100 blocks; a POOL4 re-seed resets the clock. Five new tests cover the down move, the mirror up move, the boundary, the drift bound, and the reset.\n- **Check order (low, fixed).** `burnIMD` now decides the mode, computes the batch, and only then seeds or steps the anchor and reads the pool. Two new tests pin the order.\n- **launchPool atomicity and plain-pool LP bound (info).** No code changes, both by spec. The atomic deploy dependency and the ~3.9% per-batch bound are now documented in README, OPERATIONS and the notes.\n\n**Also fixed on the way.** The invariant suite was flaky in the accepted tree: the handler's public `setUp()` was a fuzz target, so the fuzzer could redeploy the hook mid-sequence. The test now pins the five handler selectors. REVIEW.md records this and the revision round, and the ABI export was regenerated.\n\n**Left for the deployer and requester.** Retirement cannot happen without Nouns DAO governance acting; nobody in this launch can do that alone. Keepers are still needed for `burnIMD` and `pokeAnchor`, especially in fallback mode where they keep the anchor following the plain pool.","treeHash":"394191b58a996e98015df33bd86d7307396fc92f","usage":{"cachedInputTokens":3063851,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":64656,"runtime":"claude","turns":62,"wallClockMs":1245621}}],"verification":[{"checks":[{"durationMs":5451,"exitCode":0,"name":"build","output":"Compiling 85 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.28s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/utils/MedallionTestBase.sol:225:5:\n    |\n225 |     function lastBurnLog() internal returns (BurnLog memory log_) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:427:21\n    │\n427 │             return (false, pool4Ref);\n    │                     ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:431:17\n    │\n431 │         return (true, blockAnchor);\n    │                 ━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:542:46\n    │\n542 │         if (!ok || ret.length != 32) return (false, 0);\n    │                                              ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:544:32\n    │\n544 │         if (flag != 1) return (false, 0);\n    │                                ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:547:46\n    │\n547 │         if (!ok || ret.length != 32) return (false, 0);\n    │                                              ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:549:75\n    │\n549 │         if (tick < TickMath.MIN_TICK || tick > TickMath.MAX_TICK) return (false, 0);\n    │                                                                           ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:550:17\n    │\n550 │         return (true, int24(tick));\n    │                 ━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MedallionHook.sol:300:44\n    │\n300 │         return (IHooks.afterSwap.selector, unspecifiedDelta);\n    │                                            ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:297:13\n    │\n297 │             emit FeeCollected(params.zeroForOne, fee, updated);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:298:65\n    │\n298 │             if (before < CREATOR_CAP && updated >= CREATOR_CAP) emit Recouped(updated, block.number);\n    │                                                                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:285:61\n    │\n285 │             if (int256(amount0) != params.amountSpecified + int256(fee)) revert PartialFill();\n    │                                                             ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:43\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                           ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:51\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                   ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:72\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                                        ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:80\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                                                ━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `retired` is updated\n    ╭▸ src/MedallionHook.sol:387:17\n    │\n387 │                 MEDALLION_NFT.call(abi.encodeWithSelector(0x23b872dd, owner, DEAD, MEDALLION_ID));\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:390:13\n    │\n390 │             emit MedallionRetired(owner);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:396:9\n    │\n396 │         emit CreatorPaid(CREATOR, CREATOR_CAP);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:397:9\n    │\n397 │         emit LastFare(MEDALLION_ID, LAST_FARE_HASH, LAST_FARE);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/MedallionHook.sol:395:9\n    │\n395 │         poolManager.unlock(abi.encode(ACTION_PAY_CREATOR, bytes(\"\")));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:560:9\n    │\n560 │         emit AnchorSeeded(ref, block.number);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:584:9\n    │\n584 │         emit AnchorStepped(from, to, target, block.number);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:454:9\n    │\n454 │         emit IMDBurned(viaPool4, fallbackMode, batch, imdOut, ref, spot);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:495:57\n    │\n495 │                     zeroForOne: true, amountSpecified: -int256(batch), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n    │                                                         ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:499:45\n    │\n499 │             if (int256(delta.amount0()) != -int256(batch) || delta.amount1() <= 0) revert PartialFill();\n    │                                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:500:27\n    │\n500 │             uint256 out = uint256(uint128(delta.amount1()));\n    │                           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:500:35\n    │\n500 │             uint256 out = uint256(uint128(delta.amount1()));\n    │                                   ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:524:55\n    │\n524 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:524:90\n    │\n524 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                                                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:535:24\n    │\n535 │         return address(uint160(word));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint160' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:550:23\n    │\n550 │         return (true, int24(tick));\n    │                       ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int24' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:593:40\n    │\n593 │             fracDigits[i - 1] = bytes1(uint8(48 + frac % 10));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MedallionHook.sol:603:13\n    │\n603 │             digits++;\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:607:36\n    │\n607 │             out[--digits] = bytes1(uint8(48 + v % 10));\n    │                                    ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionAdversarial.t.sol:349:17\n    │\n349 │         vm.roll(block.number + 5);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionAdversarial.t.sol:539:17\n    │\n539 │         vm.roll(block.number + 5);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionAdversarial.t.sol:548:17\n    │\n548 │         vm.roll(block.number + 5);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionAdversarial.t.sol:568:17\n    │\n568 │         vm.roll(block.number + 5);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/MedallionBurn.t.sol:26:17\n   │\n26 │         vm.roll(block.number + 5);\n   │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:131:17\n    │\n131 │         vm.roll(block.number + 4);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:336:17\n    │\n336 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:363:21\n    │\n363 │             vm.roll(block.number + 1);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:422:17\n    │\n422 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:430:21\n    │\n430 │             vm.roll(block.number + 1);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:446:17\n    │\n446 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:502:21\n    │\n502 │             vm.roll(block.number + 5);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/MedallionInvariant.t.sol:60:17\n   │\n60 │         vm.roll(block.number + bound(blocks, 0, 7));\n   │         ────────━━━━━━━━━━━━─────────────────────── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionLedgerInvariant.t.sol:262:17\n    │\n262 │         vm.roll(block.number + bound(blocks, 0, 7));\n    │         ────────━━━━━━━━━━━━─────────────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionLedgerInvariant.t.sol:304:17\n    │\n304 │         vm.roll(block.number + bound(blocks, 0, 3));\n    │         ────────━━━━━━━━━━━━─────────────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":913,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 11 tests for test/MedallionRetire.t.sol:MedallionRetireTest\n[PASS] test_creatorPaidIsZeroOrCap() (gas: 916980)\n[PASS] test_retireCannotRunTwice() (gas: 921526)\n[PASS] test_retireFeesKeepAccruingAfterRetirement() (gas: 1095274)\n[PASS] test_retireIsGuardedAgainstReentrancyFromTheMedallion() (gas: 547777)\n[PASS] test_retireIsRefusedWhenTheTransferSilentlyDoesNothing() (gas: 837073)\n[PASS] test_retireMovesTheMedallionPaysTheCreatorAndEmitsInOneTransaction() (gas: 1235974)\n[PASS] test_retireRevertsBeforeTheCap() (gas: 755489)\n[PASS] test_retireRevertsOnABadOwnerOfAnswer() (gas: 799562)\n[PASS] test_retireRevertsWhenTheMedallionHasNoCode() (gas: 270443)\n[PASS] test_retireSkipsTheTransferWhenTheMedallionIsAlreadyDead() (gas: 850929)\n[PASS] test_retireWithoutApprovalIsRefusedAndChangesNothing() (gas: 797952)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 43.77ms (8.85ms CPU time)\n\nRan 17 tests for test/FareToken.t.sol:FareTokenTest\n[PASS] testFuzz_burnFromAccounting(uint256,uint256) (runs: 256, μ: 96587, ~: 84537)\n[PASS] testFuzz_transferConservesSupply(address,uint256) (runs: 256, μ: 85984, ~: 85798)\n[PASS] test_approveAndTransferFrom() (gas: 132174)\n[PASS] test_approveZeroSpenderReverts() (gas: 30329)\n[PASS] test_burnFromUsesAllowance() (gas: 144974)\n[PASS] test_burnReducesSupply() (gas: 54024)\n[PASS] test_burnRevertsOnInsufficientBalance() (gas: 34873)\n[PASS] test_constructorEmitsMintTransfer() (gas: 11158)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 117211)\n[PASS] test_metadata() (gas: 23923)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 20624)\n[PASS] test_noMintOwnerPauseOrUpgradeSurface() (gas: 283133)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 486845)\n[PASS] test_transfer() (gas: 81905)\n[PASS] test_transferFromRevertsOnInsufficientAllowance() (gas: 85116)\n[PASS] test_transferRevertsOnInsufficientBalance() (gas: 37663)\n[PASS] test_transferRevertsToZeroAddress() (gas: 30405)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 54.50ms (63.88ms CPU time)\n\nRan 33 tests for test/MedallionHook.t.sol:MedallionHookTest\n[PASS] testFuzz_feeIsTwoPercentInEveryShape(uint256,uint8) (runs: 256, μ: 285353, ~: 283114)\n[PASS] testFuzz_statusFormatsTwoTruncatedDecimals(uint256) (runs: 256, μ: 241388, ~: 241132)\n[PASS] test_afterSwapShapesTolerateAPriceLimitAndStillChargeOnGross() (gas: 226845)\n[PASS] test_burnableIsZeroUntilTheCapAndGrowsAfter() (gas: 772418)\n[PASS] test_buyWorksOnAFreshManagerSeededWithTokensOnly() (gas: 10708837)\n[PASS] test_callbacksRefuseCallersOtherThanThePoolManager() (gas: 111250)\n[PASS] test_constructorRejectsAnAddressWithoutTheFlags() (gas: 7605)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 3942)\n[PASS] test_constructorSetsLastBurnBlockAndLeavesAnchorUnseeded() (gas: 22764)\n[PASS] test_donatedClaimsDoNotCountAsFeesAndKeepTheInvariant() (gas: 548463)\n[PASS] test_exactInBuyPartialFillReverts() (gas: 153503)\n[PASS] test_exactInBuyTakesTwoPercentOfEthInAsClaims() (gas: 233975)\n[PASS] test_exactInSellTakesTwoPercentOfGrossEthAsClaims() (gas: 221297)\n[PASS] test_exactOutBuyTakesTwoPercentOfGrossEthAsClaims() (gas: 234343)\n[PASS] test_exactOutSellPartialFillReverts() (gas: 132773)\n[PASS] test_exactOutSellTakesTwoPercentOfEthOutAsClaims() (gas: 222450)\n[PASS] test_feeIsNeverTakenInTokens() (gas: 700444)\n[PASS] test_firstNativePoolIsTheLaunchPool() (gas: 17339)\n[PASS] test_lastFareIsPinned() (gas: 13380)\n[PASS] test_noAdminSurface() (gas: 234481)\n[PASS] test_otherPoolsWithTheHookAreFeeFree() (gas: 1615026)\n[PASS] test_permissionsAreExactlyTheFiveFlags() (gas: 11334)\n[PASS] test_publicConstants() (gas: 112834)\n[PASS] test_quoteIsAmountTimesPrice() (gas: 32710)\n[PASS] test_recoupedEmittedOnTheCrossingSwapWithTheOvershoot() (gas: 367502)\n[PASS] test_recoupedIsEmittedExactlyOnce() (gas: 707458)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 3962819)\n[PASS] test_statusInService() (gas: 591328)\n[PASS] test_statusRecoupedNotRetired() (gas: 409151)\n[PASS] test_swapsOnlyEverAddToTotalFees() (gas: 1105217)\n[PASS] test_tinySwapBelowFeeGranularityIsFree() (gas: 175937)\n[PASS] test_tokenTokenPoolDoesNotBecomeLaunchPoolAndNeverReverts() (gas: 10843064)\n[PASS] test_unlockCallbackRejectsUnknownActions() (gas: 36335)\nSuite result: ok. 33 passed; 0 failed; 0 skipped; finished in 62.80ms (132.91ms CPU time)\n\nRan 33 tests for test/MedallionAdversarial.t.sol:MedallionAdversarialTest\n[PASS] testFuzz_afterSwapShapesChargeOnTheRealisedGrossUnderAnyLimit(uint256,int24,bool) (runs: 512, μ: 231681, ~: 236850)\n[PASS] testFuzz_beforeSwapSpecifiedDeltaIsExactlyTwoPercent(uint128,bool) (runs: 512, μ: 25064, ~: 25091)\n[PASS] testFuzz_exactInBuyWithALimitFillsFullyOrRevertsCleanly(uint256,int24) (runs: 512, μ: 223448, ~: 237469)\n[PASS] testFuzz_exactOutSellWithALimitFillsFullyOrRevertsCleanly(uint256,int24) (runs: 512, μ: 202800, ~: 225071)\n[PASS] testFuzz_quoteIsMonotoneInTheTickAndLinearInTheAmount(uint256,int24) (runs: 512, μ: 52119, ~: 52057)\n[PASS] testFuzz_quoteMatchesTheSquaredSqrtPriceWithinRounding(uint256,int24) (runs: 512, μ: 10078, ~: 9823)\n[PASS] testFuzz_statusInServiceOverTheWholeRange(uint256) (runs: 512, μ: 27793, ~: 27856)\n[PASS] testFuzz_statusRecoupedForAnythingAtOrAboveTheCap(uint256) (runs: 512, μ: 15343, ~: 15500)\n[PASS] testFuzz_statusRetiredFormatsOneTruncatedDecimal(uint256) (runs: 512, μ: 31897, ~: 28121)\n[PASS] test_aWellBehavedPool4HookWithAfterSwapStillBurns() (gas: 2734062)\n[PASS] test_afterSwapExactInBuyAcceptsOnlyTheExactRemainder() (gas: 189719)\n[PASS] test_afterSwapExactOutSellAcceptsOnlyTheExactGross() (gas: 150733)\n[PASS] test_afterSwapOnAnotherPoolIgnoresTheDeltaEntirely() (gas: 50387)\n[PASS] test_afterSwapZeroFeeStillChecksTheFillAndTouchesNothing() (gas: 193422)\n[PASS] test_beforeSwapCarvesOnlyWhenEthIsTheSpecifiedCurrency() (gas: 82815)\n[PASS] test_beforeSwapOnAnotherPoolIsAlwaysZero() (gas: 23576)\n[PASS] test_burnableNeverDipsIntoTheCapWhenBurnsAndSwapsInterleave() (gas: 3077512)\n[PASS] test_capReachedToTheWeiIsRecoupedAndOneWeiShortIsNot() (gas: 448627)\n[PASS] test_exactOutBuyWorksOnAFreshManagerHoldingNoEth() (gas: 10713838)\n[PASS] test_feeIsTheSameWhenTheRouterSettlesWithClaims() (gas: 493511)\n[PASS] test_quoteAtTheTickExtremesDoesNotRevert() (gas: 27878)\n[PASS] test_reentrantPool4CannotBurnAgainDuringTheBurn() (gas: 2703303)\n[PASS] test_reentrantPool4CannotMoveTheAnchorDuringTheBurn() (gas: 2680499)\n[PASS] test_reentrantPool4CannotRetireDuringTheBurn() (gas: 3129383)\n[PASS] test_retireDoesNotTreatAZeroOwnerAsRetired() (gas: 657105)\n[PASS] test_retireIsAtomicWhenTheCreatorRejectsEth() (gas: 1167213)\n[PASS] test_retireRecoversAfterARefusal() (gas: 927406)\n[PASS] test_retireRefusesATransferThatLandsSomewhereElse() (gas: 529745)\n[PASS] test_retireRefusesAnOwnerWordWithDirtyUpperBits() (gas: 662929)\n[PASS] test_retireSurfacesAStringRevertFromTheMedallion() (gas: 481224)\n[PASS] test_retireThenBurnEverythingLeavesNoClaims() (gas: 3245760)\n[PASS] test_sameTokenOtherFeeTierPaysNoFee() (gas: 917972)\n[PASS] test_statusTruncatesAndNeverRounds() (gas: 63477)\nSuite result: ok. 33 passed; 0 failed; 0 skipped; finished in 108.70ms (512.72ms CPU time)\n\nRan 28 tests for test/MedallionBurn.t.sol:MedallionBurnTest\n[PASS] testFuzz_batchIsMinOfBurnableAndCap(uint256) (runs: 256, μ: 2540967, ~: 2541128)\n[PASS] test_anchorIsClampedToLastRefPlusMinusTheBand() (gas: 3606957)\n[PASS] test_anchorStepsAtMost200PerBlockTowardThePlainSpot() (gas: 2650745)\n[PASS] test_burnsAreSpacedByFiveBlocks() (gas: 2675200)\n[PASS] test_burnsNeverTouchTheCreatorsReserve() (gas: 2962968)\n[PASS] test_callerCannotChooseTheBatchOnlyRaiseTheFloor() (gas: 2676541)\n[PASS] test_callerGetsNothing() (gas: 2501409)\n[PASS] test_constructorSeedsTheAnchorWhenPool4Answers() (gas: 15442086)\n[PASS] test_fallbackBatchIsCappedAtOneHundredth() (gas: 2554765)\n[PASS] test_fallbackBurnStepsTheAnchorItselfWhenFirstInTheBlock() (gas: 2689739)\n[PASS] test_fallbackBurnUsesTheStartOfBlockAnchor() (gas: 2760662)\n[PASS] test_fallbackGuardHoldsUntilTheAnchorCatchesDown() (gas: 2972354)\n[PASS] test_fallbackRefusesPool4AndNeedsASeed() (gas: 2707350)\n[PASS] test_normalBurnReseedsEverything() (gas: 2830893)\n[PASS] test_normalBurnSpendsTheWholeRemainderWhenBelowTheBatch() (gas: 2497942)\n[PASS] test_normalBurnViaPlainPool() (gas: 2530822)\n[PASS] test_normalBurnViaPool4() (gas: 2660880)\n[PASS] test_nothingToBurnBeforeTheCapAndBelowMinBurn() (gas: 2626810)\n[PASS] test_partialFillRevertsWhenLiquidityRunsOut() (gas: 2125266)\n[PASS] test_pokeAnchorReseedsFromPool4WhenItAnswers() (gas: 2664906)\n[PASS] test_pool4ReferenceRejectsMalformedAnswers() (gas: 904154)\n[PASS] test_priceOffReferenceOnThePlainPoolUses300InNormalMode() (gas: 2633248)\n[PASS] test_priceOffReferenceViaPool4IsOneSided() (gas: 2799721)\n[PASS] test_sepoliaLikeChainHasNoPool4AndNoFallback() (gas: 488276)\n[PASS] test_slippageFloorRefusesAThinPool() (gas: 2402472)\n[PASS] test_statusCountsBurnedIMDAfterRetirement() (gas: 3114326)\n[PASS] test_tooSoonComesFirst() (gas: 58866)\n[PASS] test_uninitializedPlainPoolIsRefused() (gas: 1568982)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 131.64ms (202.41ms CPU time)\n\nRan 1 test for test/MedallionInvariant.t.sol:MedallionInvariantTest\n[PASS]\nMedallionInvariantTest invariants:\n[PASS] invariant_burnableFollowsTheFormula\n[PASS] invariant_claimsCoverTheLedger\n[PASS] invariant_creatorPaidIsZeroOrCap\n[PASS] invariant_hookNeverHoldsTokensOrEth\n MedallionInvariantTest invariants (runs: 32, calls: 768, reverts: 7)\n\n╭------------------+------------------+-------+---------+----------╮\n| Contract         | Selector         | Calls | Reverts | Discards |\n+==================================================================+\n| MedallionHandler | burn             | 158   | 7       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | closeOrOpenPool4 | 151   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | donateClaims     | 163   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | retire           | 149   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | swap             | 147   | 0       | 0        |\n╰------------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 230.62ms (204.36ms CPU time)\n\nRan 1 test for test/MedallionLedgerInvariant.t.sol:MedallionLedgerInvariantTest\n[PASS]\nMedallionLedgerInvariantTest invariants:\n[PASS] invariant_anchorStaysInsideItsBands\n[PASS] invariant_burnLedgerMatchesTheGhosts\n[PASS] invariant_claimsEqualTheLedgerPlusDonations\n[PASS] invariant_creatorIsPaidOnceExactlyTheCapAndOnlyByRetire\n[PASS] invariant_hookHoldsNothingButEthClaims\n[PASS] invariant_launchPoolNeverChanges\n[PASS] invariant_noViolationsAndNoUnexpectedReverts\n[PASS] invariant_statusMatchesTheLedger\n[PASS] invariant_totalFeesIsExactlyWhatSwapsMinted\n MedallionLedgerInvariantTest invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭---------------+--------------------+-------+---------+----------╮\n| Contract      | Selector           | Calls | Reverts | Discards |\n+=================================================================+\n| LedgerHandler | burn               | 171   | 0       | 0        |\n|---------------+--------------------+-------+---------+----------|\n| LedgerHandler | buyExactInRandom   | 167   | 0       | 0        |\n|---------------+--------------------+-------+---------+----------|\n| LedgerHandler | buyExactOutRandom  | 185   | 0       | 0        |\n|---------------+--------------------+-------+---------+----------|\n| LedgerHandler | donateClaims       | 169   | 0       | 0        |\n|---------------+--------------------+-------+---------+----------|\n| LedgerHandler | poke               | 171   | 0       | 0        |\n|---------------+--------------------+-------+---------+----------|\n| LedgerHandler | pushPlain          | 215   | 0       | 0        |\n|---------------+--------------------+-------+---------+----------|\n| LedgerHandler | retire             | 178   | 0       | 0        |\n|---------------+--------------------+-------+---------+----------|\n| LedgerHandler | roll               | 158   | 0       | 0        |\n|---------------+--------------------+-------+---------+----------|\n| LedgerHandler | sellExactInRandom  | 159   | 0       | 0        |\n|---------------+--------------------+-------+---------+----------|\n| LedgerHandler | sellExactOutRandom | 184   | 0       | 0        |\n|---------------+--------------------+-------+---------+----------|\n| LedgerHandler | setPool4           | 163   | 0       | 0        |\n╰---------------+--------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 784.69ms (756.92ms CPU time)\n\nRan 7 test suites in 788.79ms (1.42s CPU time): 124 tests passed, 0 failed, 0 skipped (124 total tests)\n","passed":true},{"durationMs":53,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"FareToken.approve(address,uint256)\",\"FareToken.burn(uint256)\",\"FareToken.burnFrom(address,uint256)\",\"FareToken.transfer(address,uint256)\",\"FareToken.transferFrom(address,address,uint256)\",\"MedallionHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"MedallionHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MedallionHook.burnIMD(bool,uint256)\",\"MedallionHook.pokeAnchor()\",\"MedallionHook.retire()\",\"MedallionHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":3,\"README.md\":186,\"REVIEW.md\":67,\"docs/DESIGN.md\":124,\"docs/OPERATIONS.md\":70,\"docs/abi/FareToken.json\":337,\"docs/abi/MedallionHook.json\":1383,\"foundry.toml\":33,\"launch.json\":29,\"remappings.txt\":4,\"src/FareToken.sol\":97,\"src/MedallionHook.sol\":611,\"test/FareToken.t.sol\":171,\"test/MedallionAdversarial.t.sol\":668,\"test/MedallionBurn.t.sol\":524,\"test/MedallionHook.t.sol\":523,\"test/MedallionInvariant.t.sol\":126,\"test/MedallionLedgerInvariant.t.sol\":486,\"test/MedallionRetire.t.sol\":196,\"test/mocks/AdversarialMocks.sol\":107,\"test/mocks/MockERC20.sol\":49,\"test/mocks/MockMedallion.sol\":100,\"test/mocks/MockPool4Hook.sol\":44,\"test/utils/HookMiner.sol\":27,\"test/utils/MedallionTestBase.sol\":254},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"2fa8708c8a95b456aae4b84a8181b5f7bb24437101f2a4c319c704fd9051a5b7","verifiedTreeHash":"73a419df83a5537afa6f39dcb300cb866db9d0ab","verifierVersion":"0.1.0+da6bdbe5"},{"checks":[{"durationMs":3105,"exitCode":0,"name":"build","output":"Compiling 72 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.94s\nCompiler run successful!\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:309:53\n    │\n309 │         if (MEDALLION_NFT.code.length == 0) return (false, address(0));\n    │                                                     ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:312:47\n    │\n312 │         if (!ok || data.length != 32) return (false, address(0));\n    │                                               ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:315:60\n    │\n315 │         if (word == 0 || word > type(uint160).max) return (false, address(0));\n    │                                                            ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:316:17\n    │\n316 │         return (true, address(uint160(word)));\n    │                 ━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:349:50\n    │\n349 │         if (POOL4_HOOK.code.length == 0) return (false, 0);\n    │                                                  ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:351:47\n    │\n351 │         if (!ok || data.length != 32) return (false, 0);\n    │                                               ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:354:32\n    │\n354 │         if (open != 1) return (false, 0);\n    │                                ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:356:47\n    │\n356 │         if (!ok || data.length != 32) return (false, 0);\n    │                                               ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:359:75\n    │\n359 │         if (tick < TickMath.MIN_TICK || tick > TickMath.MAX_TICK) return (false, 0);\n    │                                                                           ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:360:17\n    │\n360 │         return (true, int24(tick));\n    │                 ━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[missing-events-arithmetic]: `totalFees` is changed without an event but is used in arithmetic\n    ╭▸ src/MedallionHook.sol:302:9\n    │\n302 │         totalFees = previous + fee;\n    │         ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-arithmetic\n\nwarning[missing-events-arithmetic]: `imdBurned` is changed without an event but is used in arithmetic\n    ╭▸ src/MedallionHook.sol:251:9\n    │\n251 │         imdBurned += out;\n    │         ━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-arithmetic\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:303:65\n    │\n303 │         if (previous < CREATOR_CAP && totalFees >= CREATOR_CAP) emit Recouped(totalFees, block.number);\n    │                                                                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:141:63\n    │\n141 │         return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(fee)), 0), 0);\n    │                                                               ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:141:70\n    │\n141 │         return (IHooks.beforeSwap.selector, toBeforeSwapDelta(int128(uint128(fee)), 0), 0);\n    │                                                                      ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:152:69\n    │\n152 │             if (int256(delta.amount0()) != params.amountSpecified + int256(_specifiedFee(params))) {\n    │                                                                     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:160:44\n    │\n160 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                            ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:160:51\n    │\n160 │         return (IHooks.afterSwap.selector, int128(uint128(fee)));\n    │                                                   ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `retired` is updated\n    ╭▸ src/MedallionHook.sol:179:46\n    │\n179 │               (bool ok, bytes memory reason) = MEDALLION_NFT.call(\n    │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n180 │ ┃                 abi.encodeWithSignature(\"transferFrom(address,address,uint256)\", owner, DEAD, MEDALLION_ID)\n181 │ ┃             );\n    │ ┗━━━━━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:185:13\n    │\n185 │             emit MedallionRetired(owner);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:190:9\n    │\n190 │         emit CreatorPaid(CREATOR, CREATOR_CAP);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:191:9\n    │\n191 │         emit LastFare(MEDALLION_ID, LAST_FARE_HASH, LAST_FARE);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:217:9\n    │\n217 │         emit IMDBurned(amount, out, viaPool4);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:230:9\n    │\n230 │         emit AnchorUpdated(lastRef, anchor, normal);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:247:68\n    │\n247 │             poolManager.swap(_burnKey(viaPool4), SwapParams(true, -int256(amount), TickMath.MIN_SQRT_PRICE + 1), \"\");\n    │                                                                    ━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:248:41\n    │\n248 │         if (int256(delta.amount0()) != -int256(amount) || delta.amount1() <= 0) revert PartialFill();\n    │                                         ━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:249:23\n    │\n249 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:249:31\n    │\n249 │         uint256 out = uint256(uint128(delta.amount1()));\n    │                               ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:292:19\n    │\n292 │         if (fee > uint256(uint128(type(int128).max))) revert FeeTooLarge();\n    │                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:292:27\n    │\n292 │         if (fee > uint256(uint128(type(int128).max))) revert FeeTooLarge();\n    │                           ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:296:24\n    │\n296 │         return n < 0 ? uint256(-(n + 1)) + 1 : uint256(n);\n    │                        ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:296:48\n    │\n296 │         return n < 0 ? uint256(-(n + 1)) + 1 : uint256(n);\n    │                                                ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:316:31\n    │\n316 │         return (true, address(uint160(word)));\n    │                               ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint160' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:360:23\n    │\n360 │         return (true, int24(tick));\n    │                       ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int24' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:383:22\n    │\n383 │             anchor = int24(int256(anchor) + movement);\n    │                      ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int24' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MedallionHook.sol:404:15\n    │\n404 │             ++digits;\n    │               ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:408:34\n    │\n408 │             s[--digits] = bytes1(uint8(48 + n % 10));\n    │                                  ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionHook.t.sol:116:17\n    │\n116 │         vm.roll(block.number + 5);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionHook.t.sol:461:21\n    │\n461 │             vm.roll(block.number + 5);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionHook.t.sol:474:21\n    │\n474 │             vm.roll(block.number + 5);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionHook.t.sol:488:17\n    │\n488 │         vm.roll(block.number + 5);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionHook.t.sol:496:17\n    │\n496 │         vm.roll(block.number + 4);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionHook.t.sol:691:21\n    │\n691 │             vm.roll(block.number + 5);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":842,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/FareToken.t.sol:FareTokenTest\n[PASS] testFuzz_SelfTransferDoesNotChangeBalance(uint256) (runs: 256, μ: 41747, ~: 41461)\nLogs:\n  Bound result 4041\n\n[PASS] testFuzz_TransfersAndBurnsConserveSupply(uint256,uint256) (runs: 256, μ: 131540, ~: 132143)\nLogs:\n  Bound result 96\n  Bound result 27\n\n[PASS] test_BurnAndBurnFromReduceBalancesAndSupply() (gas: 204834)\n[PASS] test_ConstructorMintsExactWholeSupplyToDeployer() (gas: 57926)\n[PASS] test_InfiniteAllowanceIsNotDecreasedByTransferOrBurn() (gas: 215005)\n[PASS] test_InsufficientAllowanceAndBalanceLeaveStateIntact() (gas: 304110)\n[PASS] test_NoPrivilegedSupplyOrUpgradeFunctions() (gas: 195305)\n[PASS] test_RejectsZeroAddressesAndAllowsZeroValues() (gas: 175123)\n[PASS] test_RuntimeHasNoForbiddenOpcodes() (gas: 714791)\n[PASS] test_TransferAndTransferFromMoveExactAmounts() (gas: 204408)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 11.08ms (16.86ms CPU time)\n\nRan 4 tests for test/MedallionEdge.t.sol:MedallionEdgeTest\n[PASS] test_afterSwapModesAllowPartialFillsAndChargeOnlyActualETH() (gas: 492053)\n[PASS] test_burnCallbackCannotReenterAnyPermissionlessOperation() (gas: 855388)\n[PASS] test_nftOwnerMayDifferFromFixedPaymentBeneficiary() (gas: 508005)\n[PASS] test_referenceMinimumRejectsExcessImpactEvenWithZeroCallerMinimum() (gas: 753446)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 45.58ms (2.59ms CPU time)\n\nRan 18 tests for test/MedallionHook.t.sol:MedallionBurnTest\n[PASS] test_anchorStepsOnlyOnceAfterArbitrarilyLongIdleAndStaysInBand() (gas: 1734044)\n[PASS] test_burnAndRetireRequireTheirOwnUnlock() (gas: 437886)\n[PASS] test_burnCheckOrderingAndCooldown() (gas: 775352)\n[PASS] test_burnSizeIsCappedByRemainderAndMinimum() (gas: 732784)\n[PASS] test_callerMinOutputFailureRollsBackAllAccounting() (gas: 641295)\n[PASS] test_fallbackUsesSmallerBatchAndRefusesPool4Route() (gas: 603300)\n[PASS] test_malformedPool4ViewsFallBackWithoutUnsafeABIDecode() (gas: 1812703)\n[PASS] test_normalBurnsUseOnlyFixedPoolsAndPayOnlySink() (gas: 1263941)\n[PASS] test_normalModeReseedsFromPool4WithoutStalenessRule() (gas: 636719)\n[PASS] test_normalPlainToleranceAndPool4ToleranceDiffer() (gas: 641603)\n[PASS] test_priceGuardIsOneSided() (gas: 771345)\n[PASS] test_quoteFloorIncludesLPFeeAndFourPercentMaximumSlippage() (gas: 548975)\n[PASS] test_reopeningReseedsFallbackBandAndBlockStart() (gas: 507787)\n[PASS] test_retiredStatusTruncatesNonzeroIMDBurnsToOneDecimal() (gas: 873637)\n[PASS] test_sameBlockPokeCannotMoveBurnReference() (gas: 793924)\n[PASS] test_sepoliaKeepsConstantsAndExternalOperationsUnavailable() (gas: 47134577)\n[PASS] test_withoutAnyValidReferenceFallbackIsUnavailable() (gas: 46754920)\n[PASS] test_zeroAndPartialBurnFillsRevertWithoutSpendingClaims() (gas: 1092731)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 70.38ms (94.66ms CPU time)\n\nRan 17 tests for test/MedallionHook.t.sol:MedallionHookTest\n[PASS] testFuzz_exactInBuyFeeRoundsDownAndRemainsSolvent(uint96) (runs: 256, μ: 328182, ~: 326907)\nLogs:\n  Bound result 914884192206149072138\n\n[PASS] test_allFourSwapDirectionsChargeTwoPercentETHClaims() (gas: 847980)\n[PASS] test_alreadyDeadStillPaysCreator() (gas: 393727)\n[PASS] test_beforeSwapModesRejectPartialFillsAtomically() (gas: 452076)\n[PASS] test_buyWorksWithTokenOnlyLiquidityAndInitiallyNoManagerETH() (gas: 9902630)\n[PASS] test_callbacksAndUnlockRejectUnauthenticatedCallers() (gas: 151592)\n[PASS] test_claimDonationsDoNotIncreaseEntitlementOrBurnable() (gas: 443341)\n[PASS] test_firstNativePoolWinsAndOtherPoolsRemainFeeFree() (gas: 47064402)\n[PASS] test_permissionsConstructorAndPinnedFare() (gas: 81142)\n[PASS] test_recoupmentOnlyOnceAndSwapsNeverPayExternally() (gas: 871644)\n[PASS] test_retirePaymentFailureRollsBackNFTAndAccounting() (gas: 466668)\n[PASS] test_retireReentrancyBlocked() (gas: 404202)\n[PASS] test_retireRejectsUnavailableAndMalformedNFT() (gas: 378940)\n[PASS] test_retireRequiresCapAndApprovalThenPaysExactlyOnce() (gas: 738609)\n[PASS] test_retireRereadsOwnerAndRejectsLyingTransfer() (gas: 301694)\n[PASS] test_runtimeOpcodeWalkSkipsPushData() (gas: 4666276)\n[PASS] test_statusStringsTruncateAndLastFareEmitsOnRetirement() (gas: 576836)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 86.42ms (95.89ms CPU time)\n\nRan 1 test for test/MedallionInvariant.t.sol:MedallionInvariantTest\n[PASS]\nMedallionInvariantTest invariants:\n[PASS] invariant_claimsConserveFeesAndDonations\n[PASS] invariant_everyPurchasedIMDReachesTheSink\n[PASS] invariant_payoutAndRetirementAreAtomic\n MedallionInvariantTest invariants (runs: 64, calls: 4096, reverts: 0)\n\n╭----------------------------+----------+-------+---------+----------╮\n| Contract                   | Selector | Calls | Reverts | Discards |\n+====================================================================+\n| MedallionAccountingHandler | burn     | 1051  | 0       | 0        |\n|----------------------------+----------+-------+---------+----------|\n| MedallionAccountingHandler | donate   | 1001  | 0       | 0        |\n|----------------------------+----------+-------+---------+----------|\n| MedallionAccountingHandler | retire   | 1024  | 0       | 0        |\n|----------------------------+----------+-------+---------+----------|\n| MedallionAccountingHandler | trade    | 1020  | 0       | 0        |\n╰----------------------------+----------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 742.05ms (738.36ms CPU time)\n\nRan 5 test suites in 744.34ms (955.51ms CPU time): 50 tests passed, 0 failed, 0 skipped (50 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"FareToken.approve(address,uint256)\",\"FareToken.burn(uint256)\",\"FareToken.burnFrom(address,uint256)\",\"FareToken.transfer(address,uint256)\",\"FareToken.transferFrom(address,address,uint256)\",\"MedallionHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"MedallionHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MedallionHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"MedallionHook.burnIMD(bool,uint256)\",\"MedallionHook.pokeAnchor()\",\"MedallionHook.retire()\",\"MedallionHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":2,\"README.md\":29,\"docs/OPERATIONS.md\":86,\"docs/SECURITY.md\":43,\"docs/check_release.py\":213,\"foundry.toml\":23,\"launch.json\":27,\"remappings.txt\":4,\"src/FareToken.sol\":89,\"src/MedallionHook.sol\":413,\"test/FareToken.t.sol\":147,\"test/MedallionEdge.t.sol\":100,\"test/MedallionHook.t.sol\":734,\"test/MedallionInvariant.t.sol\":155,\"test/helpers/MedallionMocks.sol\":227},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1714,"exitCode":0,"name":"slither","output":"[medium/medium] reentrancy-no-eth at src/MedallionHook.sol:173: Reentrancy in MedallionHook.retire() (src/MedallionHook.sol#173-192):\n[medium/medium] uninitialized-local at src/MedallionHook.sol:402: MedallionHook._decimal(uint256).digits (src/MedallionHook.sol#402) is a local variable never initialized\n[medium/medium] unused-return at src/MedallionHook.sol:340: MedallionHook._spot(PoolKey) (src/MedallionHook.sol#340-344) ignores return value by (sqrtPrice,spot,None,None) = poolManager.getSlot0(key.toId()) (src/MedallionHook.sol#342)\n[low/medium] events-maths at src/MedallionHook.sol:234: MedallionHook.unlockCallback(bytes) (src/MedallionHook.sol#234-255) should emit an event for: \n[low/medium] reentrancy-benign at src/MedallionHook.sol:173: Reentrancy in MedallionHook.retire() (src/MedallionHook.sol#173-192):\n[low/medium] reentrancy-benign at src/MedallionHook.sol:234: Reentrancy in MedallionHook.unlockCallback(bytes) (src/MedallionHook.sol#234-255):","passed":true},{"durationMs":501,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/MedallionHook.sol:179: Reentrancy: State change after external call (2 places)\n[high] unsafe-casting at src/MedallionHook.sol:141: Unsafe Casting of integers (2 places)\n[low] large-numeric-literal at src/MedallionHook.sol:29: Large Numeric Literal (6 places)\n[low] literal-instead-of-constant at src/FareToken.sol:25: Literal Instead of Constant (20 places)\n[low] missing-inheritance at src/FareToken.sol:8: Missing Inheritance\n[low] state-change-without-event at src/MedallionHook.sol:123: State Change Without Event (2 places)\n[low] unchecked-return at src/MedallionHook.sol:189: Unchecked Return (2 places)","passed":true}],"detail":"launch.json is not a valid launch manifest: hook.contract: expected a contract name; hook.permissions: Invalid input: expected array, received object; token.contract: expected a contract name; pool.initialPrice: Invalid input: expected string, received number","evaluation":"checks","profile":"foundry","status":"rejected","submissionHash":"7ebbfbe39da35d994df9a3f17d4547f3ca513a5db0c6d03e2e6ca6c7f671ec20","verifiedTreeHash":"c14a6e5cdab55fe5552199cde8439d07b0c4215a","verifierVersion":"0.1.0+da6bdbe5"},{"checks":[{"durationMs":3564,"exitCode":0,"name":"build","output":"Compiling 82 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.41s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/utils/MedallionTestBase.sol:225:5:\n    |\n225 |     function lastBurnLog() internal returns (BurnLog memory log_) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:427:21\n    │\n427 │             return (false, pool4Ref);\n    │                     ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:431:17\n    │\n431 │         return (true, blockAnchor);\n    │                 ━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:542:46\n    │\n542 │         if (!ok || ret.length != 32) return (false, 0);\n    │                                              ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:544:32\n    │\n544 │         if (flag != 1) return (false, 0);\n    │                                ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:547:46\n    │\n547 │         if (!ok || ret.length != 32) return (false, 0);\n    │                                              ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:549:75\n    │\n549 │         if (tick < TickMath.MIN_TICK || tick > TickMath.MAX_TICK) return (false, 0);\n    │                                                                           ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:550:17\n    │\n550 │         return (true, int24(tick));\n    │                 ━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MedallionHook.sol:300:44\n    │\n300 │         return (IHooks.afterSwap.selector, unspecifiedDelta);\n    │                                            ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:297:13\n    │\n297 │             emit FeeCollected(params.zeroForOne, fee, updated);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:298:65\n    │\n298 │             if (before < CREATOR_CAP && updated >= CREATOR_CAP) emit Recouped(updated, block.number);\n    │                                                                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:285:61\n    │\n285 │             if (int256(amount0) != params.amountSpecified + int256(fee)) revert PartialFill();\n    │                                                             ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:43\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                           ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:51\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                   ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:72\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                                        ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:80\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                                                ━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `retired` is updated\n    ╭▸ src/MedallionHook.sol:387:17\n    │\n387 │                 MEDALLION_NFT.call(abi.encodeWithSelector(0x23b872dd, owner, DEAD, MEDALLION_ID));\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:390:13\n    │\n390 │             emit MedallionRetired(owner);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:396:9\n    │\n396 │         emit CreatorPaid(CREATOR, CREATOR_CAP);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:397:9\n    │\n397 │         emit LastFare(MEDALLION_ID, LAST_FARE_HASH, LAST_FARE);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/MedallionHook.sol:395:9\n    │\n395 │         poolManager.unlock(abi.encode(ACTION_PAY_CREATOR, bytes(\"\")));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:560:9\n    │\n560 │         emit AnchorSeeded(ref, block.number);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:584:9\n    │\n584 │         emit AnchorStepped(from, to, target, block.number);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:454:9\n    │\n454 │         emit IMDBurned(viaPool4, fallbackMode, batch, imdOut, ref, spot);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:495:57\n    │\n495 │                     zeroForOne: true, amountSpecified: -int256(batch), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n    │                                                         ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:499:45\n    │\n499 │             if (int256(delta.amount0()) != -int256(batch) || delta.amount1() <= 0) revert PartialFill();\n    │                                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:500:27\n    │\n500 │             uint256 out = uint256(uint128(delta.amount1()));\n    │                           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:500:35\n    │\n500 │             uint256 out = uint256(uint128(delta.amount1()));\n    │                                   ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:524:55\n    │\n524 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:524:90\n    │\n524 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                                                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:535:24\n    │\n535 │         return address(uint160(word));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint160' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:550:23\n    │\n550 │         return (true, int24(tick));\n    │                       ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int24' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:593:40\n    │\n593 │             fracDigits[i - 1] = bytes1(uint8(48 + frac % 10));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MedallionHook.sol:603:13\n    │\n603 │             digits++;\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:607:36\n    │\n607 │             out[--digits] = bytes1(uint8(48 + v % 10));\n    │                                    ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/MedallionBurn.t.sol:26:17\n   │\n26 │         vm.roll(block.number + 5);\n   │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:131:17\n    │\n131 │         vm.roll(block.number + 4);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:336:17\n    │\n336 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:363:21\n    │\n363 │             vm.roll(block.number + 1);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:422:17\n    │\n422 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:430:21\n    │\n430 │             vm.roll(block.number + 1);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:446:17\n    │\n446 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:502:21\n    │\n502 │             vm.roll(block.number + 5);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/MedallionInvariant.t.sol:60:17\n   │\n60 │         vm.roll(block.number + bound(blocks, 0, 7));\n   │         ────────━━━━━━━━━━━━─────────────────────── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":3434,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 11 tests for test/MedallionRetire.t.sol:MedallionRetireTest\n[PASS] test_creatorPaidIsZeroOrCap() (gas: 916980)\n[PASS] test_retireCannotRunTwice() (gas: 921526)\n[PASS] test_retireFeesKeepAccruingAfterRetirement() (gas: 1095274)\n[PASS] test_retireIsGuardedAgainstReentrancyFromTheMedallion() (gas: 547777)\n[PASS] test_retireIsRefusedWhenTheTransferSilentlyDoesNothing() (gas: 837073)\n[PASS] test_retireMovesTheMedallionPaysTheCreatorAndEmitsInOneTransaction() (gas: 1235974)\n[PASS] test_retireRevertsBeforeTheCap() (gas: 755489)\n[PASS] test_retireRevertsOnABadOwnerOfAnswer() (gas: 799562)\n[PASS] test_retireRevertsWhenTheMedallionHasNoCode() (gas: 270443)\n[PASS] test_retireSkipsTheTransferWhenTheMedallionIsAlreadyDead() (gas: 850929)\n[PASS] test_retireWithoutApprovalIsRefusedAndChangesNothing() (gas: 797952)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 4.76ms (5.37ms CPU time)\n\nRan 17 tests for test/FareToken.t.sol:FareTokenTest\n[PASS] testFuzz_burnFromAccounting(uint256,uint256) (runs: 256, μ: 98404, ~: 110143)\n[PASS] testFuzz_transferConservesSupply(address,uint256) (runs: 256, μ: 85666, ~: 85774)\n[PASS] test_approveAndTransferFrom() (gas: 132174)\n[PASS] test_approveZeroSpenderReverts() (gas: 30329)\n[PASS] test_burnFromUsesAllowance() (gas: 144974)\n[PASS] test_burnReducesSupply() (gas: 54024)\n[PASS] test_burnRevertsOnInsufficientBalance() (gas: 34873)\n[PASS] test_constructorEmitsMintTransfer() (gas: 11158)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 117211)\n[PASS] test_metadata() (gas: 23923)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 20624)\n[PASS] test_noMintOwnerPauseOrUpgradeSurface() (gas: 283133)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 486845)\n[PASS] test_transfer() (gas: 81905)\n[PASS] test_transferFromRevertsOnInsufficientAllowance() (gas: 85116)\n[PASS] test_transferRevertsOnInsufficientBalance() (gas: 37663)\n[PASS] test_transferRevertsToZeroAddress() (gas: 30405)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 26.42ms (21.17ms CPU time)\n\nRan 33 tests for test/MedallionHook.t.sol:MedallionHookTest\n[PASS] testFuzz_feeIsTwoPercentInEveryShape(uint256,uint8) (runs: 256, μ: 285294, ~: 283115)\n[PASS] testFuzz_statusFormatsTwoTruncatedDecimals(uint256) (runs: 256, μ: 241460, ~: 241230)\n[PASS] test_afterSwapShapesTolerateAPriceLimitAndStillChargeOnGross() (gas: 226845)\n[PASS] test_burnableIsZeroUntilTheCapAndGrowsAfter() (gas: 772418)\n[PASS] test_buyWorksOnAFreshManagerSeededWithTokensOnly() (gas: 10708837)\n[PASS] test_callbacksRefuseCallersOtherThanThePoolManager() (gas: 111250)\n[PASS] test_constructorRejectsAnAddressWithoutTheFlags() (gas: 7605)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 3942)\n[PASS] test_constructorSetsLastBurnBlockAndLeavesAnchorUnseeded() (gas: 22764)\n[PASS] test_donatedClaimsDoNotCountAsFeesAndKeepTheInvariant() (gas: 548463)\n[PASS] test_exactInBuyPartialFillReverts() (gas: 153503)\n[PASS] test_exactInBuyTakesTwoPercentOfEthInAsClaims() (gas: 233975)\n[PASS] test_exactInSellTakesTwoPercentOfGrossEthAsClaims() (gas: 221297)\n[PASS] test_exactOutBuyTakesTwoPercentOfGrossEthAsClaims() (gas: 234343)\n[PASS] test_exactOutSellPartialFillReverts() (gas: 132773)\n[PASS] test_exactOutSellTakesTwoPercentOfEthOutAsClaims() (gas: 222450)\n[PASS] test_feeIsNeverTakenInTokens() (gas: 700444)\n[PASS] test_firstNativePoolIsTheLaunchPool() (gas: 17339)\n[PASS] test_lastFareIsPinned() (gas: 13380)\n[PASS] test_noAdminSurface() (gas: 234481)\n[PASS] test_otherPoolsWithTheHookAreFeeFree() (gas: 1615026)\n[PASS] test_permissionsAreExactlyTheFiveFlags() (gas: 11334)\n[PASS] test_publicConstants() (gas: 112834)\n[PASS] test_quoteIsAmountTimesPrice() (gas: 32710)\n[PASS] test_recoupedEmittedOnTheCrossingSwapWithTheOvershoot() (gas: 367502)\n[PASS] test_recoupedIsEmittedExactlyOnce() (gas: 707458)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 3962819)\n[PASS] test_statusInService() (gas: 591328)\n[PASS] test_statusRecoupedNotRetired() (gas: 409151)\n[PASS] test_swapsOnlyEverAddToTotalFees() (gas: 1105217)\n[PASS] test_tinySwapBelowFeeGranularityIsFree() (gas: 175937)\n[PASS] test_tokenTokenPoolDoesNotBecomeLaunchPoolAndNeverReverts() (gas: 10843064)\n[PASS] test_unlockCallbackRejectsUnknownActions() (gas: 36335)\nSuite result: ok. 33 passed; 0 failed; 0 skipped; finished in 48.72ms (153.64ms CPU time)\n\nRan 28 tests for test/MedallionBurn.t.sol:MedallionBurnTest\n[PASS] testFuzz_batchIsMinOfBurnableAndCap(uint256) (runs: 256, μ: 2540983, ~: 2541128)\n[PASS] test_anchorIsClampedToLastRefPlusMinusTheBand() (gas: 3606957)\n[PASS] test_anchorStepsAtMost200PerBlockTowardThePlainSpot() (gas: 2650745)\n[PASS] test_burnsAreSpacedByFiveBlocks() (gas: 2675200)\n[PASS] test_burnsNeverTouchTheCreatorsReserve() (gas: 2962968)\n[PASS] test_callerCannotChooseTheBatchOnlyRaiseTheFloor() (gas: 2676541)\n[PASS] test_callerGetsNothing() (gas: 2501409)\n[PASS] test_constructorSeedsTheAnchorWhenPool4Answers() (gas: 15442086)\n[PASS] test_fallbackBatchIsCappedAtOneHundredth() (gas: 2554765)\n[PASS] test_fallbackBurnStepsTheAnchorItselfWhenFirstInTheBlock() (gas: 2689739)\n[PASS] test_fallbackBurnUsesTheStartOfBlockAnchor() (gas: 2760662)\n[PASS] test_fallbackGuardHoldsUntilTheAnchorCatchesDown() (gas: 2972354)\n[PASS] test_fallbackRefusesPool4AndNeedsASeed() (gas: 2707350)\n[PASS] test_normalBurnReseedsEverything() (gas: 2830893)\n[PASS] test_normalBurnSpendsTheWholeRemainderWhenBelowTheBatch() (gas: 2497942)\n[PASS] test_normalBurnViaPlainPool() (gas: 2530822)\n[PASS] test_normalBurnViaPool4() (gas: 2660880)\n[PASS] test_nothingToBurnBeforeTheCapAndBelowMinBurn() (gas: 2626810)\n[PASS] test_partialFillRevertsWhenLiquidityRunsOut() (gas: 2125266)\n[PASS] test_pokeAnchorReseedsFromPool4WhenItAnswers() (gas: 2664906)\n[PASS] test_pool4ReferenceRejectsMalformedAnswers() (gas: 904154)\n[PASS] test_priceOffReferenceOnThePlainPoolUses300InNormalMode() (gas: 2633248)\n[PASS] test_priceOffReferenceViaPool4IsOneSided() (gas: 2799721)\n[PASS] test_sepoliaLikeChainHasNoPool4AndNoFallback() (gas: 488276)\n[PASS] test_slippageFloorRefusesAThinPool() (gas: 2402472)\n[PASS] test_statusCountsBurnedIMDAfterRetirement() (gas: 3114326)\n[PASS] test_tooSoonComesFirst() (gas: 58866)\n[PASS] test_uninitializedPlainPoolIsRefused() (gas: 1568982)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 103.09ms (169.20ms CPU time)\n\nRan 1 test for test/MedallionInvariant.t.sol:MedallionInvariantTest\n[PASS]\nMedallionInvariantTest invariants:\n[PASS] invariant_burnableFollowsTheFormula\n[PASS] invariant_claimsCoverTheLedger\n[PASS] invariant_creatorPaidIsZeroOrCap\n[PASS] invariant_hookNeverHoldsTokensOrEth\n MedallionInvariantTest invariants (runs: 32, calls: 768, reverts: 0)\n\n╭------------------+------------------+-------+---------+----------╮\n| Contract         | Selector         | Calls | Reverts | Discards |\n+==================================================================+\n| MedallionHandler | burn             | 132   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | closeOrOpenPool4 | 139   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | donateClaims     | 110   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | retire           | 124   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | setUp            | 152   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | swap             | 111   | 0       | 0        |\n╰------------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.34s (3.32s CPU time)\n\nRan 5 test suites in 3.34s (3.52s CPU time): 90 tests passed, 0 failed, 0 skipped (90 total tests)\n","passed":true},{"durationMs":47,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"FareToken.approve(address,uint256)\",\"FareToken.burn(uint256)\",\"FareToken.burnFrom(address,uint256)\",\"FareToken.transfer(address,uint256)\",\"FareToken.transferFrom(address,address,uint256)\",\"MedallionHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"MedallionHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MedallionHook.burnIMD(bool,uint256)\",\"MedallionHook.pokeAnchor()\",\"MedallionHook.retire()\",\"MedallionHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":3,\"README.md\":186,\"REVIEW.md\":67,\"docs/DESIGN.md\":124,\"docs/OPERATIONS.md\":70,\"docs/abi/FareToken.json\":337,\"docs/abi/MedallionHook.json\":1383,\"foundry.toml\":33,\"launch.json\":29,\"remappings.txt\":4,\"src/FareToken.sol\":97,\"src/MedallionHook.sol\":611,\"test/FareToken.t.sol\":171,\"test/MedallionBurn.t.sol\":524,\"test/MedallionHook.t.sol\":523,\"test/MedallionInvariant.t.sol\":121,\"test/MedallionRetire.t.sol\":196,\"test/mocks/MockERC20.sol\":49,\"test/mocks/MockMedallion.sol\":100,\"test/mocks/MockPool4Hook.sol\":44,\"test/utils/HookMiner.sol\":27,\"test/utils/MedallionTestBase.sol\":254},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"92625eedb55942737741595ff70337f1f8acca479673352b0c7ddee4e99e03da","verifiedTreeHash":"7d86d9e8b5d3bb087ee6be9650842a5aadae7555","verifierVersion":"0.1.0+da6bdbe5"},{"checks":[{"durationMs":3690,"exitCode":0,"name":"build","output":"Compiling 82 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.52s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/utils/MedallionTestBase.sol:225:5:\n    |\n225 |     function lastBurnLog() internal returns (BurnLog memory log_) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:427:21\n    │\n427 │             return (false, pool4Ref);\n    │                     ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:431:17\n    │\n431 │         return (true, blockAnchor);\n    │                 ━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:542:46\n    │\n542 │         if (!ok || ret.length != 32) return (false, 0);\n    │                                              ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:544:32\n    │\n544 │         if (flag != 1) return (false, 0);\n    │                                ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:547:46\n    │\n547 │         if (!ok || ret.length != 32) return (false, 0);\n    │                                              ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:549:75\n    │\n549 │         if (tick < TickMath.MIN_TICK || tick > TickMath.MAX_TICK) return (false, 0);\n    │                                                                           ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:550:17\n    │\n550 │         return (true, int24(tick));\n    │                 ━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MedallionHook.sol:300:44\n    │\n300 │         return (IHooks.afterSwap.selector, unspecifiedDelta);\n    │                                            ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:297:13\n    │\n297 │             emit FeeCollected(params.zeroForOne, fee, updated);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:298:65\n    │\n298 │             if (before < CREATOR_CAP && updated >= CREATOR_CAP) emit Recouped(updated, block.number);\n    │                                                                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:285:61\n    │\n285 │             if (int256(amount0) != params.amountSpecified + int256(fee)) revert PartialFill();\n    │                                                             ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:43\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                           ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:51\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                   ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:72\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                                        ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:287:80\n    │\n287 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                                                ━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `retired` is updated\n    ╭▸ src/MedallionHook.sol:387:17\n    │\n387 │                 MEDALLION_NFT.call(abi.encodeWithSelector(0x23b872dd, owner, DEAD, MEDALLION_ID));\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:390:13\n    │\n390 │             emit MedallionRetired(owner);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:396:9\n    │\n396 │         emit CreatorPaid(CREATOR, CREATOR_CAP);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:397:9\n    │\n397 │         emit LastFare(MEDALLION_ID, LAST_FARE_HASH, LAST_FARE);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/MedallionHook.sol:395:9\n    │\n395 │         poolManager.unlock(abi.encode(ACTION_PAY_CREATOR, bytes(\"\")));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:560:9\n    │\n560 │         emit AnchorSeeded(ref, block.number);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:584:9\n    │\n584 │         emit AnchorStepped(from, to, target, block.number);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:454:9\n    │\n454 │         emit IMDBurned(viaPool4, fallbackMode, batch, imdOut, ref, spot);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:495:57\n    │\n495 │                     zeroForOne: true, amountSpecified: -int256(batch), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n    │                                                         ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:499:45\n    │\n499 │             if (int256(delta.amount0()) != -int256(batch) || delta.amount1() <= 0) revert PartialFill();\n    │                                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:500:27\n    │\n500 │             uint256 out = uint256(uint128(delta.amount1()));\n    │                           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:500:35\n    │\n500 │             uint256 out = uint256(uint128(delta.amount1()));\n    │                                   ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:524:55\n    │\n524 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:524:90\n    │\n524 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                                                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:535:24\n    │\n535 │         return address(uint160(word));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint160' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:550:23\n    │\n550 │         return (true, int24(tick));\n    │                       ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int24' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:593:40\n    │\n593 │             fracDigits[i - 1] = bytes1(uint8(48 + frac % 10));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MedallionHook.sol:603:13\n    │\n603 │             digits++;\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:607:36\n    │\n607 │             out[--digits] = bytes1(uint8(48 + v % 10));\n    │                                    ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/MedallionBurn.t.sol:26:17\n   │\n26 │         vm.roll(block.number + 5);\n   │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:131:17\n    │\n131 │         vm.roll(block.number + 4);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:336:17\n    │\n336 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:363:21\n    │\n363 │             vm.roll(block.number + 1);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:422:17\n    │\n422 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:430:21\n    │\n430 │             vm.roll(block.number + 1);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:446:17\n    │\n446 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:502:21\n    │\n502 │             vm.roll(block.number + 5);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/MedallionInvariant.t.sol:60:17\n   │\n60 │         vm.roll(block.number + bound(blocks, 0, 7));\n   │         ────────━━━━━━━━━━━━─────────────────────── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":3593,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 17 tests for test/FareToken.t.sol:FareTokenTest\n[PASS] testFuzz_burnFromAccounting(uint256,uint256) (runs: 256, μ: 94576, ~: 84525)\n[PASS] testFuzz_transferConservesSupply(address,uint256) (runs: 256, μ: 85441, ~: 85774)\n[PASS] test_approveAndTransferFrom() (gas: 132174)\n[PASS] test_approveZeroSpenderReverts() (gas: 30329)\n[PASS] test_burnFromUsesAllowance() (gas: 144974)\n[PASS] test_burnReducesSupply() (gas: 54024)\n[PASS] test_burnRevertsOnInsufficientBalance() (gas: 34873)\n[PASS] test_constructorEmitsMintTransfer() (gas: 11158)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 117211)\n[PASS] test_metadata() (gas: 23923)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 20624)\n[PASS] test_noMintOwnerPauseOrUpgradeSurface() (gas: 283133)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 486845)\n[PASS] test_transfer() (gas: 81905)\n[PASS] test_transferFromRevertsOnInsufficientAllowance() (gas: 85116)\n[PASS] test_transferRevertsOnInsufficientBalance() (gas: 37663)\n[PASS] test_transferRevertsToZeroAddress() (gas: 30405)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 10.38ms (17.34ms CPU time)\n\nRan 11 tests for test/MedallionRetire.t.sol:MedallionRetireTest\n[PASS] test_creatorPaidIsZeroOrCap() (gas: 916980)\n[PASS] test_retireCannotRunTwice() (gas: 921526)\n[PASS] test_retireFeesKeepAccruingAfterRetirement() (gas: 1095274)\n[PASS] test_retireIsGuardedAgainstReentrancyFromTheMedallion() (gas: 547777)\n[PASS] test_retireIsRefusedWhenTheTransferSilentlyDoesNothing() (gas: 837073)\n[PASS] test_retireMovesTheMedallionPaysTheCreatorAndEmitsInOneTransaction() (gas: 1235974)\n[PASS] test_retireRevertsBeforeTheCap() (gas: 755489)\n[PASS] test_retireRevertsOnABadOwnerOfAnswer() (gas: 799562)\n[PASS] test_retireRevertsWhenTheMedallionHasNoCode() (gas: 270443)\n[PASS] test_retireSkipsTheTransferWhenTheMedallionIsAlreadyDead() (gas: 850929)\n[PASS] test_retireWithoutApprovalIsRefusedAndChangesNothing() (gas: 797952)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 35.28ms (8.08ms CPU time)\n\nRan 33 tests for test/MedallionHook.t.sol:MedallionHookTest\n[PASS] testFuzz_feeIsTwoPercentInEveryShape(uint256,uint8) (runs: 256, μ: 285011, ~: 283114)\n[PASS] testFuzz_statusFormatsTwoTruncatedDecimals(uint256) (runs: 256, μ: 241457, ~: 241230)\n[PASS] test_afterSwapShapesTolerateAPriceLimitAndStillChargeOnGross() (gas: 226845)\n[PASS] test_burnableIsZeroUntilTheCapAndGrowsAfter() (gas: 772418)\n[PASS] test_buyWorksOnAFreshManagerSeededWithTokensOnly() (gas: 10708837)\n[PASS] test_callbacksRefuseCallersOtherThanThePoolManager() (gas: 111250)\n[PASS] test_constructorRejectsAnAddressWithoutTheFlags() (gas: 7605)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 3942)\n[PASS] test_constructorSetsLastBurnBlockAndLeavesAnchorUnseeded() (gas: 22764)\n[PASS] test_donatedClaimsDoNotCountAsFeesAndKeepTheInvariant() (gas: 548463)\n[PASS] test_exactInBuyPartialFillReverts() (gas: 153503)\n[PASS] test_exactInBuyTakesTwoPercentOfEthInAsClaims() (gas: 233975)\n[PASS] test_exactInSellTakesTwoPercentOfGrossEthAsClaims() (gas: 221297)\n[PASS] test_exactOutBuyTakesTwoPercentOfGrossEthAsClaims() (gas: 234343)\n[PASS] test_exactOutSellPartialFillReverts() (gas: 132773)\n[PASS] test_exactOutSellTakesTwoPercentOfEthOutAsClaims() (gas: 222450)\n[PASS] test_feeIsNeverTakenInTokens() (gas: 700444)\n[PASS] test_firstNativePoolIsTheLaunchPool() (gas: 17339)\n[PASS] test_lastFareIsPinned() (gas: 13380)\n[PASS] test_noAdminSurface() (gas: 234481)\n[PASS] test_otherPoolsWithTheHookAreFeeFree() (gas: 1615026)\n[PASS] test_permissionsAreExactlyTheFiveFlags() (gas: 11334)\n[PASS] test_publicConstants() (gas: 112834)\n[PASS] test_quoteIsAmountTimesPrice() (gas: 32710)\n[PASS] test_recoupedEmittedOnTheCrossingSwapWithTheOvershoot() (gas: 367502)\n[PASS] test_recoupedIsEmittedExactlyOnce() (gas: 707458)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 3962819)\n[PASS] test_statusInService() (gas: 591328)\n[PASS] test_statusRecoupedNotRetired() (gas: 409151)\n[PASS] test_swapsOnlyEverAddToTotalFees() (gas: 1105217)\n[PASS] test_tinySwapBelowFeeGranularityIsFree() (gas: 175937)\n[PASS] test_tokenTokenPoolDoesNotBecomeLaunchPoolAndNeverReverts() (gas: 10843064)\n[PASS] test_unlockCallbackRejectsUnknownActions() (gas: 36335)\nSuite result: ok. 33 passed; 0 failed; 0 skipped; finished in 39.16ms (130.89ms CPU time)\n\nRan 28 tests for test/MedallionBurn.t.sol:MedallionBurnTest\n[PASS] testFuzz_batchIsMinOfBurnableAndCap(uint256) (runs: 256, μ: 2540954, ~: 2541127)\n[PASS] test_anchorIsClampedToLastRefPlusMinusTheBand() (gas: 3606957)\n[PASS] test_anchorStepsAtMost200PerBlockTowardThePlainSpot() (gas: 2650745)\n[PASS] test_burnsAreSpacedByFiveBlocks() (gas: 2675200)\n[PASS] test_burnsNeverTouchTheCreatorsReserve() (gas: 2962968)\n[PASS] test_callerCannotChooseTheBatchOnlyRaiseTheFloor() (gas: 2676541)\n[PASS] test_callerGetsNothing() (gas: 2501409)\n[PASS] test_constructorSeedsTheAnchorWhenPool4Answers() (gas: 15442086)\n[PASS] test_fallbackBatchIsCappedAtOneHundredth() (gas: 2554765)\n[PASS] test_fallbackBurnStepsTheAnchorItselfWhenFirstInTheBlock() (gas: 2689739)\n[PASS] test_fallbackBurnUsesTheStartOfBlockAnchor() (gas: 2760662)\n[PASS] test_fallbackGuardHoldsUntilTheAnchorCatchesDown() (gas: 2972354)\n[PASS] test_fallbackRefusesPool4AndNeedsASeed() (gas: 2707350)\n[PASS] test_normalBurnReseedsEverything() (gas: 2830893)\n[PASS] test_normalBurnSpendsTheWholeRemainderWhenBelowTheBatch() (gas: 2497942)\n[PASS] test_normalBurnViaPlainPool() (gas: 2530822)\n[PASS] test_normalBurnViaPool4() (gas: 2660880)\n[PASS] test_nothingToBurnBeforeTheCapAndBelowMinBurn() (gas: 2626810)\n[PASS] test_partialFillRevertsWhenLiquidityRunsOut() (gas: 2125266)\n[PASS] test_pokeAnchorReseedsFromPool4WhenItAnswers() (gas: 2664906)\n[PASS] test_pool4ReferenceRejectsMalformedAnswers() (gas: 904154)\n[PASS] test_priceOffReferenceOnThePlainPoolUses300InNormalMode() (gas: 2633248)\n[PASS] test_priceOffReferenceViaPool4IsOneSided() (gas: 2799721)\n[PASS] test_sepoliaLikeChainHasNoPool4AndNoFallback() (gas: 488276)\n[PASS] test_slippageFloorRefusesAThinPool() (gas: 2402472)\n[PASS] test_statusCountsBurnedIMDAfterRetirement() (gas: 3114326)\n[PASS] test_tooSoonComesFirst() (gas: 58866)\n[PASS] test_uninitializedPlainPoolIsRefused() (gas: 1568982)\nSuite result: ok. 28 passed; 0 failed; 0 skipped; finished in 117.96ms (184.87ms CPU time)\n\nRan 1 test for test/MedallionInvariant.t.sol:MedallionInvariantTest\n[PASS]\nMedallionInvariantTest invariants:\n[PASS] invariant_burnableFollowsTheFormula\n[PASS] invariant_claimsCoverTheLedger\n[PASS] invariant_creatorPaidIsZeroOrCap\n[PASS] invariant_hookNeverHoldsTokensOrEth\n MedallionInvariantTest invariants (runs: 32, calls: 768, reverts: 0)\n\n╭------------------+------------------+-------+---------+----------╮\n| Contract         | Selector         | Calls | Reverts | Discards |\n+==================================================================+\n| MedallionHandler | burn             | 119   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | closeOrOpenPool4 | 138   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | donateClaims     | 115   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | retire           | 146   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | setUp            | 133   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | swap             | 117   | 0       | 0        |\n╰------------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.45s (3.44s CPU time)\n\nRan 5 test suites in 3.45s (3.66s CPU time): 90 tests passed, 0 failed, 0 skipped (90 total tests)\n","passed":true},{"durationMs":62,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"FareToken.approve(address,uint256)\",\"FareToken.burn(uint256)\",\"FareToken.burnFrom(address,uint256)\",\"FareToken.transfer(address,uint256)\",\"FareToken.transferFrom(address,address,uint256)\",\"MedallionHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"MedallionHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MedallionHook.burnIMD(bool,uint256)\",\"MedallionHook.pokeAnchor()\",\"MedallionHook.retire()\",\"MedallionHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":3,\"README.md\":186,\"REVIEW.md\":67,\"docs/DESIGN.md\":124,\"docs/OPERATIONS.md\":70,\"docs/abi/FareToken.json\":337,\"docs/abi/MedallionHook.json\":1383,\"foundry.toml\":33,\"launch.json\":29,\"remappings.txt\":4,\"src/FareToken.sol\":97,\"src/MedallionHook.sol\":611,\"test/FareToken.t.sol\":171,\"test/MedallionBurn.t.sol\":524,\"test/MedallionHook.t.sol\":523,\"test/MedallionInvariant.t.sol\":121,\"test/MedallionRetire.t.sol\":196,\"test/mocks/MockERC20.sol\":49,\"test/mocks/MockMedallion.sol\":100,\"test/mocks/MockPool4Hook.sol\":44,\"test/utils/HookMiner.sol\":27,\"test/utils/MedallionTestBase.sol\":254},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1880,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/MedallionHook.sol:566: MedallionHook._stepAnchor() (src/MedallionHook.sol#566-585) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/MedallionHook.sol:380: Reentrancy in MedallionHook.retire() (src/MedallionHook.sol#380-398):\n[medium/medium] uninitialized-local at src/MedallionHook.sol:281: MedallionHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes).unspecifiedDelta (src/MedallionHook.sol#281) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MedallionHook.sol:601: MedallionHook._toString(uint256).digits (src/MedallionHook.sol#601) is a local variable never initialized\n[medium/medium] unused-return at src/MedallionHook.sol:459: MedallionHook._spotChecked(PoolKey,int24,int24) (src/MedallionHook.sol#459-464) ignores return value by (sqrtPriceX96,spot,None,None) = poolManager.getSlot0(key.toId()) (src/MedallionHook.sol#461)\n[medium/medium] unused-return at src/MedallionHook.sol:566: MedallionHook._stepAnchor() (src/MedallionHook.sol#566-585) ignores return value by (sqrtPriceX96,spot,None,None) = poolManager.getSlot0(plainKey().toId()) (src/MedallionHook.sol#568)\n[medium/medium] unused-return at src/MedallionHook.sol:380: MedallionHook.retire() (src/MedallionHook.sol#380-398) ignores return value by poolManager.unlock(abi.encode(ACTION_PAY_CREATOR,bytes())) (src/MedallionHook.sol#395)\n[low/medium] reentrancy-benign at src/MedallionHook.sol:443: Reentrancy in MedallionHook._executeBurn(bool,bool,int24,uint256,uint256) (src/MedallionHook.sol#443-455):\n[low/medium] reentrancy-benign at src/MedallionHook.sol:380: Reentrancy in MedallionHook.retire() (src/MedallionHook.sol#380-398):\n[low/medium] reentrancy-benign at src/MedallionHook.sol:273: Reentrancy in MedallionHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MedallionHook.sol#273-301):\n[low/medium] reentrancy-events at src/MedallionHook.sol:273: Reentrancy in MedallionHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MedallionHook.sol#273-301):","passed":true},{"durationMs":504,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/MedallionHook.sol:293: Reentrancy: State change after external call (2 places)\n[low] large-numeric-literal at src/FareToken.sol:17: Large Numeric Literal (7 places)\n[low] literal-instead-of-constant at src/MedallionHook.sol:288: Literal Instead of Constant (15 places)\n[low] missing-inheritance at src/FareToken.sol:11: Missing Inheritance\n[low] unchecked-return at src/MedallionHook.sol:395: Unchecked Return\n[low] unused-state-variable at src/MedallionHook.sol:145: Unused State Variable","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"dd01bf34b496520ebde3bbb5d0b6193f9d19bb840221e39f34d5c5caa1d9a335","verifiedTreeHash":"572f835ae56729c45f895816d4b8297e474ba4ec","verifierVersion":"0.1.0+da6bdbe5"},{"checks":[{"durationMs":5986,"exitCode":0,"name":"build","output":"Compiling 82 files with Solc 0.8.26\nSolc 0.8.26 finished in 5.62s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/utils/MedallionTestBase.sol:225:5:\n    |\n225 |     function lastBurnLog() internal returns (BurnLog memory log_) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:558:46\n    │\n558 │         if (!ok || ret.length != 32) return (false, 0);\n    │                                              ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:560:32\n    │\n560 │         if (flag != 1) return (false, 0);\n    │                                ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:563:46\n    │\n563 │         if (!ok || ret.length != 32) return (false, 0);\n    │                                              ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:565:75\n    │\n565 │         if (tick < TickMath.MIN_TICK || tick > TickMath.MAX_TICK) return (false, 0);\n    │                                                                           ━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[boolean-cst]: misuse of a boolean constant\n    ╭▸ src/MedallionHook.sol:566:17\n    │\n566 │         return (true, int24(tick));\n    │                 ━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/boolean-cst\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MedallionHook.sol:315:44\n    │\n315 │         return (IHooks.afterSwap.selector, unspecifiedDelta);\n    │                                            ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:312:13\n    │\n312 │             emit FeeCollected(params.zeroForOne, fee, updated);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:313:65\n    │\n313 │             if (before < CREATOR_CAP && updated >= CREATOR_CAP) emit Recouped(updated, block.number);\n    │                                                                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:300:61\n    │\n300 │             if (int256(amount0) != params.amountSpecified + int256(fee)) revert PartialFill();\n    │                                                             ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:302:43\n    │\n302 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                           ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:302:51\n    │\n302 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                   ━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:302:72\n    │\n302 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                                        ━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:302:80\n    │\n302 │             uint256 gross = amount0 < 0 ? uint256(uint128(-amount0)) : uint256(uint128(amount0));\n    │                                                                                ━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[reentrancy-no-eth]: external call can be reentered before `retired` is updated\n    ╭▸ src/MedallionHook.sol:402:17\n    │\n402 │                 MEDALLION_NFT.call(abi.encodeWithSelector(0x23b872dd, owner, DEAD, MEDALLION_ID));\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:405:13\n    │\n405 │             emit MedallionRetired(owner);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:411:9\n    │\n411 │         emit CreatorPaid(CREATOR, CREATOR_CAP);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:412:9\n    │\n412 │         emit LastFare(MEDALLION_ID, LAST_FARE_HASH, LAST_FARE);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/MedallionHook.sol:410:9\n    │\n410 │         poolManager.unlock(abi.encode(ACTION_PAY_CREATOR, bytes(\"\")));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:577:9\n    │\n577 │         emit AnchorSeeded(ref, block.number);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:595:13\n    │\n595 │             emit BandRecentered(prev, anchor, block.number);\n    │             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:611:9\n    │\n611 │         emit AnchorStepped(from, to, target, block.number);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/MedallionHook.sol:470:9\n    │\n470 │         emit IMDBurned(viaPool4, fallbackMode, batch, imdOut, ref, spot);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:511:57\n    │\n511 │                     zeroForOne: true, amountSpecified: -int256(batch), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1\n    │                                                         ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:515:45\n    │\n515 │             if (int256(delta.amount0()) != -int256(batch) || delta.amount1() <= 0) revert PartialFill();\n    │                                             ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:516:27\n    │\n516 │             uint256 out = uint256(uint128(delta.amount1()));\n    │                           ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:516:35\n    │\n516 │             uint256 out = uint256(uint128(delta.amount1()));\n    │                                   ━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:540:55\n    │\n540 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                       ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:540:90\n    │\n540 │         uint256 amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);\n    │                                                                                          ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:551:24\n    │\n551 │         return address(uint160(word));\n    │                        ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint160' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:566:23\n    │\n566 │         return (true, int24(tick));\n    │                       ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int24' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:620:40\n    │\n620 │             fracDigits[i - 1] = bytes1(uint8(48 + frac % 10));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/MedallionHook.sol:630:13\n    │\n630 │             digits++;\n    │             ━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/MedallionHook.sol:634:36\n    │\n634 │             out[--digits] = bytes1(uint8(48 + v % 10));\n    │                                    ━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint8' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/MedallionBurn.t.sol:27:17\n   │\n27 │         vm.roll(block.number + 5);\n   │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:165:17\n    │\n165 │         vm.roll(block.number + 4);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:371:17\n    │\n371 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:398:21\n    │\n398 │             vm.roll(block.number + 1);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:425:29\n    │\n425 │         uint256 seedBlock = block.number;\n    │                             ━━━━━━━━━━━━\n    ‡\n434 │             vm.roll(seedBlock + i);\n    │             ────────────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:482:21\n    │\n482 │             vm.roll(block.number + 1);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:510:29\n    │\n510 │         uint256 seedBlock = block.number;\n    │                             ━━━━━━━━━━━━\n    ‡\n513 │         vm.roll(seedBlock + hook.FALLBACK_RECENTER_BLOCKS() - 1);\n    │         ──────────────────────────────────────────────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:527:29\n    │\n527 │         uint256 seedBlock = block.number;\n    │                             ━━━━━━━━━━━━\n    ‡\n536 │             vm.roll(seedBlock + i);\n    │             ────────────────────── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:550:21\n    │\n550 │             vm.roll(block.number + 1);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:608:17\n    │\n608 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:616:21\n    │\n616 │             vm.roll(block.number + 1);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:632:17\n    │\n632 │         vm.roll(block.number + 1);\n    │         ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n    ╭▸ test/MedallionBurn.t.sol:688:21\n    │\n688 │             vm.roll(block.number + 5);\n    │             ────────━━━━━━━━━━━━───── `vm.roll` changes this environment here\n    │\n    ├ help: capture it with `vm.getBlockNumber()` instead\n    ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\nwarning[environment-read-across-mutation]: `block.number` may be reused across `vm.roll`\n   ╭▸ test/MedallionInvariant.t.sol:60:17\n   │\n60 │         vm.roll(block.number + bound(blocks, 0, 7));\n   │         ────────━━━━━━━━━━━━─────────────────────── `vm.roll` changes this environment here\n   │\n   ├ help: capture it with `vm.getBlockNumber()` instead\n   ╰ help: https://getfoundry.sh/forge/linting/environment-read-across-mutation\n\n","passed":true},{"durationMs":418,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 33 tests for test/MedallionHook.t.sol:MedallionHookTest\n[PASS] testFuzz_feeIsTwoPercentInEveryShape(uint256,uint8) (runs: 256, μ: 285173, ~: 283172)\n[PASS] testFuzz_statusFormatsTwoTruncatedDecimals(uint256) (runs: 256, μ: 241500, ~: 241664)\n[PASS] test_afterSwapShapesTolerateAPriceLimitAndStillChargeOnGross() (gas: 226868)\n[PASS] test_burnableIsZeroUntilTheCapAndGrowsAfter() (gas: 772424)\n[PASS] test_buyWorksOnAFreshManagerSeededWithTokensOnly() (gas: 15641391)\n[PASS] test_callbacksRefuseCallersOtherThanThePoolManager() (gas: 111229)\n[PASS] test_constructorRejectsAnAddressWithoutTheFlags() (gas: 7605)\n[PASS] test_constructorRejectsZeroPoolManager() (gas: 3942)\n[PASS] test_constructorSetsLastBurnBlockAndLeavesAnchorUnseeded() (gas: 22764)\n[PASS] test_donatedClaimsDoNotCountAsFeesAndKeepTheInvariant() (gas: 548534)\n[PASS] test_exactInBuyPartialFillReverts() (gas: 153549)\n[PASS] test_exactInBuyTakesTwoPercentOfEthInAsClaims() (gas: 234021)\n[PASS] test_exactInSellTakesTwoPercentOfGrossEthAsClaims() (gas: 221343)\n[PASS] test_exactOutBuyTakesTwoPercentOfGrossEthAsClaims() (gas: 234389)\n[PASS] test_exactOutSellPartialFillReverts() (gas: 132796)\n[PASS] test_exactOutSellTakesTwoPercentOfEthOutAsClaims() (gas: 222496)\n[PASS] test_feeIsNeverTakenInTokens() (gas: 700536)\n[PASS] test_firstNativePoolIsTheLaunchPool() (gas: 17295)\n[PASS] test_lastFareIsPinned() (gas: 13314)\n[PASS] test_noAdminSurface() (gas: 234437)\n[PASS] test_otherPoolsWithTheHookAreFeeFree() (gas: 1615142)\n[PASS] test_permissionsAreExactlyTheFiveFlags() (gas: 11334)\n[PASS] test_publicConstants() (gas: 118210)\n[PASS] test_quoteIsAmountTimesPrice() (gas: 32890)\n[PASS] test_recoupedEmittedOnTheCrossingSwapWithTheOvershoot() (gas: 367548)\n[PASS] test_recoupedIsEmittedExactlyOnce() (gas: 707550)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 4023965)\n[PASS] test_statusInService() (gas: 591221)\n[PASS] test_statusRecoupedNotRetired() (gas: 409110)\n[PASS] test_swapsOnlyEverAddToTotalFees() (gas: 1105631)\n[PASS] test_tinySwapBelowFeeGranularityIsFree() (gas: 175983)\n[PASS] test_tokenTokenPoolDoesNotBecomeLaunchPoolAndNeverReverts() (gas: 28045602)\n[PASS] test_unlockCallbackRejectsUnknownActions() (gas: 36313)\nSuite result: ok. 33 passed; 0 failed; 0 skipped; finished in 64.31ms (168.26ms CPU time)\n\nRan 17 tests for test/FareToken.t.sol:FareTokenTest\n[PASS] testFuzz_burnFromAccounting(uint256,uint256) (runs: 256, μ: 97970, ~: 110131)\n[PASS] testFuzz_transferConservesSupply(address,uint256) (runs: 256, μ: 85717, ~: 85798)\n[PASS] test_approveAndTransferFrom() (gas: 132174)\n[PASS] test_approveZeroSpenderReverts() (gas: 30329)\n[PASS] test_burnFromUsesAllowance() (gas: 144974)\n[PASS] test_burnReducesSupply() (gas: 54024)\n[PASS] test_burnRevertsOnInsufficientBalance() (gas: 34873)\n[PASS] test_constructorEmitsMintTransfer() (gas: 11158)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 117211)\n[PASS] test_metadata() (gas: 23923)\n[PASS] test_mintsExactlyOneBillionToDeployer() (gas: 20624)\n[PASS] test_noMintOwnerPauseOrUpgradeSurface() (gas: 283133)\n[PASS] test_runtimeHasNoDelegatecallCallcodeOrSelfdestruct() (gas: 486845)\n[PASS] test_transfer() (gas: 81905)\n[PASS] test_transferFromRevertsOnInsufficientAllowance() (gas: 85116)\n[PASS] test_transferRevertsOnInsufficientBalance() (gas: 37663)\n[PASS] test_transferRevertsToZeroAddress() (gas: 30405)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 129.70ms (86.93ms CPU time)\n\nRan 11 tests for test/MedallionRetire.t.sol:MedallionRetireTest\n[PASS] test_creatorPaidIsZeroOrCap() (gas: 916916)\n[PASS] test_retireCannotRunTwice() (gas: 921616)\n[PASS] test_retireFeesKeepAccruingAfterRetirement() (gas: 1095322)\n[PASS] test_retireIsGuardedAgainstReentrancyFromTheMedallion() (gas: 547845)\n[PASS] test_retireIsRefusedWhenTheTransferSilentlyDoesNothing() (gas: 837075)\n[PASS] test_retireMovesTheMedallionPaysTheCreatorAndEmitsInOneTransaction() (gas: 1235892)\n[PASS] test_retireRevertsBeforeTheCap() (gas: 755557)\n[PASS] test_retireRevertsOnABadOwnerOfAnswer() (gas: 799740)\n[PASS] test_retireRevertsWhenTheMedallionHasNoCode() (gas: 270511)\n[PASS] test_retireSkipsTheTransferWhenTheMedallionIsAlreadyDead() (gas: 850954)\n[PASS] test_retireWithoutApprovalIsRefusedAndChangesNothing() (gas: 797910)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 131.97ms (15.48ms CPU time)\n\nRan 35 tests for test/MedallionBurn.t.sol:MedallionBurnTest\n[PASS] testFuzz_batchIsMinOfBurnableAndCap(uint256) (runs: 256, μ: 2563055, ~: 2563222)\n[PASS] test_anchorIsClampedToLastRefPlusMinusTheBand() (gas: 3672585)\n[PASS] test_anchorStepsAtMost200PerBlockTowardThePlainSpot() (gas: 2681966)\n[PASS] test_bandDoesNotRecenterBeforeRecenterBlocks() (gas: 2641363)\n[PASS] test_bandRecentersAfterALastingMoveUpAndTheFloorFollows() (gas: 20625393)\n[PASS] test_bandRecentersAfterRecenterBlocksAndBurnsFollowALastingMoveDown() (gas: 7656199)\n[PASS] test_burnsAreSpacedByFiveBlocks() (gas: 2702461)\n[PASS] test_burnsNeverTouchTheCreatorsReserve() (gas: 2991399)\n[PASS] test_callerCannotChooseTheBatchOnlyRaiseTheFloor() (gas: 2720835)\n[PASS] test_callerGetsNothing() (gas: 2523636)\n[PASS] test_constructorSeedsTheAnchorWhenPool4Answers() (gas: 57182067)\n[PASS] test_fallbackBatchIsCappedAtOneHundredth() (gas: 2576847)\n[PASS] test_fallbackBurnStepsTheAnchorItselfWhenFirstInTheBlock() (gas: 2714021)\n[PASS] test_fallbackBurnUsesTheStartOfBlockAnchor() (gas: 2785009)\n[PASS] test_fallbackGuardHoldsUntilTheAnchorCatchesDown() (gas: 3005250)\n[PASS] test_fallbackNothingToBurnComesBeforeThePlainPoolRead() (gas: 1714690)\n[PASS] test_fallbackRefusesPool4AndNeedsASeed() (gas: 2729368)\n[PASS] test_normalBurnReseedsEverything() (gas: 2860152)\n[PASS] test_normalBurnSpendsTheWholeRemainderWhenBelowTheBatch() (gas: 2520036)\n[PASS] test_normalBurnViaPlainPool() (gas: 2553030)\n[PASS] test_normalBurnViaPool4() (gas: 2683066)\n[PASS] test_nothingToBurnBeforeTheCapAndBelowMinBurn() (gas: 2489465)\n[PASS] test_partialFillRevertsWhenLiquidityRunsOut() (gas: 2147402)\n[PASS] test_pokeAnchorReseedsFromPool4WhenItAnswers() (gas: 2694073)\n[PASS] test_pool4ReferenceRejectsMalformedAnswers() (gas: 904492)\n[PASS] test_pool4UnavailableComesBeforeNothingToBurn() (gas: 1154521)\n[PASS] test_priceOffReferenceOnThePlainPoolUses300InNormalMode() (gas: 2677541)\n[PASS] test_priceOffReferenceViaPool4IsOneSided() (gas: 2849027)\n[PASS] test_recenterBoundsTheDriftOfTheReference() (gas: 12028279)\n[PASS] test_reseedFromPool4ResetsTheRecenterClock() (gas: 2979846)\n[PASS] test_sepoliaLikeChainHasNoPool4AndNoFallback() (gas: 488259)\n[PASS] test_slippageFloorRefusesAThinPool() (gas: 2424697)\n[PASS] test_statusCountsBurnedIMDAfterRetirement() (gas: 3136555)\n[PASS] test_tooSoonComesFirst() (gas: 58866)\n[PASS] test_uninitializedPlainPoolIsRefused() (gas: 1591184)\nSuite result: ok. 35 passed; 0 failed; 0 skipped; finished in 177.63ms (380.79ms CPU time)\n\nRan 1 test for test/MedallionInvariant.t.sol:MedallionInvariantTest\n[PASS]\nMedallionInvariantTest invariants:\n[PASS] invariant_burnableFollowsTheFormula\n[PASS] invariant_claimsCoverTheLedger\n[PASS] invariant_creatorPaidIsZeroOrCap\n[PASS] invariant_hookNeverHoldsTokensOrEth\n MedallionInvariantTest invariants (runs: 32, calls: 768, reverts: 8)\n\n╭------------------+------------------+-------+---------+----------╮\n| Contract         | Selector         | Calls | Reverts | Discards |\n+==================================================================+\n| MedallionHandler | burn             | 160   | 8       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | closeOrOpenPool4 | 155   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | donateClaims     | 154   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | retire           | 151   | 0       | 0        |\n|------------------+------------------+-------+---------+----------|\n| MedallionHandler | swap             | 148   | 0       | 0        |\n╰------------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 241.28ms (208.36ms CPU time)\n\nRan 5 test suites in 251.32ms (744.89ms CPU time): 97 tests passed, 0 failed, 0 skipped (97 total tests)\n","passed":true},{"durationMs":99,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"FareToken.approve(address,uint256)\",\"FareToken.burn(uint256)\",\"FareToken.burnFrom(address,uint256)\",\"FareToken.transfer(address,uint256)\",\"FareToken.transferFrom(address,address,uint256)\",\"MedallionHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"MedallionHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"MedallionHook.burnIMD(bool,uint256)\",\"MedallionHook.pokeAnchor()\",\"MedallionHook.retire()\",\"MedallionHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":3,\"README.md\":223,\"REVIEW.md\":85,\"docs/DESIGN.md\":141,\"docs/OPERATIONS.md\":90,\"docs/abi/FareToken.json\":337,\"docs/abi/MedallionHook.json\":1434,\"foundry.toml\":33,\"launch.json\":29,\"remappings.txt\":4,\"src/FareToken.sol\":97,\"src/MedallionHook.sol\":638,\"test/FareToken.t.sol\":171,\"test/MedallionBurn.t.sol\":710,\"test/MedallionHook.t.sol\":524,\"test/MedallionInvariant.t.sol\":130,\"test/MedallionRetire.t.sol\":196,\"test/mocks/MockERC20.sol\":49,\"test/mocks/MockMedallion.sol\":100,\"test/mocks/MockPool4Hook.sol\":44,\"test/utils/HookMiner.sol\":27,\"test/utils/MedallionTestBase.sol\":254},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":2897,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/MedallionHook.sol:586: MedallionHook._stepAnchor() (src/MedallionHook.sol#586-612) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/MedallionHook.sol:395: Reentrancy in MedallionHook.retire() (src/MedallionHook.sol#395-413):\n[medium/medium] uninitialized-local at src/MedallionHook.sol:296: MedallionHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes).unspecifiedDelta (src/MedallionHook.sol#296) is a local variable never initialized\n[medium/medium] uninitialized-local at src/MedallionHook.sol:628: MedallionHook._toString(uint256).digits (src/MedallionHook.sol#628) is a local variable never initialized\n[medium/medium] unused-return at src/MedallionHook.sol:395: MedallionHook.retire() (src/MedallionHook.sol#395-413) ignores return value by poolManager.unlock(abi.encode(ACTION_PAY_CREATOR,bytes())) (src/MedallionHook.sol#410)\n[medium/medium] unused-return at src/MedallionHook.sol:586: MedallionHook._stepAnchor() (src/MedallionHook.sol#586-612) ignores return value by (sqrtPriceX96,spot,None,None) = poolManager.getSlot0(plainKey().toId()) (src/MedallionHook.sol#588)\n[medium/medium] unused-return at src/MedallionHook.sol:475: MedallionHook._spotChecked(PoolKey,int24,int24) (src/MedallionHook.sol#475-480) ignores return value by (sqrtPriceX96,spot,None,None) = poolManager.getSlot0(key.toId()) (src/MedallionHook.sol#477)\n[low/medium] reentrancy-benign at src/MedallionHook.sol:395: Reentrancy in MedallionHook.retire() (src/MedallionHook.sol#395-413):\n[low/medium] reentrancy-benign at src/MedallionHook.sol:459: Reentrancy in MedallionHook._executeBurn(bool,bool,int24,uint256,uint256) (src/MedallionHook.sol#459-471):\n[low/medium] reentrancy-benign at src/MedallionHook.sol:288: Reentrancy in MedallionHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MedallionHook.sol#288-316):\n[low/medium] reentrancy-events at src/MedallionHook.sol:288: Reentrancy in MedallionHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/MedallionHook.sol#288-316):","passed":true},{"durationMs":993,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/MedallionHook.sol:308: Reentrancy: State change after external call (2 places)\n[low] large-numeric-literal at src/FareToken.sol:17: Large Numeric Literal (7 places)\n[low] literal-instead-of-constant at src/MedallionHook.sol:303: Literal Instead of Constant (15 places)\n[low] missing-inheritance at src/FareToken.sol:11: Missing Inheritance\n[low] unchecked-return at src/MedallionHook.sol:410: Unchecked Return\n[low] unused-state-variable at src/MedallionHook.sol:159: Unused State Variable","passed":true},{"durationMs":3164,"exitCode":0,"name":"proof cbfaa27a5c9e","output":"Compiling 78 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.24s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to view\n   --> test/utils/MedallionTestBase.sol:225:5:\n    |\n225 |     function lastBurnLog() internal returns (BurnLog memory log_) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\n\nRan 1 test for test/imd-proof-4212b6de/Proof_cbfaa27a5c9e.t.sol:ProofFallbackBand\n[PASS] test_fallbackBurnFollowsTheUnmanipulatedPlainPool() (gas: 10288702)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.75ms (5.92ms CPU time)\n\nRan 1 test suite in 10.56ms (8.75ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"f2f2a96cd5f47248d9761971c6ea37dc8a99383cba5f686b1ea143633c95d84e","verifiedTreeHash":"394191b58a996e98015df33bd86d7307396fc92f","verifierVersion":"0.1.0+da6bdbe5"}]}