{"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":"dddb75ec-5081-49b8-9204-f4de1725ca03","kind":"audit","nodes":[{"acceptedSubmissionHash":"89a0fe710cade425876bd204da89047ec2168a02dd4cd1420803e3cacb46cd74","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"e4e3485399f8cb648af980c7a3f986bc834fb008521c1da106e7676cd65d72c7","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"37ea3a97aed907216baab81f4fc277e8f91920de1a70bf5d02dfd768080ca2a6","dependsOn":["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":"accepted"},{"acceptedSubmissionHash":"24605a4370e1eeee3c0e453e56e32d044410e38499730c1ad85915b24f275968","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"05be0a6749979c0193c16dfbca0c4f0bd2f4c79363542bc17b47090c73e92951","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"Final check 4 for The Zero Person Billion Dollar Company ($COMPANY) on Robinhood Chain (4663), after IMD Swarm audit 78c00339, re-check f1d5def3 and final checks 363ab052, 882666b4 and 986abba2. AUDIT.md sections 4 to 8 map every finding to its fix and its test.\n\nWhat the contracts are for: CompanyToken is a fixed 1,000,000,000 supply ERC-20; its ownership is renounced in the constructor. CompanyHook owns the token's only Uniswap v4 pool, paired with IMD, with liquidity locked forever, and takes 4% of every swap in that pool: 1% to the protocol, 3% to holders. Holder fees are split 50% IMD and 10% each to NVDA, GOOGL, AAPL, GME and MSTR Robinhood stock tokens, bought IMD -> USDG -> stock at the start of every claim(). Only wallets holding at least 100,000 earn. If a wallet goes more than 7 days without claiming, buying, selling or sending, its unclaimed rewards older than 7 days expire.\n\nChanged since 986abba2; review these hardest:\n1. The IMD -> USDG hop is now checked too: minUsdOut(imdIn) prices IMD via the IMD/ETH pool (fee 1%, spacing 100, no hooks) and Chainlink ETH/USD and USDG/USD, less the IMD/USDG pool fee and IMD_TOLERANCE_BPS (10%); unlockCallback reverts PriceOff below it and the stock is skipped. Can a round still sell at a made-up price, or can the check be pushed (the IMD/ETH spot is read in the same transaction) to block or force anything? The 10% reflects IMD's two pools drifting about 5.6% apart after a 2 ETH buy on a fork.\n2. One stuck rule replaces the dead-feed and empty-pool clocks: waitingSince[stock] starts when a reserve fills from empty (distribute), moves to now on every successful purchase (convertStock), clears when the reserve empties; a stock waiting more than DEAD_AFTER (30 days) is paid as IMD one capped round at a time, whatever blocks it (stale or unusable feed, empty IMD/USDG pool, PriceOff). Can anyone trigger it early on a healthy stock, or keep a stuck stock from ever falling back?\n3. claim() now handles expiry (_recycle, lastActive) before flush and _convertAll; from-pool tags lapse after any distribution in the transaction (transient epoch bumped in _credit).\n4. The IMD/USDG pool counts as empty below 1% of a full round; then nothing is swapped and only stuck stocks fall back.\n\nPlease confirm these, check that nothing broke the solvency of the six reward assets, the flash-borrow guards, the 100,000 minimum, expiry or the scanner-relevant properties (no external calls in transfers), and report anything new.\n\nTests: cd contracts; git submodule update --init --recursive; forge test. Fork test (live Chainlink feeds and pools): FORK_RPC=https://robinhood.drpc.org forge test --mc CompanyForkTest.","parentJobId":null,"planHash":"0b637dc2d302c59221dcd2ec5051bf96aeb936316bd39b3c1ea128050a80bc19","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"dddb75ec-5081-49b8-9204-f4de1725ca03","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51423","feedbackHash":"fa44dc2df66c903bcd8be90d2a6efc1a78e7eb738b6c3c436fd45e3bf5e21c9e","nodeKey":"audit_economics","submissionHash":"89a0fe710cade425876bd204da89047ec2168a02dd4cd1420803e3cacb46cd74","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51442","feedbackHash":"7f41be476b9ce2ae1f608912f286b1bd2bce9eecf40885c5548c0caaf584fa41","nodeKey":"audit_flow","submissionHash":"e4e3485399f8cb648af980c7a3f986bc834fb008521c1da106e7676cd65d72c7","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51041","feedbackHash":"402be7fd84733927389f18ed5688e0c43b12d5d5473db25235f73d2aa49b130a","nodeKey":"audit_judge","submissionHash":"37ea3a97aed907216baab81f4fc277e8f91920de1a70bf5d02dfd768080ca2a6","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51227","feedbackHash":"483ce0581a75cf3f45b93b6b0520eb1ef37684ed19c2acc69e642dc2c51c190b","nodeKey":"audit_math","submissionHash":"24605a4370e1eeee3c0e453e56e32d044410e38499730c1ad85915b24f275968","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51166","feedbackHash":"7ab5c1db735ae07779c785a8e361a14e05d5a6dc7b6c42c378e0c6bc09334a16","nodeKey":"audit_permissions","submissionHash":"05be0a6749979c0193c16dfbca0c4f0bd2f4c79363542bc17b47090c73e92951","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"a62f0ece30aef3bbc625407dfb15211a6c5043f26759adc7684d3ce498b39ad6","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"45aa937328087de3","findings":[{"citation":"resolved","description":"`waitingSince[stock]` starts when the reserve fills and only moves on a successful purchase, so it measures time without a purchase, not time during which a purchase was impossible. If no claim() or convert() runs for 30 days (nobody tried), every stock is `stuck` on the next attempt even though it is perfectly buyable. The first attempt after the gap is then paid as IMD on any transient failure that would otherwise just skip: a stale equity feed on a weekend (`!_feedsFresh`), or a first-hop `PriceOff` caused by the IMD/ETH and IMD/USDG pools drifting apart. That drift is produced by an ordinary ETH-route buy (the fork test's 2.2 ETH of buys already pulls minUsdOut(1e18) to 8.068 USDG against a hop that pays about 8.07, so roughly 4 ETH exceeds the 10% tolerance) and can be produced on purpose by anyone with a 1%-fee swap in the IMD/ETH pool in the same transaction as convert(). All five stocks fall back at once, 4 IMD each, and the next minute's convert does it again while the condition lasts, so an attacker who holds the IMD/ETH push for N minutes converts 20*N IMD of stock reserves into IMD. Holders lose no value (they receive IMD instead of stock), so this is a design-integrity defect rather than a theft: the fallback meant for dead feeds and empty pools is reachable on a healthy stock, and anyone can trigger it after any quiet month. Fix that keeps the no-keeper design: record the time of the first failed or skipped attempt per stock (set when a round is skipped for a stale feed, an empty IMD/USDG pool or a PriceOff; cleared on a success) and require both `now > waitingSince + DEAD_AFTER` and `firstFailed != 0 && now > firstFailed + 1 day` before `_fallBackToImd` in the three stuck branches (lines 719, 734, 746). A quiet month then needs two failures a day apart, which honest claims supply for a truly dead stock, while a single transient skip after a gap only arms the clock. The existing tests test_final3_deadFeedFallsBackToImdAfter30Days, test_final3_emptyImdPoolFallsBackToImdAfter30Days and test_final2_3_dustImdPoolStillCountsAsEmpty would need a second convert() call one day later.","line":717,"path":"contracts/src/CompanyToken.sol","reproduction":"Setup as in test/Company.t.sol setUp (IMD/ETH 1:1 pool with 10,000e18 full-range liquidity, all feeds at $1). alice and bob each buy 1,000 IMD so pendingConvert[1..5] = 6e18 and waitingSince[s] = now. (a) Control: warp 1 minute, swap 600 ETH -> IMD in the IMD/ETH pool through PoolSwapTest (IMD per ETH falls about 11%; minUsdOut(1e18) becomes 1.001e18, above the ~0.991e18 the 1:1 IMD/USDG hop pays), call convert(): all stocks skipped, pendingConvert[1] stays 6e18, owed[0] unchanged. (b) Warp 31 days with no convert in between, refresh all feeds to now, do the same 600 ETH swap, call convert(). Expected: the same skip, because the stock is healthy and buyable a minute later. Actual: pendingConvert[s] == 2e18 for every stock, owed[0] grew by 5*4e18 = 20 IMD, no stock bought. Swapping the IMD back into the IMD/ETH pool and calling convert() one minute later buys NVDA normally (out[1] > 0), showing the stock was never stuck. Verified with test/scratch/Lead.t.sol::test_lead_quietMonthThenDriftForcesFallback on this commit.","severity":"low","snippet":"            bool stuck = waitingSince[a] != 0 && block.timestamp > waitingSince[a] + DEAD_AFTER;","title":"Stuck rule fires on a healthy, buyable stock: 30 idle days plus one transient skip pays every stock's round as IMD"},{"citation":"resolved","description":"The sentence follows the description of the IMD -> USDG check, but only the stock hop is checked against Chainlink. The first hop is checked against the IMD/ETH pool's slot0 price read in the same transaction (CompanyToken.minUsdOut, line 888), which anyone can move with a swap before convert() runs. What protects it is cost, not impossibility: on the live pool (block 26141069, IMD/ETH is effectively full range with about 77 ETH and 22,450 IMD of real tokens, 1% fee) moving the spot 10% takes a swap of about 4 ETH and costs about 0.04 ETH of fees per direction, enough to block a round (griefing, no gain); forcing a made-up first-hop price needs the ETH side of the pool bought out (about 77 ETH, roughly 1.5 ETH of fees round trip) plus the USDG side of IMD/USDG (about 25,800 USDG in concentrated ranges, 0.9% fee each way), to capture at most 20 IMD (about $160) per minute while the push is held, so it is unprofitable at today's depth and reserves. The claim 'however thin the pool' is therefore false for the first hop: if the IMD/ETH pool thins out, the first-hop tolerance offers no protection at all, exactly the empty-pool scenario of audit 986abba2 finding 1 moved one pool over. The AUDIT.md section 9 wording ('Within that tolerance, sandwiches stay bounded by the 20 IMD round ceiling') is the accurate statement. Also stale: contracts/test/Company.fork.t.sol line 69 still says 'after fee and 5% tolerance' while IMD_TOLERANCE_BPS is 10%. Fix: reword the README sentence to apply to the stock hop only and state the first-hop assumption (IMD/ETH liquidity must stay deep relative to 20 IMD per minute of reserves), and update the fork test comment.","line":30,"path":"README.md","reproduction":"Setup as in test/Company.t.sol. alice and bob buy 1,000 IMD each; warp 1 minute; swap 600 ETH -> IMD in the hookless IMD/ETH pool in the same transaction before convert(). minUsdOut(1e18) returns 1.001e18 while the IMD/USDG hop pays about 0.991e18 per IMD, so every stock reverts PriceOff and is skipped (pendingConvert unchanged), contradicting 'a price manipulated inside a transaction can't pass this' in the blocking direction; in the other direction, selling IMD into IMD/ETH lowers minUsdOut by the same mechanism and widens the room for an IMD/USDG sandwich beyond 10%. Documentation only; no code change required beyond the comment.","severity":"info","snippet":"A price manipulated inside a transaction can't pass this, however thin the pool.","title":"README overstates the first-hop check: the IMD/ETH reference is a same-transaction spot, not an oracle"},{"citation":"resolved","description":"The only test of minUsdOut is the made-up-price position in an emptied IMD/USDG pool. Nothing in the suite exercises the ordinary case the brief asks about: the IMD/ETH spot drifting more than 10% from IMD/USDG (every stock must skip with PriceOff and nothing may fall back before 30 days), nor the ETH/USD or USDG/USD feed being stale (both are read in _feedsFresh and minUsdOut and hold all five stocks). I verified both paths behave as designed with scratch tests (all stocks held, owed[0] unchanged, one round per stock paid as IMD only after 30 days), so this is a coverage gap, not a defect. Suggested tests: (1) buy 1,000 IMD twice, warp 1 minute, swap 600 ETH -> IMD in the IMD/ETH pool, convert(): assert out[s] == 0 and pendingConvert[s] unchanged for all s; (2) set ethFeed.updatedAt = now - 5 days with the others fresh, convert(): same assertions; same for usdFeed.","line":1243,"path":"contracts/test/Company.t.sol","reproduction":"State: the test suite at this commit. Input: grep for a test that moves the IMD/ETH pool price or sets ethFeed/usdFeed stale before convert(). Expected: at least one such test. Actual: none (ethFeed and usdFeed are only ever set fresh by _refreshFeedsExcept; the IMD/ETH pool is only used by the ETH router tests). Scratch reproductions: test/scratch/Lead.t.sol::test_lead_quietMonthThenDriftForcesFallback part (a) and test/scratch/Feeds.t.sol.","severity":"info","snippet":"    function test_final3_1_madeUpPricePositionGetsNothing() public {","title":"No unit test for the new first-hop drift skip or for a stale ETH/USD or USDG/USD feed"}],"hash":"05be0a6749979c0193c16dfbca0c4f0bd2f4c79363542bc17b47090c73e92951","nodeId":"4d81e2d6-8e3e-4dff-b546-06f7e090dc48","outcome":"completed","summary":"Review complete. The findings file holds three entries, no tracked file was changed, and only the scratch directory is new.\n\n**Outcome.** The four changes since 986abba2 hold up. I found one Low on the new stuck rule and two informational notes. No solvency, flash-borrow, minimum-holding or expiry regression, and transfers still make no external calls.\n\n**What I verified**\n\n- **First-hop check (question 1).** It can be pushed in the same transaction, but only at a cost. On the live chain the IMD/ETH pool is effectively full range, with about 77 ETH and 22,450 IMD of real tokens. Moving the spot 10% costs about 0.04 ETH of fees per direction and only blocks a round. Forcing a made-up price needs the ETH side bought out plus the USDG side of IMD/USDG, for at most 20 IMD per minute of gain, so it is unprofitable. The IMD/USDG pool now holds about 8,900 IMD of virtual depth, four times the 2,200 IMD threshold where in-tolerance sandwiching turns profitable. Worth watching, since it was 19,750 at launch.\n- **Stuck rule (question 2).** The clock cannot be shortened or started early. Reserve top-ups do not restart it, and only a real purchase moves it. A stuck stock cannot be kept from falling back except by a genuine fair-price purchase, which is the desired outcome.\n- **Claim ordering and tag epochs (question 3).** Expiry before flush is strictly tighter for the protocol. Every path that bumps the epoch is unreachable while another caller holds the PoolManager unlocked, so borrowed tokens never see a tag lapse. The only lapse that helps a wallet is the capital-backed variant already accepted in AUDIT.md section 9.\n- **Empty IMD/USDG rule (question 4).** Confirmed by reading and by a 400-seed randomized run over stale feeds, blocked recipients, pool drifts, donations, third-party trades and 31-day gaps. Solvency of all six assets and the pending/waitingSince invariant held in every step.\n- The full suite and the live fork test pass at the pinned commit.\n\n**Findings written**\n\n1. **Low.** The stuck rule measures time without a purchase, not time when a purchase was impossible. After any 30-day gap with no claim or convert, one transient skip pays every stock's round as IMD. A 4 ETH buy through the ETH route or a deliberate IMD/ETH swap in the same transaction triggers it, 20 IMD per minute while held. Holders lose no value. Suggested fix keeps the no-keeper design by requiring a recorded failed attempt at least a day earlier.\n2. **Info.** The README claim that a same-transaction manipulation \"can't pass this, however thin the pool\" is only true for the stock hop. The fork test comment still says 5% tolerance.\n3. **Info.** No unit test covers the first-hop drift skip or a stale ETH/USD or USDG/USD feed. I verified both paths behave correctly with scratch tests.\n\nScratch tests under `contracts/test/scratch/` reproduce the Low and the coverage gap.","treeHash":null,"usage":{"cachedInputTokens":4442764,"inputTokens":770,"model":"claude-fable-5-1","outputTokens":95159,"runtime":"claude","turns":46,"wallClockMs":1931646}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"89214b73ec1e0b7b","findings":[],"hash":"12e8a926be4250614d413cea40a8ee05e9a926cf710b50a04bc7c5432688608d","nodeId":"aecb38e1-2471-43f6-8edb-23253aaeb30a","outcome":"failed","summary":"runtime reported <synthetic>, not the required premium model claude-fable-5-1","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"<synthetic>","outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":5866}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"11e4593175677f7e","findings":[{"citation":"resolved","description":"AUDIT.md section 8 finding 4 and the DEAD_AFTER NatSpec promise that a stock waiting more than 30 days without a purchase is paid as IMD \"one capped round at a time, whatever blocks it\", i.e. MAX_ROUND_IMD / 5 = 4 IMD per round (the amount the imdPoolEmpty branch at lines 711 and 719 uses). But on the other two stuck paths (`!_feedsFresh` at line 734 and PriceOff at line 746) `_fallBackToImd` receives `imdIn`, which was already clipped to `maxConvert() / 5` (line 715) and to `stockRoundLimit(a)` (line 730). Both are same-transaction reads of in-range liquidity, and once the real liquidity has left a pool any LP sets them: a position with 80 IMD of virtual IMD depth keeps `maxConvert()` just above the 0.2 IMD emptiness threshold (so cap = 0.04-0.06 IMD), and a stock-pool position with about 27 USDG of virtual depth in a 0.3% pool keeps `stockRoundLimit` just above cap / 100 = 0.04 IMD. In a one-tick-spacing range such a position costs about 1 token and is fully withdrawable. Posted at a made-up price, every round reverts PriceOff, so (a) the stock is skipped for 30 days where a truly empty pool would have been paid at once at the full 4 IMD (lines 726-728), and (b) after 30 days the fallback moves 0.06-0.10 IMD per round instead of 4 IMD, about 1/70 of the intended pace: with a few claims a day a 6 IMD reserve takes months and a reserve of a few hundred IMD years. No value is extracted; the holders' stock share is frozen. This is the residual of 882666b4 finding 3 (dust position stops the fallback): the 1% threshold moved the dust attack from \"never\" to \"1% speed\". The regime also arises without an attacker: on the live IMD/USDG pool (block 82538953) the active liquidity 2.53e16 sits in [-263790, -247950] and only about 3.3e14 (1.3%) remains below tick -263790, so an IMD price below about $3.4 would make maxConvert() about 0.29 IMD and every stuck fallback 0.06 IMD. A related gap: a dust purchase that succeeds moves waitingSince to now (line 778), so a dust position at the true price keeps a dead stock pool \"alive\" at 1% throughput and the stock never counts as stuck. Fix (verified with the attached test: it fails on this commit and passes with this change): on all three stuck paths pass `pendingConvert[a] > MAX_ROUND_IMD / (ASSETS - 1) ? MAX_ROUND_IMD / (ASSETS - 1) : pendingConvert[a]` to `_fallBackToImd` instead of the clipped `imdIn`; consider also treating a pool as empty below a larger fraction of a full round than 1% and moving `waitingSince` only on purchases of at least a meaningful fraction of the round.","line":746,"path":"contracts/src/CompanyToken.sol","reproduction":"Mock setup as in Company.t.sol (1:1 pools, $1 feeds). Two 1,000 IMD buys leave 6 IMD waiting per stock. (A) IMD/USDG pool: the LP removes all 10,000e18 liquidity (maxConvert() = 0). Attacker: 1-wei swap with price limit to tick -6930 (1 IMD = 0.5 USDG, true value 1), then modifyLiquidity(-7020, -6840, 90e18): costs under 2 tokens; maxConvert() = 0.318 IMD, above the 0.2 IMD threshold, so the pool counts as non-empty and cap = 0.0636 IMD. convert() at +1 minute: PriceOff, pending stays 6 IMD. convert() at +31 days (feeds refreshed): each stock falls back by 0.0636 IMD (owed[0] grows by 0.318 IMD for the five stocks). Expected: one capped round of 4 IMD per stock (pending 6 -> 2), as in the truly-empty case. (B) IMD/USDG healthy, NVDA pool: LP removes all liquidity; stockRoundLimit(1) = 0 and a convert() pays 4 IMD as IMD immediately (pending 6 -> 2). Instead the attacker posts modifyLiquidity(target-120, target+120, 45e18) at tick +-6960 (1 NVDA = 2 USDG): stockRoundLimit(1) = 0.0959 IMD, above cap/100 = 0.04. convert() at +1 minute: PriceOff, pending stays 6 IMD (frozen where it was paid at once before). convert() at +31 days: 0.0959 IMD moved instead of 4 IMD. Run: cd contracts; forge test --match-path test/scratch/StuckFallbackThrottle.t.sol (both tests fail: \"63633824830293874 < 2000000000000000000\" and \"95897401689536770 < 2000000000000000000\").","severity":"medium","snippet":"if (stuck) _fallBackToImd(a, imdIn);","title":"Stuck-stock fallback pays only the pool-clipped imdIn: a ~1-token dust position throttles the 30-day IMD fallback about 70x and freezes an abandoned stock pool's reserve for 30 days"},{"citation":"resolved","description":"Read from the live PoolManager (0x8366a39CC670B4001A1121B8F6A443A643e40951, block 82538953) with extsload of each pool's slot0: the protocolFee field (bits 184-207) is 0x3e83e8 = 1000 pips in each direction (0.1%) on IMD/USDG (0xaf5bcd88...406f), GME/USDG (0x3d436b4f...063b) and IMD/ETH (0xd2fc01ee...8f02), and 0x019019 = 25 pips on USDG/NVDA (0x6444a8e0...29c5); protocolFeeController is 0x6d0009504D129CF5002Dba61D9Ae8575AA79314c. v4 takes the protocol fee from the swap input before the LP fee, so the IMD -> USDG hop costs 0.9991% rather than 0.9% and the GME hop 1.099% rather than 1%. minStockOut (line 870) and minUsdOut (line 893) remove only the LP fee before applying the 3% and 10% tolerances, so the effective margins are 2.9% and 9.9%, and the \"0.9% fees\" sandwich-cost reasoning in the MAX_ROUND_IMD NatSpec and README line 29 understates the attacker's cost by 0.1 pp (in the protocol's favour). Not exploitable; it only reduces headroom, most for GME whose 1% fee already uses most of the margin. Fix: subtract ProtocolFeeLibrary.calculateSwapFee(slot0.protocolFee, lpFee) read from the pool's slot0 instead of the fixed LP fee, or document the 0.1% as part of the tolerance budget. Related stale figure: the NatSpec at line 123 and README line 130 give the IMD/USDG depth as ~19,750 IMD; at block 82538953 the in-range liquidity 2.534e16 at sqrtPriceX96 2.305e23 is 8,840 IMD / 72,700 USDG of virtual depth, still 4x (not 9x) above the ~2,200 IMD break-even.","line":870,"path":"contracts/src/CompanyToken.sol","reproduction":"cast call 0x8366a39CC670B4001A1121B8F6A443A643e40951 \"extsload(bytes32,uint256)(bytes32[])\" $(cast keccak $(cast abi-encode \"f(bytes32,uint256)\" 0xaf5bcd88bb2b6084ca18319385b61fcbe17ec579032074861004f2668168406f 6)) 4 --rpc-url https://robinhood.drpc.org returns slot0 0x0000000023283e83e8fc1aea...: lpFee 0x002328 = 9000, protocolFee 0x3e83e8 = 1000|1000 pips. Expected by the code: a 0.9% hop fee; actual: 0.9991%. For GME (0x3d436b4f...063b) slot0 0x0000000027103e83e8fc45ea...: lpFee 10000, protocolFee 1000|1000.","severity":"info","snippet":"fair = (fair * (1_000_000 - stockPools[asset].fee)) / 1_000_000;","title":"Uniswap v4 protocol fee (0.1%) is switched on for the route pools on Robinhood Chain; the tolerance checks and the sandwich-cost figures only account for the LP fee"},{"citation":"resolved","description":"Line 130 states that conversions \"run at the pool price of the moment and have no minimum output\", which contradicts line 30 and the code: since f1d5def3 finding 1 every stock purchase must receive at least 97% of the Chainlink-implied amount (minStockOut) and since 986abba2 finding 1 the IMD -> USDG hop at least 90% of IMD's IMD/ETH + Chainlink value (minUsdOut), both enforced in unlockCallback. The same line gives the IMD/USDG depth as about 19,750 IMD at launch; the live pool (block 82538953) has 8,840 IMD of virtual depth at the current price, so the margin above the ~2,200 IMD sandwich break-even is about 4x. Fix: reword the risk item to describe the two oracle-bounded minimums and the 10%/3% residual tolerance, and refresh or drop the depth figure (also in the MAX_ROUND_IMD NatSpec at contracts/src/CompanyToken.sol line 123).","line":130,"path":"README.md","reproduction":"Read README.md line 130 against contracts/src/CompanyToken.sol lines 788-791 (two PriceOff minimum-output checks). Expected: documentation matches the enforced minimums; actual: it says there is none.","severity":"info","snippet":"- **Conversion pricing.** Conversions run at the pool price of the moment and have no minimum output. The 20 IMD round ceiling, the per-stock pool limits and the 1-minute spacing keep sandwiching unprofitable while the IMD/USDG pool holds more than about 2,200 IMD of depth (about 19,750 at launch).","title":"README \"Known risks\" still says conversions have no minimum output and quotes a launch depth of ~19,750 IMD"}],"hash":"24605a4370e1eeee3c0e453e56e32d044410e38499730c1ad85915b24f275968","nodeId":"03285638-d743-441a-89d0-f9999bf49d65","outcome":"completed","summary":"Review complete. The findings file is at the repository root, source files are untouched (`git diff` is empty), and the only new path is `contracts/test/scratch/`, which is mine.\n\n**Outcome.** One medium finding with a Foundry proof, two info findings. The four changed areas you asked about hold up, with one gap in area 2.\n\n**Findings written to `.imd-findings.json`**\n\n- **Medium: the 30-day stuck fallback pays only the pool-clipped `imdIn`.** The rule promises \"one capped round (4 IMD) at a time, whatever blocks it\", but on the stale-feed and PriceOff paths `_fallBackToImd` gets an amount already clipped to `maxConvert()/5` and `stockRoundLimit`. Once real liquidity has left a pool, a position costing about one token (80 IMD of virtual depth, or ~27 USDG in a 0.3% stock pool) keeps the pool just above the 1% emptiness threshold. At a made-up price every round is PriceOff, so an abandoned stock pool's reserve is frozen for 30 days where a truly empty pool is paid at once, and afterwards falls back at 0.06 to 0.10 IMD per round instead of 4. The proof test fails on this commit and passes with a three-line change: use `min(pending, MAX_ROUND_IMD/5)` on the stuck paths. The live IMD/USDG pool is concentrated, with only 1.3% of its liquidity below tick -263790, so this regime also arises with no attacker if IMD drops below about $3.4.\n- **Info: v4 protocol fee is on.** Live slot0 reads show a 0.1% protocol fee on IMD/USDG, GME/USDG and IMD/ETH (25 pips on NVDA). The tolerance checks subtract only the LP fee, so effective margins are 9.9% and 2.9%. Not exploitable.\n- **Info: README line 130** still says conversions have \"no minimum output\" and quotes ~19,750 IMD depth. Live depth is ~8,840 IMD, 4x above the ~2,200 IMD sandwich break-even.\n\n**Confirmed, with live numbers (block 82538953, IMD ≈ $8.15)**\n\n1. **First-hop check.** The IMD/ETH reference is 88% full-range liquidity (25,700 IMD / 82 ETH virtual). Loosening it enough for a 50% discount on a 20 IMD round would mean selling ~8,600 IMD at 1% fee each way, for a gain under 10 IMD. Blocking it costs ~0.08 ETH per block and only delays rounds. A made-up IMD/USDG price is caught. After the fork test's 2.2 ETH buy the gap was 9%, so a ~4.5 ETH buy via the ETH route pauses conversions until arbitrage, as you accept.\n2. **Stuck rule.** `waitingSince` only ever moves to now or 0, so nobody can age a healthy stock. The one gap is the medium finding above.\n3. **Claim ordering and epoch tags.** `_recycle` runs before flush, and the only `_credit` reachable inside a foreign unlock is router-only, so borrowed tokens can never see a tag lapse. Correct.\n4. **Empty-pool threshold.** Works as stated for the IMD/USDG pool; the cliff it creates is the medium finding.\n\nSolvency accounting for all six assets, the flash-borrow guards, the 100,000 minimum boundaries, expiry boundaries (7 days, 4-day oracle age, 30-day stuck, 1-minute interval) and the no-external-call property of transfers all check out. All 58 unit tests and the fork test pass. Not run: Slither, long fuzz campaigns beyond the configured 256 runs.","treeHash":null,"usage":{"cachedInputTokens":2984890,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":96127,"runtime":"claude","turns":43,"wallClockMs":1459265}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"3bed38612db34f32","findings":[{"citation":"resolved","description":"Merged from audit_math (medium) and audit_flow (low); both reproduce. The DEAD_AFTER rule (NatSpec at lines 146-149, AUDIT.md section 8 finding 4) promises that a stock waiting more than 30 days without a purchase is paid as IMD one capped round at a time, whatever blocks it; the imdPoolEmpty branch (lines 710-711, 719) does pay MAX_ROUND_IMD / 5 = 4 IMD per round. But on the other two stuck paths, the stale-feed branch (line 734) and the PriceOff catch (line 746), _fallBackToImd receives imdIn after it was clipped to cap = maxConvert() / 5 (line 715) and to stockRoundLimit(a) (line 730). Both clips are sized for a real swap; a fallback swaps nothing, so they have no reason to apply to it. Both are same-transaction reads of in-range virtual liquidity, so once the real liquidity has left a pool, whoever posts the last position sets the fallback size. (a) IMD/USDG pool abandoned (as in test_final3_1): a straddling position with ~80 IMD of virtual depth keeps maxConvert() just above the 0.2 IMD emptiness threshold (cap = 0.04 IMD per stock). Posted at a made-up price it costs 0.36 IMD plus dust USDG, fully withdrawable; every round reverts PriceOff, so (i) the stock is skipped for 30 days where a truly empty pool would be paid 4 IMD at once and (ii) after 30 days each round moves 0.04 IMD per stock instead of 4 IMD, i.e. 1/100 of the intended pace: a 6 IMD reserve needs ~150 rounds, a few hundred IMD needs years, and with inflow above ~0.2 IMD per minute of claims it never drains. This is the residual of 882666b4 finding 3 (dust position stops the fallback), which the 1% threshold moved from 'never' to '1% speed'. (b) Stock pool: a ~27 USDG full-range dust position in a 0.3% pool keeps stockRoundLimit just above cap / 100 = 0.04 IMD; with that stock's feed dead the stale-feed stuck path pays 0.045 IMD per round instead of 4 IMD. A related gap: with a fresh feed the same dust position lets a 0.045 IMD purchase succeed every round, which moves waitingSince to now (line 778), so a dead stock pool never counts as stuck while draining at 1%. The regime also arises without an attacker: on the live IMD/USDG pool (block 82558449) the in-range liquidity 2.52e16 gives 8,550 IMD of virtual depth, and the specialist's read of the tick bitmap found only about 1.3% of it below tick -263790, so an IMD price under ~$3.4 would make maxConvert() about 0.3 IMD and every stuck fallback ~0.06 IMD. No value is extracted; holders' stock share (50% of holder fees) is withheld. Fix (verified: the attached proof fails on this commit and passes with it): on all three stuck paths pass min(pendingConvert[a], MAX_ROUND_IMD / (ASSETS - 1)) to _fallBackToImd instead of the clipped imdIn, keeping the depth-derived clips for real purchases only. Consider also treating a pool as empty below a larger fraction of a full round than 1%, and moving waitingSince only on purchases of a meaningful fraction of a round.","line":746,"path":"contracts/src/CompanyToken.sol","reproduction":"Mock setup as in test/Company.t.sol (1:1 pools, $1 feeds). alice and bob each buy 1,000 IMD: pendingConvert = 6 IMD per stock, waitingSince = now. (a) The LP removes all 10,000e18 IMD/USDG liquidity (maxConvert() = 0). Attacker: 1-wei swap with sqrtPriceLimit at tick -207000 (IMD worth 1e-9 USDG), then modifyLiquidity(-207090, -206910, 2.6e15): costs 0.3647 IMD + 3.7e8 wei USDG; maxConvert() = 203065671655427927 (0.203 IMD >= 0.2, pool counts as non-empty, cap = 0.0406 IMD). convert() at +1 minute: every stock PriceOff, pendingConvert(1) stays 6e18. convert() at +31 days (feeds refreshed): owed[0] grows by 203065671655427925 wei in total and pendingConvert(1) = 5959386865668914415. Expected (empty-pool rule, control test without the dust position): owed[0] + 20e18, pendingConvert(1) = 2e18. (b) IMD/USDG healthy; the NVDA LP removes its 1,000,000e18 liquidity (stockRoundLimit(1) = 0, a convert() would pay 4 IMD at once); attacker adds a full-range position of liquidity 30e18: stockRoundLimit(1) = 0.045e18 >= cap / 100. NVDA feed set 31 days old, others fresh, warp +31 days, convert(): NVDA fallback moves 45000000000000000 wei (0.045 IMD) instead of 4e18. Run: cd contracts; forge test --match-path test/scratch/ProofStuckFallbackThrottle.t.sol -vv (test_A1_dustImdUsdPositionThrottlesStuckFallback fails '5959386865668914415 != 2000000000000000000'; test_A2_dustStockPoolThrottlesDeadFeedFallback fails '45000000000000000 != 4000000000000000000'; the control passes).","severity":"medium","snippet":"                    if (stuck) _fallBackToImd(a, imdIn);","title":"Stuck-stock IMD fallback moves only the swap-clipped imdIn, so a ~0.4 IMD dust position (or a thin live range) throttles the 30-day fallback about 100x and freezes the stock reserve"},{"citation":"resolved","description":"Merged from audit_economics and audit_permissions (both low); both reproduce. waitingSince[a] starts when the reserve fills (distribute, line 490) and only a successful purchase moves it (convertStock, line 778). It measures time without a purchase, not time during which a purchase was tried and impossible: a month in which nobody calls claim() or convert() (router trades flush and distribute but never convert) makes every stock 'stuck' on the next attempt although each is perfectly buyable. In that state the stale-feed branch (lines 733-736) and the PriceOff catch (lines 745-748) fall back at once instead of skipping. Both conditions are transient by the contract's own design: MAX_ORACLE_AGE (4 days) exists so that equity feeds that pause over long weekends and holidays make the stock wait, and IMD_TOLERANCE_BPS exists so that IMD's two pools drifting apart after an ETH-route buy makes the round wait for arbitrage. The PriceOff variant is also attacker-triggerable at will: anyone can push the IMD/ETH spot more than 10% in the same transaction as convert() (on the live pool about 4 ETH through the 1% pool, ~0.08 ETH of fees for the round trip) and, because _fallBackToImd neither moves waitingSince nor clears stuck, repeat it every CONVERT_INTERVAL; the stale-feed variant repeats by itself until the feed updates, so a whole reserve can be gone before the next feed update. Holders receive full value in IMD, so no funds are lost; what is lost is the promised stock exposure and AUDIT.md guarantee 7 ('only on a real failure'), and the requester's question 2 (can anyone trigger it early on a healthy stock) is answered yes. Fix that keeps the no-keeper property: record per stock the time of the first failed or skipped attempt since the last success (set on a skip for a stale feed, an empty IMD/USDG pool or a PriceOff; cleared on a success) and require both waitingSince + DEAD_AFTER and that first failure being at least a day old (or the feed itself being older than DEAD_AFTER in the stale-feed branch) before the three stuck fallbacks (lines 719, 734, 746). A quiet month then arms the clock on its first skip instead of paying. Existing tests test_final3_deadFeedFallsBackToImdAfter30Days, test_final3_emptyImdPoolFallsBackToImdAfter30Days and test_final2_3_dustImdPoolStillCountsAsEmpty would need a second convert() a day later.","line":717,"path":"contracts/src/CompanyToken.sol","reproduction":"Mock setup as in test/Company.t.sol. alice and bob each buy 1,000 IMD (pendingConvert = 6e18 per stock, waitingSince = now). (1) vm.warp(+31 days) with no claim() or convert() in between; refresh every feed except NVDA; set NVDA updatedAt = now - 4 days - 1 (just past MAX_ORACLE_AGE). convert(): NVDA pendingConvert = 2e18, owed[0] += 4e18 (ConversionFailed). Expected, as on day 29 (control test) and as test_recheck1_staleFeed_holdsThatStock expects: pendingConvert(1) stays 6e18, owed[0] unchanged. convert() again at +1 minute: pendingConvert(1) = 0, the whole NVDA reserve paid as IMD. (2) Same quiet month, all feeds fresh; in the same transaction swap 600 ETH -> IMD in the 1:1 IMD/ETH pool (minUsdOut(1e18) becomes 1001004664283999998 while the IMD/USDG hop pays ~0.991e18), then convert(): every stock PriceOff and stuck, pendingConvert = 2e18 for all five, owed[0] += 20e18, nothing bought. Expected (control at day 1 with the same swap): all skipped, reserves unchanged. Run: cd contracts; forge test --match-path test/scratch/ProofQuietMonthFallback.t.sol -vv (test_B1_quietMonthThenWeekendStaleFeedPaysHealthyStockAsImd and test_B2_quietMonthThenImdEthPushPaysAllStocksAsImd fail '2000000000000000000 != 6000000000000000000'; the two controls and test_B1_repeatsEveryMinute pass).","severity":"low","snippet":"            bool stuck = waitingSince[a] != 0 && block.timestamp > waitingSince[a] + DEAD_AFTER;","title":"waitingSince counts idle time, so after 30 days without any claim or convert the first transient skip (stale feed, IMD/ETH drift) pays a healthy stock's reserve as IMD, repeatable every minute"},{"citation":"resolved","description":"From audit_economics (low); reproduced, and widened by a second variant found while reproducing. The requester's question 4 states the intended rule for an IMD/USDG pool without real liquidity at its price: nothing is swapped and only stuck stocks fall back. 986abba2 finding 1 closed the made-up LOW price with minUsdOut. Two paths remain that pay every stock's capped round as IMD immediately, with no stuck check, from a position that holds no USDG at all: (1) Made-up HIGH price: stockRoundLimit (lines 850-856) converts each stock pool's USDG depth into IMD at the IMD/USDG sqrtPrice read in the same transaction. A single-sided IMD position whose range starts at the current tick is in range (maxConvert() reads its virtual depth, here the full 20 IMD round) while the inflated price makes every deep stock pool's limit read below cap / 100, so line 727 credits min(pending, cap) of each reserve to holders as IMD without any swap and without the 30-day wait. (2) True price: the same single-sided position at the real price passes every limit, convertStock tries the swap, the position has no USDG to give, _swapExactIn reverts Slippage (not PriceOff), and the catch at line 749 falls back at once. Both cost about 36-40 IMD of real tokens (recoverable) after the IMD/USDG pool is out of range; both hit all five stocks and repeat every minute until the reserves are empty. Precondition: the IMD/USDG pool exhausted at its price, either abandoned or pushed out of its concentrated range (the specialist measured ~110,000 USDG to do that on a fork, ~2,000 USDG of fees round trip; the live pool at block 82558449 has 8,550 IMD / 74,500 USDG of virtual depth in range). Holders receive full value in IMD, so this is the class AUDIT.md section 9 accepts for a purchase made to fail on purpose, but it is cheaper and broader than the example given there (no stock-pool LP role, no failed purchase at the stock hop) and it defeats the empty-pool rule the requester asked to confirm. Fix: compare the IMD/USDG spot with the IMD/ETH + Chainlink reference (the comparison minUsdOut makes) before any swap and, when it is more than IMD_TOLERANCE_BPS away, treat the pool as unusable exactly like the imdPoolEmpty branch (skip, fall back only if stuck); price stockRoundLimit's USDG-to-IMD conversion at that reference rather than the spot; and in the catch, fall back immediately only for failures the IMD/USDG pool cannot cause (route a Slippage from the first hop through the stuck rule as PriceOff already is).","line":726,"path":"contracts/src/CompanyToken.sol","reproduction":"Mock setup as in test/Company.t.sol. alice and bob buy 1,000 IMD each (6e18 per stock); the LP removes all IMD/USDG liquidity (maxConvert() = 0, as in test_final3_1). (1) Attacker: 1-wei swap with sqrtPriceLimit at tick 108000 (1 IMD = 49,000 USDG), then modifyLiquidity(108000, 108090, 2e24): 40.57 IMD and 0 USDG leave the attacker. Now maxConvert() = 20e18 and stockRoundLimit(s) = 30615782074609986 (0.031 IMD < cap / 100 = 0.04 IMD) for all five stocks although each pool still has 1,000,000e18 liquidity at Chainlink's price. warp +1 minute, convert(): no swap (token IMD balance unchanged), pendingConvert = 2e18 for all five, owed[0] += 20e18. (2) Same pool state, position at the true price: modifyLiquidity(0, 90, 8000e18) (35.9 IMD, 0 USDG) with the tick at 0; maxConvert() = 20e18, stockRoundLimit(1) > 1,000 IMD; convert(): every stock's swap gets zero fill, Slippage, immediate fallback: pendingConvert = 2e18 for all five, owed[0] += 20e18. Expected in both: nothing swapped and reserves unchanged (only a stock stuck for DEAD_AFTER may fall back). Run: cd contracts; forge test --match-path test/scratch/ProofExhaustedPoolImmediateFallback.t.sol -vv (both tests fail 'healthy stock reserve must stay: 2000000000000000000 != 6000000000000000000').","severity":"low","snippet":"            if (poolLimit < cap / 100) {","title":"In an exhausted IMD/USDG pool a single-sided IMD position (~36 IMD) makes all five stocks fall back at once, bypassing the empty-pool rule and the 30-day wait"},{"citation":"resolved","description":"From audit_math (info); verified on-chain. The live PoolManager (0x8366a39CC670B4001A1121B8F6A443A643e40951, block 82558449) has protocolFeeController 0x6d0009504D129CF5002Dba61D9Ae8575AA79314c and slot0.protocolFee set on the route pools: 0x3e83e8 (1000 pips = 0.1% in each direction) on IMD/USDG (0xaf5bcd88...406f) and GME/USDG (0x3d436b4f...063b), 0x019019 (25 pips) on USDG/NVDA (0x6444a8e0...29c5). v4 charges ProtocolFeeLibrary.calculateSwapFee(protocolFee, lpFee) = protocolFee + lpFee - protocolFee * lpFee / 1e6 on the input, so the IMD -> USDG hop costs 0.9991% rather than 0.9% and the GME hop 1.099% rather than 1%. minUsdOut (line 893) and minStockOut (line 870) remove only the pool's lpFee before applying the 10% and 3% tolerances, so the margins actually available are about 9.9% and 2.9%, and the '0.9% fees' sandwich-cost figures in the MAX_ROUND_IMD NatSpec (lines 118-124) and README line 29 understate the attacker's cost by 0.1 percentage point (in the protocol's favour). Not exploitable; it only narrows headroom, most for GME whose 1% fee already uses most of its 3% margin. Fix: subtract the swap fee actually charged (read slot0.protocolFee and apply ProtocolFeeLibrary.calculateSwapFee) instead of the fixed lpFee, or document the 0.1% as part of the tolerance budget.","line":893,"path":"contracts/src/CompanyToken.sol","reproduction":"cast call 0x8366a39CC670B4001A1121B8F6A443A643e40951 'extsload(bytes32)(bytes32)' $(cast keccak $(cast abi-encode 'f(bytes32,uint256)' 0xaf5bcd88bb2b6084ca18319385b61fcbe17ec579032074861004f2668168406f 6)) --rpc-url https://robinhood.drpc.org returns 0x0000000023283e83e8fc1d31000000000000000000003188ad123aa665115a0b: lpFee 0x002328 = 9000, protocolFee 0x3e83e8 = 1000|1000 pips. GME (0x3d436b4f...063b): 0x0000000027103e83e8fc45ea...: lpFee 10000, protocolFee 1000|1000. NVDA (0x6444a8e0...29c5): 0x0000000000640190190361ab...: lpFee 100, protocolFee 25|25. Expected by the code: a 0.9% hop fee; actual: 0.9991%.","severity":"info","snippet":"        fair = (fair * (1_000_000 - imdUsdPool.fee)) / 1_000_000;","title":"Uniswap v4 protocol fee (0.1%) is switched on for the route pools on Robinhood Chain; minUsdOut and minStockOut subtract only the LP fee, so the effective tolerances are 9.9% and 2.9%"},{"citation":"resolved","description":"Merged from audit_math and audit_permissions (both info); all four statements checked against the tree and the live pool. (1) README line 130 says conversions 'run at the pool price of the moment and have no minimum output', contradicting line 30 and the code: every stock purchase must receive at least 97% of the Chainlink-implied amount (minStockOut, unlockCallback line 791) and the IMD -> USDG hop at least 90% of IMD's IMD/ETH + Chainlink value (minUsdOut, line 789). (2) README line 30 ends 'A price manipulated inside a transaction can't pass this, however thin the pool': true for the stock hop (Chainlink), false for the first hop, whose reference is the IMD/ETH pool's slot0 read in the same transaction (minUsdOut, line 888). Anyone can move it with a swap before convert(): in the mock setup a 600 ETH swap into the 1:1 IMD/ETH pool makes every round PriceOff (blocking direction, see the quiet-month finding), and selling IMD into IMD/ETH lowers the minimum and widens the room for an IMD/USDG sandwich beyond 10%. What protects the first hop is the cost of moving a deep IMD/ETH pool plus the 20 IMD ceiling, not an oracle; if the IMD/ETH pool thins out the check protects nothing. AUDIT.md section 9's wording is the accurate one. (3) README line 130 and the NatSpec at contracts/src/CompanyToken.sol lines 123-124 give the IMD/USDG depth as about 19,750 IMD at launch; the live pool (block 82558449, in-range liquidity 25239561974856391 at sqrtPriceX96 0x3188ad123aa665115a0b) has about 8,550 IMD / 74,500 USDG of virtual depth, 4x (not 9x) above the ~2,200 IMD break-even. (4) contracts/test/Company.fork.t.sol line 69 says 'after fee and 5% tolerance' while IMD_TOLERANCE_BPS is 10%. Fix: reword the risk item to describe the two oracle-bounded minimums and the 10%/3% residual tolerances; limit the 'manipulated inside a transaction' claim to the stock hop and state the first-hop assumption (IMD/ETH liquidity must stay deep relative to 20 IMD per minute of reserves); refresh or drop the depth figure in both places; fix the fork test comment.","line":130,"path":"README.md","reproduction":"Read README.md line 130 against contracts/src/CompanyToken.sol lines 788-791 (two PriceOff minimum-output checks): the README says there is no minimum. Read README.md line 30 against minUsdOut (line 884-895): the reference is a same-transaction pool spot. Mock setup as in test/Company.t.sol: alice and bob buy 1,000 IMD, warp 1 minute, swap 600 ETH -> IMD in the IMD/ETH pool, convert(): minUsdOut(1e18) = 1001004664283999998, every stock skipped, reserves unchanged (test_B2_control_day1ImdEthPushJustSkips in test/scratch/ProofQuietMonthFallback.t.sol). Live depth: the liquidity slot keccak(poolId, 6) + 3 of IMD/USDG reads 0x59ab3f75cc3ac7 = 2.52e16; L * 2^96 / sqrtPriceX96 = 8,549 IMD. Expected: documentation matches the code and the pool; actual: four stale or wrong statements.","severity":"info","snippet":"- **Conversion pricing.** Conversions run at the pool price of the moment and have no minimum output. The 20 IMD round ceiling, the per-stock pool limits and the 1-minute spacing keep sandwiching unprofitable while the IMD/USDG pool holds more than about 2,200 IMD of depth (about 19,750 at launch).","title":"README and NatSpec misstate the conversion price checks: 'no minimum output' and 'however thin the pool' are wrong, the ~19,750 IMD depth figure is stale, a fork-test comment says 5% tolerance"},{"citation":"resolved","description":"From audit_permissions (info); confirmed by inspection. The only test of minUsdOut is test_final3_1_madeUpPricePositionGetsNothing (an emptied IMD/USDG pool with a position at a made-up price). Nothing in the suite exercises the ordinary case the brief asks about in question 1: the IMD/ETH spot drifting more than IMD_TOLERANCE_BPS from the IMD/USDG price (every stock must skip with PriceOff and nothing may fall back before 30 days), nor the ETH/USD or USDG/USD feed being stale (both are read in _feedsFresh and minUsdOut and hold all five stocks). ethFeed and usdFeed are only ever set fresh (lines 704-705 and the _refreshFeedsExcept helper at 1133-1139) and the IMD/ETH pool is only used by the ETH-router tests. Both paths behave as designed in scratch reproductions (test_B2_control_day1ImdEthPushJustSkips in test/scratch/ProofQuietMonthFallback.t.sol covers the drift skip), so this is a coverage gap, not a defect; it matters because the two new checks are the main change under review and because the quiet-month finding shows the same paths misbehaving after 30 days. Suggested tests: (1) two 1,000 IMD buys, warp 1 minute, swap 600 ETH -> IMD in the IMD/ETH pool, convert(): out[s] == 0 and pendingConvert[s] unchanged for all s, owed[0] unchanged; (2) ethFeed.set(1e8, now - 4 days - 1) with the others fresh, convert(): same assertions; the same for usdFeed; (3) the day-31 variants of both once the stuck rule is revised.","line":1243,"path":"contracts/test/Company.t.sol","reproduction":"grep -n 'ethFeed\\|usdFeed' contracts/test/Company.t.sol: only lines 704-705 and 1134-1135, all setting a fresh timestamp; grep for a swap on the imdEth key outside the ETH-router tests: none. Expected: at least one test that drifts the IMD/ETH price or stales ethFeed/usdFeed before convert(); actual: none.","severity":"info","snippet":"    function test_final3_1_madeUpPricePositionGetsNothing() public {","title":"No unit test covers the first-hop drift skip (IMD/ETH spot moved more than 10%) or a stale ETH/USD or USDG/USD feed"}],"hash":"37ea3a97aed907216baab81f4fc277e8f91920de1a70bf5d02dfd768080ca2a6","nodeId":"aecb38e1-2471-43f6-8edb-23253aaeb30a","outcome":"completed","summary":"The review is complete. The findings file is written at the repository root, the source tree is unchanged, and every snippet and proof was checked against the tree.\n\n**What I did**\n\nI read the four contracts, the tests, AUDIT.md and the README, built and ran the full suite (58 tests pass) and the live fork test against Robinhood Chain (passes). I then reproduced each specialist finding with my own Foundry tests under `contracts/test/scratch/`, verified two on-chain claims with `cast` against the live PoolManager, and merged duplicates.\n\n**Findings kept, in `.imd-findings.json` (6 findings, 3 with proofs)**\n\n- **Medium, stuck fallback throttled.** Merged audit_math and audit_flow. On the stale-feed and PriceOff stuck paths the fallback moves the swap-clipped amount, so a 0.36 IMD dust position in an abandoned IMD/USDG pool shrinks the 30-day fallback from 4 IMD to 0.04 IMD per stock. Reproduced exactly (0.203 IMD total instead of 20). I also confirmed the proof passes under the obvious fix by temporarily patching and restoring the contract.\n- **Low, quiet month then transient skip.** Merged audit_economics and audit_permissions. After 31 idle days, a feed one second past the 4-day limit, or a same-transaction IMD/ETH push, pays healthy stocks as IMD. Both reproduced, plus the repeat-every-minute drain.\n- **Low, exhausted-pool immediate fallback.** From audit_economics, widened: a single-sided IMD position with no USDG triggers an immediate, non-stuck-gated fallback of all five stocks, both at a made-up high price (via the stock-pool limit) and at the true price (via Slippage). Both reproduced.\n- **Info, v4 protocol fee.** Verified on-chain: 0.1% protocol fee on IMD/USDG and GME, 25 pips on NVDA. The tolerance math subtracts only the LP fee.\n- **Info, documentation.** Merged the two README findings: \"no minimum output\", \"however thin the pool\", the stale 19,750 IMD depth figure (live depth is about 8,550 IMD), and the fork test's \"5% tolerance\" comment.\n- **Info, test coverage.** No unit test for the first-hop drift skip or a stale ETH/USD or USDG/USD feed.\n\n**Dropped or changed**\n\nNothing was dropped. Severity for the throttle finding is set to medium (the stronger of the two specialists' ratings) because holders' stock share is withheld rather than paid in another asset, which is worse than the two low findings. No new defects beyond the Slippage variant were found in solvency, the flash-borrow guards, the 100,000 minimum, expiry, or the no-external-calls-in-transfer property.","treeHash":null,"usage":{"cachedInputTokens":3016108,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":62870,"runtime":"claude","turns":41,"wallClockMs":958235}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"56e50117311155be","findings":[{"citation":"resolved","description":"waitingSince[a] starts when a reserve fills (distribute) and only a successful purchase (convertStock) moves it. It is not an \"attempted and failed\" clock: it also counts time in which no claim() or convert() ran at all. After 30 days without a successful purchase of a stock, whatever the reason, `stuck` is true, and in that state the `!_feedsFresh(a)` branch (line 733-736) and the PriceOff catch (line 745-748) fall back at once instead of waiting. Both of those are explicitly transient conditions by the contract's own design: MAX_ORACLE_AGE = 4 days exists so that equity feeds that pause over weekends and holidays make the stock wait, and IMD_TOLERANCE_BPS exists so that the two IMD pools drifting apart after an ETH-route buy makes the round wait until arbitrage. For a low-activity token a month with no claim or convert call is ordinary; nothing has to be wrong with the stock. The next call then lands on a Saturday (stock feed older than 4 days) or right after a large ETH-route buy (on the fork at block 82539044 a 4 ETH buy moves minUsdOut(1 IMD) from 7.26 to 7.98 USDG against the ~8.07 USDG the IMD/USDG pool pays, 8 ETH to 8.73, so a single 5-8 ETH buy makes every round PriceOff until arbitrage), and that stock's round is credited to holders as IMD. Because _fallBackToImd neither moves waitingSince nor clears `stuck`, this repeats every CONVERT_INTERVAL: anyone can call convert() once a minute over the weekend and turn the entire reserve of every stock in that state into IMD before the feed updates on Monday. Guarantee 7 in AUDIT.md (\"only on a real failure\") and the requester's question 2 (\"can anyone trigger it early on a healthy stock\") are therefore not met. Holders receive full value in IMD, so there is no loss of funds; the promised stock exposure is what is lost, and the fee recipient is unaffected. Fix that keeps the no-keeper property: in the !_feedsFresh branch, fall back only when the feed itself is dead (latestRoundData reverts/answers <= 0, or updatedAt is older than DEAD_AFTER), not merely older than MAX_ORACLE_AGE; and for the PriceOff catch, either require `stuck` to be measured from the first failed attempt after the last success (set waitingSince on the first skip after a success instead of in distribute()), or additionally require that the IMD/USDG spot has been off the IMD/ETH reference at a prior attempt more than DEAD_AFTER ago. Alternatively move waitingSince forward on every fallback so a transient block costs at most one round.","line":717,"path":"contracts/src/CompanyToken.sol","reproduction":"State: NVDA pool deep and at Chainlink's price, usdFeed/ethFeed fresh. 1) alice and bob each buy 1,000 IMD through CompanyRouter: pendingConvert[1] = 6 IMD, waitingSince[1] = now. 2) vm.warp(+31 days) with no claim() or convert() call. 3) Refresh every feed except NVDA; set NVDA's updatedAt = now - 4 days - 1 (weekend-stale, exactly the MAX_ORACLE_AGE case). 4) Anyone calls convert(). Expected (as on day 29, see the control test, and as test_recheck1_staleFeed_holdsThatStock expects): NVDA round skipped, pendingConvert[1] stays 6e18, owed[0] unchanged. Actual: pendingConvert[1] = 2e18, owed[0] += 4e18 (ConversionFailed). 5) Call convert() again at +1 minute and +2 minutes: pendingConvert[1] = 0; the whole NVDA reserve was paid as IMD before the feed could update. The same happens with a fresh feed if the IMD/USDG hop is PriceOff on that call (e.g. right after a 5-8 ETH buy through CompanyEthRouter): the round is paid as IMD although the stock pool is healthy.","severity":"low","snippet":"            bool stuck = waitingSince[a] != 0 && block.timestamp > waitingSince[a] + DEAD_AFTER;","title":"The 30-day stuck clock runs while nobody calls, so after a quiet month the first transient block (weekend-stale feed, a >10% IMD/ETH drift) pays a healthy stock as IMD, and anyone can drain the whole "},{"citation":"resolved","description":"The requester's question 4 states the intended behaviour for an IMD/USDG pool without real liquidity at its price: nothing is swapped and only stuck stocks fall back; 986abba2 finding 1 closed the made-up *low* price with minUsdOut. The made-up *high* price is still unhandled, and it does not need any swap to do harm: stockRoundLimit (lines 850-856) converts each stock pool's USDG depth into IMD using the IMD/USDG sqrtPrice read in the same transaction. With that price inflated, a deep stock pool's limit is a tiny IMD amount, `poolLimit < cap / 100` is true for every stock, and line 727 credits min(pending, cap) of each stock's reserve to holders as IMD immediately, with no 30-day wait and no stuck check. Reachable state on the live pools: IMD/USDG is concentrated, not full range. On a fork at block 82539044, buying IMD with ~110,000 USDG drives it to tick 887271 with liquidity 0 (fee 0.9% = ~990 USDG each way; the IMD bought, 7,532, is swapped back afterwards); selling ~5,300 IMD does the same downward. In that void a 1-wei swap sets the price anywhere, and a single-sided position holding ~35 IMD of real tokens gives 8,000 IMD of virtual depth at a price 50,000x the fair one, so maxConvert() reads the full 20 IMD round (cap = 4 IMD per stock) while stockRoundLimit(NVDA) reads ~0.004 IMD < 0.04. One convert() then pays 20 IMD of reserves as IMD, and every minute after that until the reserves are empty. Cost: about 2,000 USDG of pool fees plus capital held for a few minutes; no LP role in any stock pool is needed, and it hits all five stocks. The outcome (holders paid in IMD at full value) is the class AUDIT.md section 9 accepts for a purchase made to fail on purpose, but this path is cheaper and broader than the example given there, needs no failed purchase, and defeats the empty-pool rule the requester asked to confirm. Note that re-pricing stockRoundLimit alone is not enough: with a correct limit the round would try the swap, the dust position holds no USDG, _swapExactIn reverts Slippage and the catch at line 749 falls back anyway. A fix that meets the stated rule: treat an IMD/USDG spot that is more than IMD_TOLERANCE_BPS away from the IMD/ETH+Chainlink reference (the same comparison minUsdOut makes, done before any swap) as \"pool unusable\": skip the stock, fall back only if stuck, exactly as the imdPoolEmpty branch does; and in the catch, fall back immediately only for failures that cannot be caused by the IMD/USDG pool's state (e.g. the stock token refusing the contract), routing Slippage through the stuck rule as PriceOff already is.","line":726,"path":"contracts/src/CompanyToken.sol","reproduction":"Unit setup as in Company.t.sol but with the IMD/USDG position concentrated in [-9000, 9000] (like the live pool). 1) alice and bob buy 1,000 IMD each: 6 IMD reserved per stock; every stockRoundLimit > 1,000 IMD. 2) Attacker swaps USDG for IMD with a price limit at tick 9,090: the real range is exhausted, getLiquidity == 0, maxConvert() == 0. 3) Attacker adds a position [108000, 108090] with liquidity 2e24 (single-sided, ~35 IMD of real tokens). 4) Attacker swaps 1 wei with price limit at tick 108,000: slot0 tick = 108,000, liquidity = 2e24, maxConvert() == 20e18, stockRoundLimit(s) < 0.04e18 for all five stocks although each stock pool still holds 1,000,000e18 of liquidity at Chainlink's price. 5) warp +1 minute, anyone calls convert(). Expected: nothing swapped, every pendingConvert stays 6e18 (only a stock stuck for DEAD_AFTER may fall back). Actual: pendingConvert = 2e18 for all five, owed[0] += 20e18, five ConversionFailed events, no stock bought; a second call one minute later empties the reserves. Attacker outlay in the test: ~2,151 IMD+USDG units tied up, recoverable.","severity":"low","snippet":"            if (poolLimit < cap / 100) {\n                _fallBackToImd(a, imdIn);","title":"stockRoundLimit prices USDG in IMD at the IMD/USDG spot: a dust position at a made-up high IMD price in an exhausted IMD/USDG pool makes every deep stock pool look empty, and all five rounds are paid "}],"hash":"89a0fe710cade425876bd204da89047ec2168a02dd4cd1420803e3cacb46cd74","nodeId":"e3a679a7-2907-41bb-8b35-57ea50a4c972","outcome":"completed","summary":"The review is complete. The findings file holds two Low findings, each with a self-contained Foundry proof that fails on the current code. No source files were changed; only `contracts/test/scratch/` and `.imd-findings.json` were added.\n\n**What I confirmed on the four changed areas**\n\n- **Question 1, the first-hop check.** A round cannot sell at a made-up low IMD/USDG price: the floor comes from the IMD/ETH pool plus Chainlink, and loosening it is expensive at live depth. On the fork at block 82539044, selling 8,000 IMD into IMD/ETH lowers `minUsdOut` by 42% and 64,000 IMD by 93%, costing about 1% in fees each way, against a gain capped at 20 IMD per round. Blocking is cheap but useless: a 4 ETH buy brings the floor to 7.98 USDG against about 8.07 paid, and 8 ETH to 8.73, so any 5 to 8 ETH buy pauses conversions until arbitrage. That matches what AUDIT.md section 9 already accepts.\n- **Question 2, the stuck clock.** Nobody can keep a stuck stock from falling back, and a stuck stock cannot be un-stuck without a real purchase. But the clock can fire early on a healthy stock, which is finding 1.\n- **Question 3, claim ordering and the transient epoch.** Correct. A distribution can only happen inside a router's own unlock, and the only user code that runs there is the ETH-sell `receive` callback, which runs after the flush, so borrowed tokens are always tagged with the current epoch. The lapsed-tag path requires real capital and costs more than the accepted gift variant.\n- **Question 4, the empty IMD/USDG pool.** The \"nothing is swapped, only stuck stocks fall back\" rule holds for an actually empty pool, but a made-up high price defeats it, which is finding 2.\n\n**Findings**\n\n1. **Low, CompanyToken.sol line 717.** The 30-day clock keeps running while nobody calls. After a quiet month, the first call that meets a transient block pays a healthy stock's round as IMD: a weekend-stale feed, or a PriceOff right after a large ETH-route buy. Since the fallback does not move the clock, anyone can repeat it every minute and drain the whole reserve into IMD over a weekend. Value is preserved as IMD, so this is griefing of the promised stock exposure, but it contradicts guarantee 7 (\"only on a real failure\").\n2. **Low, CompanyToken.sol line 726.** `stockRoundLimit` prices USDG in IMD at the IMD/USDG spot read in the same transaction. The live pool is concentrated and can be driven out of range for about 110,000 USDG of swap volume (roughly 1,000 USDG fee each way). A dust position holding about 35 IMD at a price 50,000 times fair then makes `maxConvert` read a full round while every deep stock pool reads as empty, and all five stocks' rounds are credited as IMD at once, with no swap and no 30-day wait. Re-pricing the limit alone is not enough, because the swap would then fail with Slippage and fall back through the catch anyway.\n\n**Solvency, flash-borrow guards, 100,000 minimum, expiry, and transfers** held under everything I traced: the six-asset accounting is conserved on every path including the new fallback paths, claim and recycle refuse mid-unlock, distribution cannot run mid-unlock except from a router, weights and corrections stay consistent, and `_transfer` makes no external calls.\n\n**Test runs:** the full suite passes (58 tests), the live fork test passes, and the two proof files fail as reported. Not covered: Slither or long fuzz runs, and the stock tokens' real blocklist behaviour, which the fork test does not exercise.","treeHash":null,"usage":{"cachedInputTokens":2717827,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":94676,"runtime":"claude","turns":43,"wallClockMs":1533982}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2b9b0095482c54e6","findings":[{"citation":"resolved","description":"The amount a stuck stock falls back as IMD is `imdIn = min(pendingConvert[a], cap)` (and then `min(., stockRoundLimit(a))`), where `cap` is derived from the IMD/USDG pool's depth reading `maxConvert()` unless that reading is below 1% of a full round. A fallback never touches either pool, so these caps have no economic reason to apply to it; they only matter for a real swap. In an abandoned IMD/USDG pool an attacker can repeat the 986abba2 finding-1 setup (move the empty pool's price for free to a made-up tick, then post one narrow position there) and size that position so `maxConvert()` reads just above 0.2 IMD: with a one-tick-spacing range this costs about 0.36 IMD of capital, fully recoverable. The pool then no longer counts as empty, every purchase correctly reverts PriceOff (the new minUsdOut check works), every stock becomes stuck after 30 days, but each fallback round then moves only `cap = 0.203 / 5 = 0.04 IMD` per stock instead of the `MAX_ROUND_IMD / 5 = 4 IMD` the empty-pool rule would move: a 100x slowdown, one round per minute. With one claim every 10 minutes that is about 5.8 IMD per stock per day; a reserve inflow of 0.3% of daily volume above ~1,900 IMD/day then grows faster than the fallback drains it, and the reserves never catch up while the pool stays abandoned. The same clamp also applies on the stale-feed stuck path, where `imdIn` has already been reduced to `stockRoundLimit(a)` (3.4 IMD for GME on the fork) before `_fallBackToImd` runs. No funds are lost and no attacker profit exists; the effect is an indefinite delay of stock-share payouts in the already-abnormal dead-pool state, which is exactly the state the 30-day rule was added for (882666b4 finding 3 was the stopped version of this). Minimal fix: when `stuck` (and in the `poolLimit < cap / 100` branch) fall back `min(pendingConvert[a], MAX_ROUND_IMD / (ASSETS - 1))` regardless of `roundCap` and `poolLimit`, since the fallback does not swap; keep the depth-derived caps for real purchases only.","line":711,"path":"contracts/src/CompanyToken.sol","reproduction":"State: IMD/USDG pool emptied (as in test_final3_1_madeUpPricePositionGetsNothing), pendingConvert = 6 IMD per stock. Attacker swaps 1 wei with sqrtPriceLimit = tick -207000 (IMD worth ~1e-9 USDG) and adds a position [target-90, target+90] with liquidity 2.6e15 (cost measured: 0.3647 IMD + dust USDG). Then token.maxConvert() = 203065671655427927 wei (0.203 IMD, >= 0.2 so imdPoolEmpty = false). Warp 31 days, refresh feeds, call convert(). Expected (empty-pool rule): owed[0] grows by 5 x 4 IMD = 20 IMD and pendingConvert(1) drops to 2 IMD. Actual: owed[0] grows by 203065671655427925 wei (0.203 IMD total, 0.0406 per stock) and pendingConvert(1) = 5.959 IMD. Scratch test test/scratch/ThrottledFallback.t.sol fails on the current code with `203065671655427925 < 19999000000000000000`.","severity":"low","snippet":"        uint256 cap = (imdPoolEmpty ? MAX_ROUND_IMD : roundCap) / (ASSETS - 1);","title":"Stuck-stock fallback is throttled to ~1% of its size by a 0.36 IMD position just above the emptiness threshold"}],"hash":"e4e3485399f8cb648af980c7a3f986bc834fb008521c1da106e7676cd65d72c7","nodeId":"79db9a74-cb4c-41cf-a1fc-8da5c04f774e","outcome":"completed","summary":"The review is complete. One Low finding is recorded in `.imd-findings.json` with a failing Foundry proof under `test/scratch/`. Nothing critical, high or medium survived verification.\n\n**What I checked**\n\nI read all four contracts, the deploy scripts, the full unit suite and the fork test, then traced the four changed areas against the Pashov, x-ray, Trail of Bits and ethskills checklists. The 57 unit tests pass, and the fork test passes against live Robinhood Chain state with all five stocks converting.\n\n**The finding (Low)**\n\nThe 30-day fallback for a stuck stock is sized by the IMD/USDG depth reading even though a fallback never swaps. In an abandoned IMD/USDG pool, an attacker repeats the 986abba2 finding-1 setup and sizes one narrow position so `maxConvert()` reads 0.203 IMD, just above the 1% threshold. Measured cost is 0.36 IMD of recoverable capital. Every purchase then correctly reverts PriceOff, every stock becomes stuck, but each fallback round moves 0.04 IMD per stock instead of 4 IMD. That is a 100x slowdown, and with modest volume the reserves never catch up. The stale-feed path has the same clamp through `stockRoundLimit`. No funds are lost. The minimal fix is to fall back `min(pendingConvert, MAX_ROUND_IMD / 5)` whenever `stuck` is true, keeping the depth caps for real swaps only.\n\n**Confirmations on the four requested items**\n\n- **IMD to USDG check.** The math, decimals and overflow bounds are correct, and no attacker-reachable Panic inside `convertStock` exists that would force a fallback. Pushing the IMD/ETH spot can block a round for one block, at roughly 2% of a swap that moves that pool more than 10%. Loosening the check to sell at a bad price only pays if the IMD/USDG pool is thin and the IMD/ETH manipulation fee is below the round's value, which is far from true at the depth the fork shows. Both pools being empty makes the check vacuous, but then IMD itself is dead.\n- **Stuck rule.** It cannot fire early on a healthy stock unless no purchase happened for 30 days, and then only a genuine failure in that call converts one round to IMD. Nothing an outsider does can reset `waitingSince` without a real purchase, and no path lets a stuck stock avoid falling back when a caller supplies full gas.\n- **Claim ordering and tags.** Expiry runs before flush and convert, so from-pool tags are exact at that point. No distribution can run inside a foreign unlock, so borrowed tokens never shed their tag. The only user code that runs inside a router unlock is an ETH `receive()` on the sell path, after the seller is already active.\n- **Emptiness threshold.** Behaves as described, with the throttle above as its one gap.\n\n**Nothing broke elsewhere.** Solvency holds for all six assets on every path, including the fallback and reverted purchases. The flash-borrow guards, the 100,000 minimum, expiry boundaries and the no-external-call property of transfers are intact.","treeHash":null,"usage":{"cachedInputTokens":2563368,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":68665,"runtime":"claude","turns":39,"wallClockMs":1226980}}],"verification":[]}