{"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":"7736b1b9-14c6-4b78-b72c-878233fad702","kind":"audit","nodes":[{"acceptedSubmissionHash":"44a8f059e497aa8204d4c3f6d9fd69df319a50738efe9a8016ecfeae204fcdd5","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":"eca3d4de54a6edece317761f2cbf578c551d6e475dcfa10a50461ef0b740fb76","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":"6760a00ab9fe18e017ad303ea75fc7b4eeb04007209c26c381a99aaf57902b57","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"197225040cc242b2232325d96967cf10e924863decf362425c3f48ffb85050fa","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":"84a962d779bf2112e3387208fdd43db33f51c46c741ec315994945cdc951c751","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":"go over the v6 folder","parentJobId":null,"planHash":"d44599b6ed9d854a8372b94c9935a42574c9a0543ef58186a5b8f7159145d073","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"7736b1b9-14c6-4b78-b72c-878233fad702","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":"51163","feedbackHash":"27ed34ff41a34a2c26a7c2f8b367d3f5c458a9eaf6e1c9e7f6763e3843809645","nodeKey":"audit_economics","submissionHash":"44a8f059e497aa8204d4c3f6d9fd69df319a50738efe9a8016ecfeae204fcdd5","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52482","feedbackHash":"3becb277b8d3d57c7762e828351a3567c976ab2249b0ecec68514509559e4317","nodeKey":"audit_flow","submissionHash":"eca3d4de54a6edece317761f2cbf578c551d6e475dcfa10a50461ef0b740fb76","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51041","feedbackHash":"4ba88cf292199f82224bb2f385fa2aa8dcb8f6b1afdb8fe5db30ed3a69f5c76d","nodeKey":"audit_judge","submissionHash":"6760a00ab9fe18e017ad303ea75fc7b4eeb04007209c26c381a99aaf57902b57","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51027","feedbackHash":"36c39ca685b1f33f697a8afa9d94a99bf15fa3f625f339abe30f44d6c71a1f35","nodeKey":"audit_math","submissionHash":"197225040cc242b2232325d96967cf10e924863decf362425c3f48ffb85050fa","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51534","feedbackHash":"27b83d2217a7a1b4e1667818a654e18cadc693b54ccfe20c230fd1d7fa8826c3","nodeKey":"audit_permissions","submissionHash":"84a962d779bf2112e3387208fdd43db33f51c46c741ec315994945cdc951c751","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"7aa5bb467e99bfa6179b204eb5171bfa7fc806c11c2f7875176f4cbed5ec2eba","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"61b4050710026370","findings":[{"citation":"resolved","description":"Boundary x invariant. Audit 7 (I-2) moved the per-agent swap count and the per-token turnover window to a rolling 24 hours precisely because a calendar-day reset let a keeper fit two days' budget into a few seconds. The pool-level cap for OPEN pools (`_useCap`, used by `swap()` when `p.guard == Guard.OPEN`) was not changed: it still keys on `block.timestamp / 1 days`, so `used0`/`used1` are zeroed at 00:00:00 UTC regardless of when the budget was last consumed. The invariant the admin relies on when sizing `cap0`/`cap1` (\"most token0 / token1 that may be sold through this pool per UTC day\", and the NatSpec's \"keep those small\" because the OPEN pool has no price feed) is therefore only a bound of 2x cap within any 24-hour period, and the two halves can be adjacent blocks. The farm's own NatSpec documents the cap as per UTC day, so this is an inconsistency with the rolling-window design adopted elsewhere in the same contract rather than a violation of the literal wording; the risk it leaves is that a compromised keeper can push twice the intended volume of agents' USDG through the feed-less MSFT95 pool (where only the keeper's own `minOut` protects the price) around midnight. Neither farm.test.js (line 172-184) nor farm.attack.test.js exercises `_useCap` across a day boundary; the midnight tests at farm.test.js:326-332 and farm.attack.test.js:85-88 cover only the per-agent turnover window.","line":647,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"State: an OPEN pool listed with `setGuard(idx, true, address(0), 0, 0, 0, cap0 = 1000e6, cap1 = 1e30)` where token0 is USDG (6 decimals); agent 7 holds 2000e6 USDG (so its per-agent turnover base is 2000e6 and the 2-swap count is within SWAPS_PER_DAY). Input: (1) at block.timestamp = D*86400 + 86399 (23:59:59 UTC) keeper calls `swap(7, idx, true, 1000e6, 1)`: `_useCap` sets `p.day = D`, `p.used0 = 1000e6 <= cap0`, succeeds. (2) at block.timestamp = (D+1)*86400 (one second later) keeper calls `swap(7, idx, true, 1000e6, 1)`: `today = D+1 != p.day`, `used0` resets to 0, then `used0 = 1000e6 <= cap0`, succeeds. Expected under the rolling-window rule the rest of the contract now follows: the second swap reverts `OverCap` because 2000e6 > cap0 within 24 hours. Actual: 2000e6 of agents' USDG is sold through the feed-less pool in two consecutive seconds. Fix: give `Pool` a `capStart` timestamp and reset `used0/used1` when `block.timestamp >= capStart + 1 days`, exactly as `_useTurnover` does at line 436, and update the NatSpec at line 114.","severity":"low","snippet":"        uint64 today = uint64(block.timestamp / 1 days);\n        if (p.day != today) { p.day = today; p.used0 = 0; p.used1 = 0; }","title":"OPEN-pool daily cap resets at UTC midnight: 2x cap0/cap1 can be sold through the pool within seconds"},{"citation":"resolved","description":"Precision x invariant. `_distribute` adds the full collected fee to `owed[token]` and grows `acc0` by `mulDiv(f0, Q128, liquidity)` (rounded down). `_credit` then pays each agent `mulDiv(s, acc0, Q128) - debt0` (rounded down) minus a further 1 wei, but sets `debt0` to the un-reduced `mulDiv(s, acc0, Q128)`. The subtracted wei is therefore never credited to the agent, never to the treasury, and never to any later credit: it stays counted in `owed[token]`, which `skim()` treats as agents' money. The guard is also unnecessary: with both roundings already floor, sum over agents of floor(s_i * acc / 2^128) <= floor(L * acc / 2^128) <= f0, so per-agent rounding can never exceed what the pool paid without the extra wei. The effect is that `owed[token]` drifts above (sum of balanceOf + treasuryOwed + pendingFees) by one wei per `_credit` with non-zero pending fees, per token, forever; after every agent has exited, that residue can never leave the farm because `skim` computes `bal - owed`. The amounts are dust (1e-6 USDG per credit), so this is an accounting-invariant note, not a fund-loss. farm.test.js only asserts `balanceOf(farm) >= owed` (its `solvent()` helper, line 24), which the drift satisfies, so no test detects it.","line":470,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"State: a farm pool with `p.liquidity = 2_000_000`, two agents each holding `shares = 1_000_000`, `protocolBps = 0`. Input: a swap in the pool earns the position exactly 3 wei of token0; keeper calls `claimFees(agentA, idx)` then `claimFees(agentB, idx)`. Arithmetic: `_distribute`: `acc0 += mulDiv(3, 2^128, 2_000_000)`, `owed[token0] += 3`. `_credit(A)`: `pendingFees = mulDiv(1_000_000, acc0, 2^128) = 1`; `f0 = 1 - 1 = 0`; `debt0[A] = 1`; nothing is credited. Same for B. Result: `owed[token0]` grew by 3, agents' balances grew by 0, `treasuryOwed` by 0, `pendingFees` for both agents is now 0. Both holders `holderRemove` all shares and `withdraw` everything; `skim(token0)` returns 0 although the farm holds the 3 wei and no agent can ever claim them. Expected: `owed[token0] == sum(balanceOf) + treasuryOwed + sum(pendingFees)` up to the acc-floor remainder (here 1 wei). Actual: it exceeds that by 1 wei per credit. Fix: delete the two `-= 1` lines (the floor rounding already guarantees the stated property), or set `debt0/debt1` to `mulDiv(s, acc, Q128) - 1` when the wei is withheld so it stays claimable.","severity":"info","snippet":"        if (f0 > 0) f0 -= 1;\n        if (f1 > 0) f1 -= 1;\n        uint256 s = shares[idx][agentId];\n        debt0[idx][agentId] = FullMath.mulDiv(s, p.acc0, Q128);\n        debt1[idx][agentId] = FullMath.mulDiv(s, p.acc1, Q128);","title":"_credit drops 1 wei per credited token but still advances the agent's debt, so owed[token] is permanently overstated and the wei is unskimmable"},{"citation":"resolved","description":"Boundary x precision. `_fairOut` for a sale of the stock side computes `mulDiv(amountIn, f * 10**dOut, 1e18 * 10**dIn)`; with an 18-decimal stock, 6-decimal USDG and a feed price f = 500e18 the result is `amountIn * 5e-10`, which rounds to 0 for every `amountIn <= 2_000_000_000` (2e-9 stock, about $1e-6). The floor then becomes 0, and if the keeper also passes `minOut = 0` (allowed for ORACLE pools; only OPEN pools require `minOut > 0`), `out < floor_` can never trigger, so the swap may return nothing. The NatSpec promise that every swap returns at least the feed value less slipBps is therefore only true above the truncation threshold. The value at stake per swap is below $1e-6 and the keeper's swap count is capped at 8 per rolling day per agent, so the economic impact is negligible; this is recorded as a precision boundary where the stated invariant lapses.","line":338,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"State: an ORACLE pool with token0 = 18-decimal stock (stockIs0 = true), token1 = USDG (6 decimals), fresh feed at 500e8 with feedDec 8 (so f = 500e18), slipBps = 150; agent 1 holds 1e9 units of the stock. Input: keeper calls `swap(1, idx, true, 1_000_000_000, 0)`. Arithmetic: `_fairOut = mulDiv(1e9, 500e18 * 1e6, 1e18 * 1e18) = floor(5e35 / 1e36) = 0`; `floor_ = 0 * 9850 / 10000 = 0`; `minOut = 0` leaves `floor_ = 0`. Any `out`, including 0, passes `if (out < floor_)`. Expected: the swap returns at least 0.985 * fair value = 492 USDG units, or reverts `TooLittle`. Actual: it may complete with `out = 0`. Fix: compute the floor with rounding up (`FullMath.mulDivRoundingUp`) and/or revert when `floor_ == 0` on ORACLE and PEG pools unless `minOut > 0`.","severity":"info","snippet":"            floor_ = _fairOut(p, zeroForOne, amountIn) * (10000 - p.slipBps) / 10000;","title":"ORACLE swap floor truncates to zero for dust amountIn, so the slipBps bound does not hold for tiny swaps"},{"citation":"resolved","description":"Precision at the small-amount boundary. Both branches of `_fee` round toward the swapper, so the protocol fee is below the nominal 3% of gross USDG by up to one unit per swap, and a swap whose USDG leg is below 34 units (6-decimal USDG: $0.000034) pays no fee at all (`fee == 0` returns ZERO_DELTA at line 160 and 186). The fee is not compoundable and gas on Robinhood Chain far exceeds the 1-unit saving, so there is no profitable farming path; this is recorded because the Math Precision guide asks that fees round up, and `rewards.distribute` ultimately pays this USDG to MSFT95 holders.","line":147,"path":"msft95-contracts-audit-v6/v4/MSFT95RewardHook.sol","reproduction":"Input: a sell of exact MSFT95 that moves 33 USDG units out of the pool (`afterSwap`, USDG unspecified, amountSpecified < 0): `_fee(33, true) = 33 * 300 / 10000 = 0`, so the swapper keeps all 33 units; expected under round-up: 1 unit to the hook. Input: an exact-output buy where the pool takes 9701 USDG units: `_fee(9701, false) = 9701 * 300 / 9700 = 300` (exact value 300.03); the swapper pays 10001 units of which the hook keeps 300, i.e. 2.9997% rather than 3%. Fix: use `Math.mulDiv(amount, b, 10_000, Math.Rounding.Ceil)` and `Math.mulDiv(amount, b, 10_000 - b, Math.Rounding.Ceil)`, or accept and document that fees round in the swapper's favour.","severity":"info","snippet":"        return gross ? amount * b / 10_000 : amount * b / (10_000 - b);","title":"Hook fee rounds down and is zero for USDG amounts below 34 units; exact-output fee is floor(amount*3/97)"}],"hash":"197225040cc242b2232325d96967cf10e924863decf362425c3f48ffb85050fa","nodeId":"96eb38dd-8f8f-4bf5-a3b5-89031a761d2b","outcome":"completed","summary":"Review complete. The findings file at `.imd-findings.json` holds four substantiated items, all low or info. No file in the repository was modified.\n\n**Scope covered.** I audited every Solidity file in `msft95-contracts-audit-v6` against the Math Precision, Boundary and Numerical Gap guides: the vault, the agents NFT, the token, the staking contract, the vesting and agent-stakes contracts, the reward hook, the router and the agent farm. For each value-moving formula I traced concrete inputs through decimals, rounding direction, intermediate overflow, zero and max boundaries, and the accounting invariants that depend on them. I also verified by script that the three agent id tables hold exactly 3,333 unique ids with no gaps, and I diffed v6 against v5 to focus on the changed turnover-window code.\n\n**What held up.** The dividend accounting in the token, the streamed-reward maths in staking, the vesting and unlock schedules, the vault's windows and caps, the hook's before/after fee split against Uniswap v4 delta semantics, and the farm's liquidity, price, fair-value and share-accounting formulas are all consistent at the medium-or-above level. Every division I checked rounds against the party who could farm it, every intermediate product fits with the decimal bounds the contracts enforce, and the solvency invariants in staking and the farm hold under rounding.\n\n**Findings written.**\n- **Low, farm OPEN-pool cap.** The pool-level daily cap still keys on the UTC calendar day, so two full budgets can be sold through the feed-less MSFT95 pool in consecutive seconds around midnight. Audit 7 fixed the same pattern for the per-agent windows but left this one. Concrete two-swap sequence given.\n- **Info, farm fee credit.** The 1-wei withholding in `_credit` is never credited to anyone yet is still counted in `owed`, so `owed` drifts above real claims forever and `skim` can never recover it. The guard is also unnecessary given the existing floor rounding.\n- **Info, farm oracle floor.** For dust stock sales the fair-value floor truncates to zero, so the slip bound lapses when the keeper passes a zero minimum. Value at stake is under a millionth of a dollar per swap.\n- **Info, hook fee rounding.** Fees round toward the swapper and are zero below 34 USDG units.\n\n**Limits of this review.** The project is a Hardhat build with no vendored dependencies and no Foundry configuration, so nothing was compiled or executed here. All conclusions rest on reading the code, the pinned OpenZeppelin 5.1.0 and Uniswap v4-core 1.0.2 semantics, and arithmetic checked by script. Since no finding reached high severity, no proof test was required or written. A clean result on the remaining formulas is evidence of careful review, not proof that no defect exists.","treeHash":null,"usage":{"cachedInputTokens":1385147,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":54138,"runtime":"claude","turns":25,"wallClockMs":821361}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8af9903f4ad1eed0","findings":[{"citation":"resolved","description":"The cosigner check is per voucher (amount > cosignAbove), while the only cumulative limit (dailyCap in _spend) is enforced with or without a cosigner. Nonces are per user and chosen by the signer, and anyone may submit vouchers, so a holder of the SIGNER key alone can issue many vouchers of exactly cosignAbove to an address it controls and pay out the full dailyCap of every listed token, and 2x dailyCap across a window edge, without the COSIGNER key ever being involved. The stated purpose of the COSIGNER ('held on separate infrastructure', required for 'large withdrawals') is therefore not met for the large flow that matters: the second key only gates single large vouchers, not large totals. With the launch limits in scripts/mainnet/lib.js (LIMITS) the single-key, cosigner-free daily extraction is: USDC 20 vouchers x 250 = 5,000 (the entire dailyCap); MSFT95 20 x 5,000 = 100,000 per window, 200,000 inside 24h at the window edge; ETH 20 x 0.1 = 2 ETH. Fix that preserves the design: track a per-token cosigner-free window (e.g. uncosignedWindow[token] spent in _spend) so that once uncosigned payouts in the rolling 24h exceed cosignAbove (or a separate uncosignedDailyCap) further vouchers need the cosigner; or make cosignAbove a per-user rolling total instead of a per-voucher threshold.","line":253,"path":"msft95-contracts-audit-v6/contracts/MSFT95Vault.sol","reproduction":"State: vault deployed with the launch config for USDC: maxWithdrawal 1000e6, dailyCap 5000e6, cosignAbove 250e6; SIGNER key S held by the attacker, COSIGNER key never used. Input: 20 vouchers Withdrawal{user: attacker, token: USDC, amount: 250e6, nonce: 1..20, epoch: voucherEpoch, issuedAt: now, deadline: now+1h}, each signed only by S; call withdraw(w_i, [sigS_i]) 20 times in one block. Expected (per the NatSpec 'Large withdrawals also need a second signature from a COSIGNER'): a flow of 5,000 USDC leaving in one block requires the cosigner. Actual: every call passes because 250e6 > 250e6 is false, so needCosigner is false and _verify accepts the single signature; _spend accumulates to exactly dailyCap 5000e6 (spent > cap is false at equality) and 5,000 USDC leaves. Repeat at window.start + 1 days for another 5,000, i.e. 10,000 USDC within 24h and 1 second, cosigner never consulted.","severity":"low","snippet":"        _verify(w, sigs, w.amount > c.cosignAbove);","title":"Vault cosigner requirement is bypassed by splitting: one stolen SIGNER key moves the whole dailyCap with no second signature"},{"citation":"resolved","description":"Fees are distributed into per-liquidity accumulators by _distribute (owed[token] grows) but the protocol's cut is only computed in _credit, at credit time, from the current protocolBps. setTreasury changes protocolBps immediately and the farm's documented protection ('A new or re-tuned pool only becomes usable after delay, so holders see it coming and can freeze the keeper for their agent or leave') does not cover it: there is no usableAt-style delay on the fee cut, and no snapshot of the cut at collection time. So the admin can raise the cut up to MAX_PROTOCOL_BPS (20%) and that rate is taken out of fees that were earned under a lower rate and are merely uncredited (which is the normal state: fees are credited lazily on add/remove/claimFees). Holders cannot defend themselves because they have no advance notice, and their only tool, setFrozen, does not stop _credit from being applied when they later exit (holderRemove calls _credit). Unprivileged amplifier: retroactive sweep of already-earned value. Impact is bounded to 20% of uncredited fees per agent, hence low. Fix preserving the owner role: either apply protocolBps changes after `delay` (store pendingBps/pendingAt and use the old value until pendingAt), or accrue the protocol cut inside _distribute at collection time (acc0 gets f0*(10000-protocolBps)/10000, treasuryOwed gets the rest) so a later change cannot touch fees already collected.","line":413,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"State: one ORACLE farm pool, protocolBps = 500 (5%), agent #1 holds all the liquidity and the position has accrued 100e6 USDG of swap fees in the pool; nobody has called claimFees. pendingFees(1, idx) returns 100e6 (before cut), so the agent expects 95e6 credited. Input: admin calls setTreasury(treasury, 2000), then the holder calls claimFees(1, idx) (or holderRemove). Expected: 95e6 - 1 credited to balanceOf[1][USDG] (the 5% that applied when the fees were earned), treasuryOwed[USDG] = 5e6. Actual: _credit computes c0 = (100e6 - 1) * 2000 / 10000 = 19,999,999, credits 80,000,000 to the agent and 19,999,999 to treasuryOwed: 15e6 USDG of fees the agent had already earned move to the treasury, with no delay and no event the holder could have acted on in advance.","severity":"low","snippet":"        treasury = t; protocolBps = bps;","title":"Farm setTreasury applies a new protocol cut retroactively to fees agents already earned, with no delay"},{"citation":"resolved","description":"Audit 7 I-2 moved the per-agent turnover and swap count to a rolling 24-hour window because a calendar-day reset let a keeper 'fit two days' budget into a few seconds'. The same shape remains in _useCap for OPEN pools: `today = block.timestamp / 1 days` and used0/used1 reset when the day changes. OPEN pools are exactly the pools with no feed floor (the MSFT95/USDG pool): the only protections on a keeper swap there are the keeper's own minOut (BadConfig only requires minOut > 0, so minOut = 1 passes) and cap0/cap1, which deploy-farm.js sizes at M95_DAY_CAP (default $1,000 of USDG buys per day). A compromised keeper that is also the pool's counterparty therefore extracts up to 2 x cap within two seconds around 00:00 UTC, in addition to the rolling per-agent turnover (which is per agent, so with many agents the pool cap is the binding limit). The per-agent turnover does not help across agents: the pool cap is the only aggregate limit and it is the one still on a calendar boundary. Fix: use the same rolling window as _useTurnover (store a start timestamp; reset when block.timestamp >= start + 1 days), or document cap0/cap1 as 'up to 2x per 24h' and halve the configured value.","line":648,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"State: MSFT95/USDG v4 pool listed OPEN with cap0 = 1000e6 USDG (cash is token0), active and past usableAt; 16 agents each holding >= 125e6 USDG (so neither the per-agent turnover nor SWAPS_PER_DAY binds); keeper key compromised, attacker provides the counter-liquidity. Input: at block.timestamp = D*86400 - 1 the keeper calls swap(agent_i, idx, true, 125e6, 1) for agents 1..8 (used0 = 1000e6 = cap0, a 9th call reverts OverCap); at block.timestamp = D*86400 (next block, 1 second later) it calls swap(agent_i, idx, true, 125e6, 1) for agents 9..16. Expected (the documented intent of the audit 7 fix: budgets are per rolling 24 hours): the second batch reverts OverCap until 24 hours after the first. Actual: p.day != today resets used0 to 0 and all 8 further swaps succeed; 2,000e6 USDG is sold through the OPEN pool at the attacker's price (minOut = 1) within one second, twice the configured daily cap.","severity":"low","snippet":"        if (p.day != today) { p.day = today; p.used0 = 0; p.used1 = 0; }","title":"OPEN-pool daily caps (cap0/cap1) still reset at midnight UTC, so two days' budget fits in two seconds on the pool with no price floor"},{"citation":"resolved","description":"Two claims in the keeper-loss documentation (which the README asks reviewers to check: 'whether anything still compounds across windows faster than the documented bound') do not hold. (1) 'within any 24 hours ... at most what the agent held': the window opens at the first swap and resets 86400 s later, so used can reach base in the last second of window 1 and again in the first second of window 2; any 24h span straddling the boundary sees 2x base per token and 16 swaps, the same 2x the vault documents for dailyCap but omitted here. (2) 'A round trip USDG -> stock -> USDG costs at most about 2 x the stock pool's slipBps ... USDG -> USDC -> stock -> USDC is 2 x stock slip + 1 x peg slip': turnover is per input token, so with two listed dollar tokens and stock pools quoted in each (or one stock listed against both), the path USDG -> S1 (USDG/S1) -> USDC (S1/USDC) -> S2 (USDC/S2) -> USDG (S2/USDG) uses each input token once and crosses four stock pools: 4 x stock slip per window (8 legs, 4 x slip at the boundary), each leg taken by a keeper that is the counterparty. With slipBps 150 that is about 6% per window instead of the stated about 3%, and about 12% in a two-second span at a window edge. No code change is strictly required; the bound should be stated correctly so operators size slipBps and agents' deposits against the real number, or the window should be anchored (e.g. cap total swapped value per agent per rolling 24h across tokens).","line":60,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"State: tokens USDG, USDC, S1, S2 listed via ORACLE pools USDG/S1, S1/USDC, USDC/S2, S2/USDG, all fresh and on-price; agent #1 holds 1,000e6 USDG and nothing else; slipBps 150 everywhere. Input, at t0: swap(1, USDG/S1, sell USDG 1000e6) [USDG base 1000e6 used 1000e6]; swap(1, S1/USDC, sell all S1) [S1 base = received, used all]; swap(1, USDC/S2, sell all USDC) [USDC base = received, used all]; swap(1, S2/USDG, sell all S2) [S2 used all]. Each leg passes _useTurnover (first use of each token) and returns fair x 0.985, so the agent ends with about 1000e6 x 0.985^4 = 941e6 USDG, a 5.9% loss. Expected from the NatSpec: at most about 2 x 1.5% = 3% for a stock round trip plus a small peg slip. Then at t0 + 86400 the same four legs run again on the fresh window (another 5.9%); the two batches sit one second apart, so inside one 24-hour span the agent lost about 11.5%, against a documented 'within any 24 hours ... at most what the agent held' once.","severity":"info","snippet":"///   - Per agent and per token, within any 24 hours the keeper can swap at most what the agent held of that token at its\n///     first swap of the window. The window is rolling (it starts at that first swap), like the swap count, so nothing","title":"Farm NatSpec loss bound is understated: a rolling window allows 2x turnover inside any 24h and two dollar tokens allow 4 stock legs per window"},{"citation":"resolved","description":"_distribute adds the full collected fee to owed[token] and to the accumulator. _credit then pays the agent floor(s*acc/Q128) - debt minus 1 wei, but resets debt to the full floor(s*acc/Q128), so the subtracted wei is neither credited to anyone nor removed from owed. skim only returns balance - owed, and owed still counts the wei, so it is unreachable by agents, treasury and skim alike: each credit of each token strands 1 wei (plus the floor-rounding dust of _distribute, which is likewise inside owed). The subtraction is also unnecessary for its stated purpose ('per-agent rounding can never add up to more than the pool actually paid'): per-agent amounts are already floors of exact shares whose sum cannot exceed the collected total. Impact is dust, non-compounding, so informational. Fix: drop the -1, or add the subtracted wei to treasuryOwed[token] so owed stays claimable, or let skim treat owed as sum(balances) + treasuryOwed + sum(pendingFees).","line":470,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"State: one farm pool, one agent holding all liquidity, protocolBps 0; the position collects exactly 5 wei of token0 fees. Input: claimFees(agent, idx). Expected: owed[token0] equals what is claimable, i.e. the agent is credited 5 wei (or 4 with 1 wei going to the treasury), and skim(token0) returns any excess. Actual: _distribute sets owed[token0] = 5 and acc0 = 5*Q128/L; pendingFees returns 5; _credit credits 4, sets debt0 to the full 5-wei share so pendingFees becomes 0; owed[token0] stays 5 while balanceOf sums to 4 and treasuryOwed is 0; skim(token0) computes bal(5) - owed(5) = 0 and returns 0. The 1 wei can never leave the contract; every subsequent credit strands another.","severity":"info","snippet":"        if (f0 > 0) f0 -= 1;\n        if (f1 > 0) f1 -= 1;","title":"Fee rounding leaves wei permanently stuck: _credit drops 1 wei per credit into owed, which skim can never recover and no agent can claim"}],"hash":"44a8f059e497aa8204d4c3f6d9fd69df319a50738efe9a8016ecfeae204fcdd5","nodeId":"bf5ab486-5c9e-40b7-9752-accb2a6784e3","outcome":"completed","summary":"Review complete. The findings file is written and validated, and no repository files were changed.\n\n**Scope covered.** All on-chain code in `msft95-contracts-audit-v6` (vault, agents NFT, token, reward hook, router, staking, vesting, agent farm, agent stakes, Create2 deployer) was read in full against the Economic Security, Invariant and Flow Gap guides, plus the launch and farm deploy scripts for the economic parameters they set. I also checked the generated agent tier lists for duplicates and completeness (3,333 unique ids, disjoint tiers, allocations sum to 249,967,700).\n\n**Core invariants held.** Farm `owed[token] <= balance` across V3 and V4 add, remove, swap and fee paths, `p.liquidity == sum(shares)`, staking `owed + streaming <= held`, token dividend accounting across exclusions, and hook fee consistency between `beforeSwap` and the `PartialFill` check all traced correctly. No critical or high defect survived, so no Foundry proof files were written.\n\n**Findings reported (5), in `.imd-findings.json`:**\n\n- **Low, Vault line 253.** The cosigner check is per voucher, so a stolen SIGNER key alone pays the full `dailyCap` by splitting into vouchers of exactly `cosignAbove`. With launch limits that is 5,000 USDC or 100,000 MSFT95 per window, double at a window edge, cosigner never consulted.\n- **Low, Farm line 413.** `setTreasury` changes `protocolBps` immediately and `_credit` applies it to fees already earned but not yet credited. Admin can move up to 20% of pending fees with no delay, unlike pool re-tuning which waits `delay`.\n- **Low, Farm line 648.** OPEN-pool `cap0`/`cap1` still reset at midnight UTC. The audit 7 fix made turnover and swap count rolling but left the pool cap, which is the only aggregate limit on the pool with no price floor. Two days' budget fits in two seconds.\n- **Info, Farm line 60.** The documented keeper-loss bound is understated: 2x turnover inside any 24h at the window edge, and with two dollar tokens a 4-stock-leg path per window (about 6% at slip 150, not about 3%).\n- **Info, Farm line 470.** The `f0 -= 1` in `_credit` strands 1 wei per credit inside `owed`, unreachable by agents, treasury or `skim`.\n\n**Trust assumptions noted, not reported as defects.** Keeper-as-counterparty losses bounded by `slipBps` per leg, the OPEN pool relying on keeper `minOut`, farm MSFT dividends going to the treasury rather than agents, and the Safe's ability to raise caps are all documented design choices.","treeHash":null,"usage":{"cachedInputTokens":1892166,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":48247,"runtime":"claude","turns":26,"wallClockMs":767172}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3bed38612db34f32","findings":[{"citation":"resolved","description":"Audit 7 I-2 moved the per-agent turnover window (_useTurnover, line 436) and the per-agent swap count (_swapDay, line 330) to a rolling 24 hours because a calendar-day reset let a keeper fit two days' budget into a few seconds. The third limit checked by swap(), the pool-wide cap0/cap1 that _useCap enforces for OPEN pools, was left keyed on block.timestamp / 1 days (lines 647-648), so used0/used1 are zeroed at 00:00:00 UTC whatever was spent at 23:59:59. OPEN pools are the ones with no feed and no floor (the MSFT95/USDG pool: deploy-farm.js sizes its cap at M95_DAY_CAP), where the only protections on a keeper swap are the keeper's own minOut (minOut = 1 passes the BadConfig check) and this cap. The per-agent turnover does not help because the cap is shared across all agents and a keeper spreads sales over many of them. The farm NatSpec (line 69) names cap0/cap1 as the bound to 'keep small', and the README's v6 bullet and MAINNET.md present the farm's limits as no longer resetting at midnight, but the aggregate bound is really 2x cap in any 24-hour span, with the two halves in adjacent blocks. Reported independently by four specialists (economics, permissions, math, flow); merged here. Fix: give Pool a window start (reset used0/used1 when block.timestamp >= start + 1 days, exactly as _useTurnover does) and update the NatSpec at line 114; or document cap0/cap1 as 'up to 2x per 24h' and halve the configured value.","line":648,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"Hardhat test run against this code (test-v4 harness: PoolManager, V4TestToken USDG 6 dec / MSFT95 18 dec, FarmMockAgents, farm delay 0). State: MSFT95/USDG v4 pool listed OPEN; setGuard(idx, true, 0, 0, 0, 0, cap on the USDG side = 1000e6, 1e30 on the other); agents 1 and 2 each deposited 2000e6 USDG; keeper has KEEPER_ROLE. Input: evm_setNextBlockTimestamp(midnightUTC - 3); swap(1, idx, usdIs0, 1000e6, 1) succeeds (used = cap). evm_setNextBlockTimestamp(midnightUTC - 2); swap(2, idx, usdIs0, 1e6, 1) reverts OverCap (cap holds within the day). evm_setNextBlockTimestamp(midnightUTC); swap(2, idx, usdIs0, 1000e6, 1). Expected under the rolling-window rule the rest of swap() follows: revert OverCap until midnightUTC - 3 + 86400. Actual: the call succeeds; pool(idx).used0/used1 reads 1000e6 again, agent 1 and agent 2 each hold 1000e6 USDG less, i.e. 2000e6 USDG (2x cap) was sold through the OPEN pool at minOut = 1 across one second. The existing suites never cross a day boundary on _useCap: farm.test.js 172-184 checks the cap inside one day and the midnight tests (farm.attack.test.js 85-88) cover only the per-agent turnover.","severity":"low","snippet":"        if (p.day != today) { p.day = today; p.used0 = 0; p.used1 = 0; }","title":"Agent farm: OPEN-pool daily caps (cap0/cap1) still reset at UTC midnight, so 2x cap can be sold through the feed-less pool within seconds"},{"citation":"resolved","description":"The farm NatSpec (lines 70-72) says a re-tuned pool 'only becomes usable after delay, so holders see it coming and can freeze the keeper for their agent or leave'. setGuard and listV3/listV4 honour that through usableAt; setTreasury does not: the new treasury and protocolBps take effect in the same transaction. Fees are distributed into per-liquidity accumulators by _distribute at collection time but the protocol's cut is only computed in _credit (line 476, f0 * protocolBps / 10000) when the agent's fees are credited, lazily on add/remove/claimFees. Raising the cut (up to MAX_PROTOCOL_BPS = 20%) therefore reaches fees that were earned while a lower rate was advertised and are merely uncredited, which is the normal state between claims. The holder's only tool, setFrozen, does not prevent _credit from running when they later exit (holderRemove calls _credit), and there is no event ahead of the change to act on. The admin role is a trust assumption; the defect is that this one fee-affecting parameter skips the notice period the design promises. Impact is bounded to 20% of uncredited fees per agent. Reported by economics and permissions specialists; merged. Fix preserving the admin's power: queue the change (pendingBps/pendingTreasury, effectiveAt = now + delay) and apply it only after delay, or take the cut inside _distribute at collection time so a later change cannot touch fees already collected.","line":413,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"Hardhat test run against this code. State: farm deployed with protocolBps 500 (5%), delay 0 (the value does not matter: no delay path exists for setTreasury); agent 1 enters the AAPL/USDG ORACLE pool with $10,000; an outside trader churns the pool 4 times each way so the position earns fees; nothing has been credited. Measured by snapshot: claimFees(1, idx) at the advertised 5% credits USDG 179650 / AAPL 718609027594033 to the agent and USDG 9455 / AAPL 37821527768106 to treasuryOwed. Revert the snapshot. Input: admin calls setTreasury(treasury, 2000) then keeper calls claimFees(1, idx). Expected (per the stated design): the new rate cannot apply before delay, so the holder can claim at 5% first, and fees earned at 5% are credited at 5%. Actual: the same fee total (189105 USDG units) is split 151284 to the agent and 37821 to treasuryOwed, i.e. 20% is taken out of fees earned at 5%, in the very next block after the change, and pool(idx).usableAt is unchanged (no delay started).","severity":"low","snippet":"        treasury = t; protocolBps = bps;","title":"Agent farm: setTreasury changes protocolBps with no delay, and _credit applies the new cut to fees earned under the old rate"},{"citation":"resolved","description":"The contract NatSpec (lines 17-19) states that 'Mint money follows the tokenomics and nobody can take more than their share: distribute() ... sends 70% into the casino vault's house bankroll, 10% to the team wallet and 20% to the liquidity wallet'. setPayees (line 153) has no delay and _setPayees only checks that vault_ has code and that team_ and lp_ are non-zero. It does not check that vault_ is the MSFT95Vault or that it credits a bankroll, and the three payees may be the same address. The owner (the Safe after handover, the deployer before it) can therefore point all three legs at addresses it controls and call distribute() (anyone may). This is an owner trust assumption, not a permission bypass, but the written guarantee is false as the code stands and players are told the 70% backs the casino bankroll. Fix options that keep the role: make casinoVault immutable or changeable only behind a delay with an event; require that vault_ has granted this contract BANKROLL_ROLE (IAccessControl(vault_).hasRole); or correct the NatSpec to say the owner can redirect proceeds.","line":156,"path":"msft95-contracts-audit-v6/contracts/MSFT95Agents.sol","reproduction":"Hardhat test run against this code (0.8.24 build). State: MSFT95Agents deployed with saleOpen = true, prices 0.1/0.05/0.02 ETH, casinoVault = a contract with a payable fundBankroll (stand-in for the vault), team and lp = separate EOAs; a buyer mints 10 Executive agents, so address(agents).balance = 1 ETH. Input: the owner deploys Sink { function fundBankroll(address,uint256) external payable {} function pull(address to) external { payable(to).transfer(address(this).balance); } } and calls setPayees(address(sink), owner, owner); the buyer calls distribute(); anyone calls sink.pull(owner). Expected per the NatSpec: setPayees rejects a payee set that does not pay 70% into the vault's bankroll (or the change waits behind a delay), and the vault receives 0.7 ETH. Actual: setPayees succeeds (sink has code, owner is non-zero), distribute() sends 0.7 ETH to the sink and 0.3 ETH to the owner, the original vault receives 0, and after pull the owner holds the full 1 ETH.","severity":"low","snippet":"        if (vault_.code.length == 0 || team_ == address(0) || lp_ == address(0)) revert BadPayee();","title":"Agents NFT: the owner can repoint all three payees at itself at once via setPayees, taking 100% of mint proceeds despite the NatSpec 'nobody can take more than their share'"},{"citation":"resolved","description":"The NatSpec (lines 14-17) promises that 'the owner can also exclude other contracts (a router, a presale) but never a wallet', and that the staking contract and the games vault 'stay eligible'. The only check in setExcluded is account.code.length != 0. Safe multisigs (including the project's own TREASURY/OPS Safes and any holder's Safe), ERC-4337 smart accounts and, on chains with Pectra, EOAs carrying an EIP-7702 delegation all have code, so the owner can stop any of them from earning future MSFT dividends; their share is then re-shared across the remaining eligible holders, the owner's own holdings included. Nothing stops excluding MSFT95Staking or the vault either, which cuts stakers off from the staked MSFT95's MSFT. What was already earned stays claimable (credit[]), so the loss covers future distributions only. This is an owner trust assumption, documented here because the NatSpec states a guarantee the code does not enforce. Fix: pin the excludable set at deployment (or an owner allowlist additions to which wait behind a delay), explicitly refuse the staking contract and vault, or reword the NatSpec to the real rule ('any address with code').","line":128,"path":"msft95-contracts-audit-v6/v4/MSFT95Token.sol","reproduction":"Hardhat test run against this code. State: MSFT95Token with 1,000,000,000 supply minted to the owner; 10,000,000 MSFT95 transferred to a contract-wallet stand-in (any address with code; a Safe behaves the same), 490,000,000 to Alice; eligibleSupply = 1e9 tokens. Input: owner calls setExcluded(contractWallet, true), then anyone calls distribute(1000 MSFT). Expected per the NatSpec ('never a wallet'): setExcluded reverts NotAContract for a wallet and the contract wallet earns 1000 * 10M / 1e9 = 10 MSFT, Alice 490 MSFT. Actual: setExcluded succeeds, excluded[contractWallet] = true, eligibleSupply drops to 990,000,000; after the distribution claimable(contractWallet) = 0 and claimable(alice) = 494.95 MSFT (the excluded wallet's share re-shared). Then setExcluded(MSFT95Staking, true) also succeeds and excluded[staking] = true, so stakers stop receiving MSFT from the staked supply.","severity":"low","snippet":"            if (account.code.length == 0) revert NotAContract(account);","title":"MSFT95Token: 'never a wallet' exclusion rule relies on code.length, so the owner can exclude Safe/contract wallets and the staking contract from dividends"},{"citation":"resolved","description":"once() handles a recorded transfer that has no receipt by calling build() and then signAndSend(key, req, s.nonce, true) (line 333): it re-signs the SAME nonce with higher fees so only one copy can ever land. For 'treasury' and 'liquidity' build() is a pure populateTransaction (line 348), so this works, and rehearse.js section 10 c exercises it only on 'liquidity' (faults.crash = 'liquidity:unsendable'). For 'economy' build() first runs send(m.approve(...)): a real transaction from the deployer account through the hardhat signer, with an automatically chosen (pending) nonce, awaited with tx.wait() (line 107) and no timeout. Both possible states of a stuck fundBankroll at nonce N break the same-nonce design. (1) N is still pending but underpriced: the approve takes nonce N+1 and wait() blocks until N is mined, which is exactly what is not happening; the launch hangs inside build() before reaching the re-sign, with no message, and re-running repeats the hang. (2) N was dropped by the node: the approve is mined as nonce N; signAndSend then signs fundBankroll with nonce N, broadcast fails ('nonce too low', only logged at line 315), receiptOf finds nothing and the run throws 'isn't mined yet: run again' (line 345); the next run sees used (N+1) > s.nonce and stops with 'no receipt ... although the deployer's nonce N is used' (line 329), and the explorer shows nonce N was the script's own approve, so the operator has to work out that the economy part was never paid and delete split.economy by hand. No double payment occurs in either state, but the resume that audit 7 M-1 introduced does not work for the first and largest part (250,032,300 MSFT95 to the vault's bankroll). The approve is also unnecessary on the re-sign path: the allowance set before the original fundBankroll is still in place because fundBankroll never executed. Fix: on the re-sign path do not send anything inside build(): check allowance first and approve only when allowance < amount, give that approve an explicit recorded nonce like the parts themselves, or make the approve its own recorded part before 'economy'.","line":349,"path":"msft95-contracts-audit-v6/scripts/mainnet/lib.js","reproduction":"Code trace (script, no chain state needed). State: deployments/<network>Launch.json has steps.vault done and split.economy = {hash: H, nonce: N, raw: R} where R was broadcast with a maxFeePerGas below what the chain now needs (or the node dropped it); the deployer's latest nonce == N. Run launch.js (resume). Expected (per the comments at lines 277-286, MAINNET.md section 3 and AUDIT-7 M-1): the economy transfer is re-signed with nonce N and higher fees and pays once. Actual path: once('economy') -> receiptOf -> null -> rebroadcast R (line 323) -> still no receipt -> used (N) > N is false -> line 333 evaluates build() -> line 349 runs send(m.approve(vault, 250032300e18)) = (await tx).wait() with the signer's pending nonce. If R is still pending in the node: the approve queues at nonce N+1 and wait() never returns; the launch hangs with no message. If R was dropped: the approve mines as nonce N; the fundBankroll re-signed with nonce N is rejected by every node; the run ends with 'economy: ... isn't mined yet: run again'; the next run ends with 'no receipt for H, ... although the deployer's nonce N is used', and the vault's bankroll stays unpaid until the operator deletes split.economy by hand. Compare rehearse.js lines 260-263, where the identical fault on 'liquidity' passes because that build() sends nothing; no rehearsal check faults 'economy'.","severity":"low","snippet":"      await once(\"economy\", async () => { await send(m.approve(A().vault, parts.economy[1])); return V.fundBankroll.populateTransaction(A().msft95, parts.economy[1]); });","title":"Launch split: the economy part's re-sign path first sends a fresh approve from the deployer, so a same-nonce re-sign either hangs or can never be mined"},{"citation":"resolved","description":"Two claims in the keeper-loss documentation (which the README asks reviewers to check: 'whether anything still compounds across windows faster than the documented bound') do not hold. (1) 'within any 24 hours ... at most what the agent held' (line 60): the window opens at the first swap and resets 86400 s later, so used can reach base in the last second of window 1 and again in the first second of window 2; any 24h span straddling the boundary sees 2x base per token and 16 swaps, the same 2x the vault NatSpec documents for dailyCap (MSFT95Vault.sol line 26) but omitted here. (2) 'A round trip USDG -> stock -> USDG costs at most about 2 x the stock pool's slipBps ... USDG -> USDC -> stock -> USDC is 2 x stock slip + 1 x peg slip': turnover is tracked per (agent, input token) and the base of each token's window is read when that token is first sold, including what just arrived from a previous leg. With two listed dollar tokens and stock pools quoted in each, the path USDG -> S1 -> USDC -> S2 -> USDG uses each input token once and crosses four stock pools: about 4 x stock slip per window (about 6% at slipBps 150 instead of the stated about 3%), and about 12% in a two-second span at a window edge, each leg taken by a keeper that is also the counterparty. No code change is strictly required; the bound should be stated correctly so operators size slipBps and agents size their deposits against the real number, or the window should cap total swapped value per agent per rolling 24h across tokens. Documentation-only, so informational.","line":65,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"Hardhat test run against this code (two tokens suffice to show the per-token accounting). State: ORACLE AAPL/USDG pool, slipBps 150, fresh feed at $250; agent 1 holds 1000e6 USDG and 10e18 AAPL. Input, in one block: swap(1, idx, sell USDG, 1000e6, 0) then swap(1, idx, sell AAPL, <entire AAPL balance including what the first leg returned>, 0). Expected from the NatSpec: one turnover of 'what the agent held' per 24h. Actual: both legs pass _useTurnover; turnover(1, USDG) = used 1000000000 / base 1000000000 and turnover(1, AAPL) = used 13987748549259055285 / base 13987748549259055285, i.e. the AAPL leg's base includes the USDG just converted, so the same dollars cross a stock pool twice in one window (two legs, 2 x slip) and, with a second dollar token and a second stock, four times. Arithmetic for the 4-leg path at slipBps 150: 1000 x 0.985^4 = 941 USDG (5.9% per window); repeating at window.start + 86400 gives about 11.5% inside one 24-hour span.","severity":"info","snippet":"///     A round trip USDG -> stock -> USDG costs at most about 2 x the stock pool's slipBps of the agent's cash per 24","title":"Agent farm NatSpec understates the keeper loss bound: 2x turnover fits inside any 24h at a window edge, and per-token windows allow four stock legs per window"},{"citation":"resolved","description":"_distribute adds the full collected fee to owed[token] and grows acc by mulDiv(f, Q128, liquidity) (floor). _credit then credits the agent floor(s*acc/Q128) - debt minus a further 1 wei, but sets debt to the un-reduced floor(s*acc/Q128) (lines 473-474). The subtracted wei is neither credited to the agent nor to treasuryOwed nor to any later credit; it stays counted in owed[token], which skim() treats as agents' money (line 279). The guard is also unnecessary for its stated purpose ('per-agent rounding can never add up to more than the pool actually paid'): with both roundings already floors, the sum over agents of floor(s_i*acc/Q128) <= floor(L*acc/Q128) <= f. The effect is that owed[token] exceeds sum(balanceOf) + treasuryOwed + sum(pendingFees) by one wei per credit with non-zero pending fees, per token, forever; after every agent has left, that residue can never leave the farm because skim computes bal - owed. Amounts are dust (1e-6 USDG per credit) and do not compound, so informational. farm.test.js only asserts balanceOf(farm) >= owed (its solvent() helper), which this drift satisfies. Reported by economics and math specialists; merged. Fix: delete the two -= 1 lines, or add the withheld wei to treasuryOwed[token] so owed stays claimable, or set debt to the reduced value so the wei stays pending.","line":470,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"Hardhat test run against this code. State: farm with protocolBps 0; agent 1 is the only liquidity holder in the AAPL/USDG v4 pool; fees have been collected into the accumulators by a claim for agent 2 (which holds nothing), so pendingFees(1, idx) = USDG 94553 / AAPL 378215277681070 and nothing new is collected on the next call. Input: keeper calls claimFees(1, idx). Expected: the agent is credited 94553 / 378215277681070 (or the remainder goes to the treasury) and owed[token] stays equal to what is claimable. Actual: balanceOf(1) grows by 94552 / 378215277681069, pendingFees(1, idx) becomes 0 / 0, treasuryOwed stays 0, and owed[USDG] is unchanged, so 1 wei of each token is now counted as owed to nobody. The holder then calls holderRemove(1, idx, all shares) and withdraw(1, token, max) for both tokens: the farm still holds 3 USDG units, owed[USDG] says 3, no agent has a balance or pending fees, and skim.staticCall(USDG) returns 0 (bal - owed = 0). The 3 units can never leave the contract; every further credit strands another.","severity":"info","snippet":"        if (f0 > 0) f0 -= 1;\n        if (f1 > 0) f1 -= 1;","title":"Agent farm: _credit withholds 1 wei per token but advances the agent's debt to the full share, so owed[token] drifts up and the wei is unclaimable and unskimmable"},{"citation":"resolved","description":"_fairOut for a sale of the stock side computes mulDiv(amountIn, f * 10**dOut, 1e18 * 10**dIn) (line 638). With an 18-decimal stock, a 6-decimal dollar token and a feed price f the result is amountIn * f / 1e30, which rounds to 0 for every amountIn below 1e30 / f (4e9 wei of stock at $250, about $1e-6). The floor then becomes 0, and because minOut = 0 is accepted on ORACLE pools (only OPEN pools require minOut > 0, line 343), the check out < floor_ can never trigger, so the swap may complete with out = 0. The NatSpec promise that 'every swap must return at least the feed's value less slipBps' (line 57) therefore holds only above the truncation threshold. The value at stake per swap is below $1e-6 and the keeper's swap count is capped at 8 per rolling day per agent, so the economic impact is negligible; recorded as a precision boundary where the stated invariant lapses. Fix: compute the floor with FullMath.mulDivRoundingUp, and/or revert when floor_ == 0 on ORACLE and PEG pools unless minOut > 0.","line":338,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"Hardhat test run against this code. State: ORACLE AAPL(18 dec)/USDG(6 dec) v4 pool, fresh FarmMockFeed at 250e8 with 8 decimals (f = 250e18), slipBps 150; agent 1 holds 1e9 wei of AAPL. Input: keeper calls swap(1, idx, sellAAPL, 1_000_000_000, 0). Arithmetic: _fairOut = mulDiv(1e9, 250e18 * 1e6, 1e18 * 1e18) = floor(2.5e35 / 1e36) = 0; floor_ = 0 * 9850 / 10000 = 0; minOut 0 leaves floor_ = 0. Expected: the swap returns at least 0.985 x fair value (0.246 USDG units, i.e. at least 1 unit with round-up) or reverts TooLittle. Actual: the call succeeds, the agent's USDG balance grows by 0 and its AAPL balance is reduced: the swap returned nothing and no TooLittle was raised.","severity":"info","snippet":"            floor_ = _fairOut(p, zeroForOne, amountIn) * (10000 - p.slipBps) / 10000;","title":"Agent farm: the ORACLE swap floor truncates to zero for dust amountIn, so the slipBps guarantee lapses and a swap may return nothing"},{"citation":"resolved","description":"Both branches of _fee truncate toward the swapper, so the protocol fee is below the nominal 3% of gross USDG by up to one unit per swap, and a swap whose USDG leg is below 34 units (6-decimal USDG: $0.000034) pays no fee at all (fee == 0 returns ZERO_DELTA at line 160 and 0 at line 186). The fee is not compoundable and gas on Robinhood Chain far exceeds the 1-unit saving, so there is no profitable farming path; recorded because fees are conventionally rounded in the protocol's favour and rewards.distribute ultimately pays this USDG to MSFT95 holders. Fix: Math.mulDiv(amount, b, 10_000, Math.Rounding.Ceil) and Math.mulDiv(amount, b, 10_000 - b, Math.Rounding.Ceil), or accept and document that fees round in the swapper's favour.","line":147,"path":"msft95-contracts-audit-v6/v4/MSFT95RewardHook.sol","reproduction":"Arithmetic on the code as written with feeBps = 300. Input: a sell of exact MSFT95 that moves 33 USDG units out of the pool (afterSwap, USDG not the specified currency, amountSpecified < 0): _fee(33, true) = 33 * 300 / 10000 = 0, so the hook returns 0 and the swapper keeps all 33 units; expected under round-up: 1 unit to the hook. Input: an exact-output buy where the pool takes 9701 USDG units: _fee(9701, false) = 9701 * 300 / 9700 = 300 (exact value 300.03); the swapper pays 10001 units of which the hook keeps 300, i.e. 2.9997% instead of 3%.","severity":"info","snippet":"        return gross ? amount * b / 10_000 : amount * b / (10_000 - b);","title":"Reward hook: _fee rounds down in the swapper's favour and is zero for USDG legs below 34 units"}],"hash":"6760a00ab9fe18e017ad303ea75fc7b4eeb04007209c26c381a99aaf57902b57","nodeId":"5440faf6-4b86-4e05-8319-fdf66783669e","outcome":"completed","summary":"The review is complete. The findings file at the repository root holds 9 merged findings, each reproduced against the v6 code.\n\n**What I did**\n\n- Copied the v6 package to a scratch directory outside the repository, installed its pinned dependencies there, compiled both builds, and ran Hardhat reproduction tests for 7 of the specialist claims. The remaining 2 were reproduced by arithmetic and by tracing the launch script.\n- Nothing in the repository was changed except `.imd-findings.json`. No proofs were attached because no finding is high or critical.\n\n**Kept, merged from 15 specialist reports into 9 findings**\n\n- Low: OPEN-pool cap0/cap1 still reset at UTC midnight. Four specialists reported this. The test sold twice the cap one second apart; the in-day OverCap revert held, the cross-midnight one did not.\n- Low: setTreasury changes protocolBps instantly and the cut is applied in `_credit` to fees earned earlier. Two specialists reported this. The test showed the same fee total split at 5% before the change and at 20% right after it, with no delay started.\n- Low: Agents NFT setPayees lets the owner route all mint proceeds to itself. The test ended with the owner holding the full 1 ETH and the vault stand-in holding 0.\n- Low: MSFT95Token setExcluded accepts any address with code, including contract wallets and the staking contract. Both exclusions succeeded in the test.\n- Low: the launch split's economy re-sign path sends a fresh approve first, which either hangs behind the stuck nonce or consumes it. Reproduced by code trace; the rehearsal only faults the liquidity part.\n- Info: NatSpec loss bound is understated. The test confirmed per-token turnover windows let converted value cross a stock pool again in the same window.\n- Info: `_credit` strands 1 wei per credit inside `owed`. Two specialists reported this. After a full exit the farm held 3 USDG units that neither any agent nor skim could reach.\n- Info: ORACLE swap floor truncates to zero for dust input. A 1e9 wei AAPL sale returned 0 USDG with no revert.\n- Info: hook fee rounds down and is zero below 34 USDG units. Confirmed by arithmetic.\n\n**Dropped**\n\n- The vault cosigner-splitting report. The behaviour is real, but docs/AUDIT.md line 19 states it as the accepted trust model: a compromised SIGNER can take up to dailyCap in pieces no larger than cosignAbove. It is a documented design bound, not a defect.","treeHash":null,"usage":{"cachedInputTokens":1626803,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":34691,"runtime":"claude","turns":30,"wallClockMs":469939}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"739094f0e08668ee","findings":[{"citation":"resolved","description":"The contract NatSpec says the admin 'tunes the guards and the protocol's cut of fees' and that 'a new or re-tuned pool only becomes usable after `delay`, so holders see it coming and can freeze the keeper for their agent or leave'. setGuard and listV3/listV4 honour that by setting usableAt = now + delay. setTreasury does not. The new protocolBps takes effect in the same transaction. Fees are only cut when they are credited per agent: _credit (line 476) computes `f0 * protocolBps / 10000` at credit time, on fees that may have built up over weeks. Raising the cut is therefore retroactive: it applies to swap fees earned while the advertised rate was lower. The admin can also batch setTreasury and claimFees (the admin can grant itself KEEPER_ROLE at once), so holders get no window to call claimFees at the old rate. The treasury address, which receives both the cut and every skim(), also changes with no delay. The admin role is a trust assumption, but this is the one fee-affecting parameter that skips the notice period the design promises.","line":411,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"Setup: farm deployed with protocolBps_ = 0 and delay = 2 days. Agent #7 holds liquidity in ORACLE pool 0, and over a month the pool accrues 1,000 USDG of fees for the agent's share, not yet credited. Step 1: the admin calls setTreasury(T, 2000). Step 2: in the same block, the keeper (or an admin-granted keeper) calls claimFees(7, 0). Expected (per the stated design): the cut can't change before `delay` has passed, so the holder has time to claimFees at 0% first, and fees earned at 0% are credited at 0%. Actual: _credit takes c = 1000e6 * 2000 / 10000 = 200 USDG into treasuryOwed[USDG], and balanceOf[7][USDG] rises by only 800 USDG. Fix: queue bps and treasury changes (pendingBps, pendingTreasury, effectiveAt = now + delay) and apply them only after delay. Holders can then lock in the current rate with claimFees beforehand. This keeps the admin's power to set the cut.","severity":"low","snippet":"    function setTreasury(address t, uint16 bps) external onlyRole(DEFAULT_ADMIN_ROLE) {\n        if (!(t != address(0) && bps <= MAX_PROTOCOL_BPS)) revert BadConfig();\n        treasury = t; protocolBps = bps;","title":"AgentFarm: setTreasury raises the protocol cut instantly and applies it to fees agents earned before the change, bypassing the holder-notice delay"},{"citation":"resolved","description":"The contract NatSpec states: 'Mint money follows the tokenomics and nobody can take more than their share: distribute() ... sends 70% into the casino vault's house bankroll, 10% to the team wallet and 20% to the liquidity wallet'. setPayees has no delay. _setPayees only checks that vault_ has code and that team_ and lp_ are non-zero. It does not check that vault_ is the MSFT95Vault, that vault_ credits the bankroll, or that the three payees differ. The owner can therefore point all three legs at addresses it controls, then call distribute() itself (anyone may). That takes the whole mint balance, including the 70% that players are told backs the casino bankroll. Under the design the owner keeps the power to change payees, but the written guarantee is false as the code stands.","line":153,"path":"msft95-contracts-audit-v6/contracts/MSFT95Agents.sol","reproduction":"State: 1,000 Executive agents sold, so address(agents).balance = 100 ETH. The owner deploys `contract Sink { function fundBankroll(address,uint256) external payable {} function pull(address to) external { payable(to).transfer(address(this).balance); } }` and calls setPayees(address(sink), owner, owner). It then calls distribute(). Expected per the NatSpec: 70 ETH reaches MSFT95Vault as BankrollFunded, and only 10 ETH reaches the team. Actual: Sink receives 70 ETH (fundBankroll accepts it with no vault accounting), the owner receives 10 + 20 ETH directly, and Sink.pull hands over the rest, so the owner ends with 100 ETH. Fix options: make casinoVault immutable (or changeable only behind a delay with an event), and/or require IAccessControl(vault_).hasRole(BANKROLL_ROLE, address(this)). Otherwise correct the NatSpec to say the owner can redirect proceeds.","severity":"low","snippet":"    function setPayees(address vault_, address team_, address lp_) external onlyOwner { _setPayees(vault_, team_, lp_); }","title":"MSFT95Agents: the owner can redirect 100% of mint proceeds at once via setPayees, contradicting 'nobody can take more than their share'"},{"citation":"resolved","description":"The NatSpec promises that 'the owner can also exclude other contracts (a router, a presale) but never a wallet', and that the staking contract and games vault 'stay eligible'. The only check is account.code.length != 0. Several kinds of wallet have code: Safe multisigs (including the project's own TREASURY/OPS Safes and any holder's Safe), ERC-4337 smart accounts, and on chains with Pectra, EOAs that set an EIP-7702 delegation (0xef0100 || impl, 23 bytes of code). The owner can stop any of these from earning future MSFT dividends. Nothing stops it excluding the staking contract or the vault either, which redirects their share to every remaining eligible holder, the owner's own holdings included. What was already earned stays claimable, so the loss covers future distributions only.","line":128,"path":"msft95-contracts-audit-v6/v4/MSFT95Token.sol","reproduction":"State: Alice holds 10,000,000 MSFT95 in a Safe (code.length > 0). eligibleSupply is 500M. The owner calls setExcluded(aliceSafe, true). The call succeeds, excluded[aliceSafe] becomes true and eligibleSupply drops to 490M. Then someone calls distribute(1,000 MSFT). Expected per the NatSpec: the call reverts (Alice is a wallet), and Alice earns 1000 * 10M / 500M = 20 MSFT. Actual: Alice earns 0 and the other holders share her 20 MSFT. The same works for MSFT95Staking (setExcluded(staking, true)), after which stakers stop receiving the staked MSFT95's MSFT. Fix: fix the excludable set at deployment (or via an allowlist the owner can only add to after a delay), explicitly forbid excluding the staking contract and vault, or reword the NatSpec to the real guarantee ('any address with code').","severity":"low","snippet":"            if (account.code.length == 0) revert NotAContract(account);","title":"MSFT95Token: the rule that a wallet can't be excluded relies on code.length, so the owner can exclude smart-contract wallets and EIP-7702 delegated EOAs"},{"citation":"resolved","description":"Audit 7 (I-2) moved the per-agent turnover limit and the swap count to rolling 24-hour windows because a calendar reset 'let a keeper fit two days' budget into a few seconds'. The keeper's minimum-out on an OPEN pool (no feed, e.g. MSFT95) is its own choice. The NatSpec names the pool's own daily caps (cap0 / cap1) as the bound on what a compromised or colluding keeper can push through it: 'the bound is one turnover per agent plus the pool's own daily caps (cap0 / cap1), so keep those small'. _useCap still resets at the UTC day boundary, so that pool-wide bound is really 2x cap in any 24 hours, not 1x. The per-agent turnover window doesn't help here, because the cap is shared across all agents and a keeper spreads the sales over many of them.","line":647,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"State: OPEN pool idx 3 (MSFT95/USDG), with cap0 = 100,000 MSFT95 and slipBps irrelevant (OPEN). The keeper has agents 1..20 with 10,000 MSFT95 each and none of them frozen. At 23:59:58 UTC the keeper calls swap(agent_i, 3, true, 10_000e18, 1) for agents 1..10, so used0 reaches 100,000 (= cap0). At 00:00:00 UTC, two seconds later, p.day != today resets used0 to 0, and swap succeeds for agents 11..20. Expected (cap per 24h, as for turnover after audit 7): the second batch reverts with OverCap. Actual: 200,000 MSFT95 is sold at keeper-chosen minOut = 1 within 2 seconds. Fix: use a rolling window like the turnover limit, e.g. a uint64 start that resets used0/used1 when block.timestamp >= start + 1 days, or a token-bucket. Alternatively, document that the real bound is 2x cap0/cap1.","severity":"low","snippet":"        uint64 today = uint64(block.timestamp / 1 days);\n        if (p.day != today) { p.day = today; p.used0 = 0; p.used1 = 0; }","title":"AgentFarm: OPEN-pool sell caps still reset at midnight UTC, so the keeper can sell 2x cap within seconds"}],"hash":"84a962d779bf2112e3387208fdd43db33f51c46c741ec315994945cdc951c751","nodeId":"3b39b00d-e74b-42ef-8731-2bd700cc9dcb","outcome":"completed","summary":"I found no way around any role check in the v6 folder: every function a role is supposed to guard enforces that role on every path. I did find four low-severity gaps where the code lets a privileged role do more than the docs promise. They're in `.imd-findings.json`, and each one's quoted code matches the file at its line.\n\n**Findings (all low):**\n1. **`v4/MSFT95AgentFarm.sol:411` `setTreasury`:** the admin can raise the protocol's cut of fees (up to 20%) or change the treasury address with no waiting period. The docs promise holders `delay` (2 days by default) of notice before any change. The higher cut also hits fees agents earned before the change: 1,000 USDG earned at 0% gets 200 USDG cut if credited right after the change.\n2. **`contracts/MSFT95Agents.sol:153` `setPayees`:** the owner can point all three payout addresses at contracts or wallets it controls, then call `distribute()` and take 100% of mint money. That includes the 70% meant for the casino bankroll, which contradicts the comment \"nobody can take more than their share\".\n3. **`v4/MSFT95Token.sol:128` `setExcluded`:** the docs say a wallet can never be excluded from MSFT dividends. The only check is whether the address has code, so the owner can exclude multisig and smart-account wallets (Safe, ERC-4337) and EIP-7702 wallets, and also the staking contract the docs say \"stays eligible\". Future dividends then go to the other holders.\n4. **`v4/MSFT95AgentFarm.sol:647` `_useCap`:** the sell caps on pools with no price feed still reset at midnight UTC. A keeper can sell twice the cap within two seconds by spreading the sales across agents (100k at 23:59:58, another 100k at 00:00:00). Audit 7 fixed this for the other two keeper limits but not this one.\n\n**What I covered:** every state-changing function, constructor role assignment and callback in the v6 contracts: Vault, Agents, AgentFarm, AgentStakes, Staking, Token, RewardHook, Router, Vesting and Create2Deployer. I also checked who holds each role after `deploy-farm.js` and `scripts/mainnet/lib.js` run.\n\n**What held up:**\n- The Vault keeps signer, cosigner and guardian keys separate, voids every outstanding voucher when a signing key is revoked, and limits withdrawals by nonce and daily cap.\n- The callback checks in the farm, hook and router only accept calls from the right pool or pool manager.\n- Agents' farm money can always be taken out by the holder.\n- The keeper can never reach the agents' locked stakes.\n- Vesting can only ever pay its beneficiary.\n\nThe remaining admin and owner powers (pausing, listing tokens and pools, the conversion route, the price feed) are documented as trust assumptions, so I didn't report them.\n\n**Limits:** no test file is attached. Proofs are only required for critical and high findings, and the v6 folder has no Foundry setup or installed packages to run them against. Each finding instead gives the exact transactions, values, and expected against actual result. I changed no files apart from writing `.imd-findings.json`.","treeHash":null,"usage":{"cachedInputTokens":1253643,"inputTokens":30,"model":"claude-opus-5-5","outputTokens":22074,"runtime":"claude","turns":16,"wallClockMs":255319}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e8e860c7f2306473","findings":[{"citation":"resolved","description":"once() handles a recorded transfer with no receipt by calling build() and then signAndSend(key, req, s.nonce, true) (line 333), i.e. it re-signs the SAME nonce with higher fees so only one copy can ever land. For 'treasury' and 'liquidity' build() is a pure populateTransaction, so this works and the rehearsal (section 10 c) exercises it on 'liquidity'. For 'economy' build() first does `await send(m.approve(...))`: a real transaction from the same deployer account, sent through the hardhat signer with an automatically chosen nonce and awaited with tx.wait() (no timeout). That breaks the same-nonce design in both possible states of the stuck fundBankroll transaction (nonce N):\n\n(1) N is still in the node's mempool but underpriced: the approve gets nonce N+1 and .wait() blocks until N is mined, which is exactly what is not happening. The script hangs inside build() and never reaches the re-sign; killing it and re-running repeats the hang. Neither of the two 'stop for a manual check' messages in MAINNET.md covers this state, so the operator has no documented way forward (they must manually replace nonce N from the deployer key).\n\n(2) N was dropped by the node: the approve takes nonce N itself and is mined. signAndSend then signs fundBankroll with nonce N, broadcast fails ('nonce too low', only logged), receiptOf finds nothing and the run throws 'isn't mined yet: run again'. The next run sees used (N+1) > s.nonce and stops with 'no receipt ... although the deployer's nonce N is used ... check the explorer'. The explorer shows nonce N was the script's own approve, so the operator has to work out that the economy part was never paid and delete split.economy by hand. No double payment occurs in either state (the balance guard and nonce reuse hold), but the resume that audit 7 M-1 introduced does not work for the first and largest part (250,032,300 MSFT95 to the vault), and the rehearsal never faults 'economy'.\n\nFix: do not send the approve inside build() on the re-sign path. Either sign the approve the same way (recorded, explicit nonce) as a separate recorded part before 'economy', or check allowance first and only approve when allowance < amount (an approve mined before the original fundBankroll is still in place), and give the re-sign path a bounded wait instead of tx.wait().","line":349,"path":"msft95-contracts-audit-v6/scripts/mainnet/lib.js","reproduction":"State: deployments/<network>Launch.json has steps.vault done and split.economy = {hash: H, nonce: N, raw: R} where R was broadcast with a maxFeePerGas below what the chain now needs (or the node dropped it). Deployer's latest nonce == N. Run launch.js (resume).\nExpected (per the comments at lines 277-286 and MAINNET.md section 3): the economy transfer is re-signed with nonce N and higher fees and pays once.\nActual: once('economy') -> receiptOf -> null -> rebroadcast R -> still no receipt -> used (N) > N is false -> `await build()` -> `send(m.approve(vault, 250032300e18))`. If R is still pending in the node: approve is queued at nonce N+1 and tx.wait() never returns; the launch hangs with no message. If R was dropped: approve is mined as nonce N; the fundBankroll re-signed with nonce N is rejected by every node; the run ends with 'economy: ... isn't mined yet: run again'; the next run ends with 'no receipt for H, ... although the deployer's nonce N is used', and the vault's bankroll remains unpaid until the operator deletes split.economy by hand. Compare the same scenario with faults.crash = 'liquidity:unsendable' in rehearse.js (section 10 c), which passes because that build() sends nothing.","severity":"low","snippet":"      await once(\"economy\", async () => { await send(m.approve(A().vault, parts.economy[1])); return V.fundBankroll.populateTransaction(A().msft95, parts.economy[1]); });","title":"Launch split: the economy part's re-sign path sends an approve from the deployer first, so a same-nonce re-sign either hangs behind the stuck nonce or can never be mined"},{"citation":"resolved","description":"v6 moved the per-agent turnover window (_useTurnover) and the per-agent swap count (_swapDay) to a rolling 24 hours because, as AUDIT-7.md I-2 puts it, a calendar-day reset lets 'a keeper fit two days' budget into a few seconds'. The third limit in swap(), the OPEN pool's aggregate cap0/cap1 consumed by _useCap, was left on the calendar day (block.timestamp / 1 days). On an OPEN pool there is no feed and no floor: the only protections are the keeper's own minOut (which a compromised keeper sets to 1), the per-agent turnover, and this pool-wide cap, which deploy-farm.js sets to M95_DAY_CAP (default $1,000) on the MSFT95/USDG pool. The cap is therefore the only bound on how much of ALL agents' USDG a stolen keeper key can push through the hooked MSFT95 pool (3% fee to the hook, plus whatever a counterparty takes), and its 'per day' bound is actually 2x per day around every UTC midnight. The NatSpec on cap0 (line 114) says 'per UTC day', so the code matches its own comment, but the README (v6 bullet) and MAINNET.md 5 (lines 110-111, still describing 'what the agent held at the day's first swap of that token') present the farm's limits as no longer resetting at midnight. Fix: give each pool a {start, used0, used1} rolling window like Turnover (reset when block.timestamp >= start + 1 days), and update MAINNET.md line 110-111 to the rolling-window wording.","line":647,"path":"msft95-contracts-audit-v6/v4/MSFT95AgentFarm.sol","reproduction":"Setup as in test-v4/farm.test.js 'buys MSFT95 through an OPEN pool': farm.listV4(m95/usdg key, OPEN, false, 0, false, 0, 0, 0); farm.setGuard(iM, true, 0, 0, 0, 0, cap0 = 1000e6 on the USDG side, 0 on the other); two agents each holding 1,000 USDG; keeper has KEEPER_ROLE.\n1. evm_setNextBlockTimestamp(midnightUTC - 1); farm.swap(agent1, iM, usdIs0, 1000e6, 1) -> succeeds, used = 1000e6 = cap0.\n2. same timestamp: farm.swap(agent2, iM, usdIs0, 1e6, 1) -> reverts OverCap (cap holds).\n3. evm_setNextBlockTimestamp(midnightUTC + 1); farm.swap(agent2, iM, usdIs0, 1000e6, 1) -> succeeds: $2,000 of agents' USDG sold through the OPEN pool 2 seconds apart.\nExpected (the bound the v6 README and AUDIT-7 I-2 describe for the farm's limits): at most cap0 = $1,000 in any 24 hours, so step 3 reverts OverCap until midnightUTC - 1 + 86400.\nActual: step 3 passes because p.day != today resets used0 to 0. The corresponding per-agent check in farm.attack.test.js (line 86-88, 'crossing midnight UTC doesn't give it a fresh budget') has no counterpart for cap0/cap1.","severity":"low","snippet":"        uint64 today = uint64(block.timestamp / 1 days);\n        if (p.day != today) { p.day = today; p.used0 = 0; p.used1 = 0; }","title":"OPEN-pool daily caps (cap0/cap1) still reset at midnight UTC, so a keeper can spend two days' cap within seconds; the rolling-window fix of audit 7 I-2 did not reach _useCap"}],"hash":"eca3d4de54a6edece317761f2cbf578c551d6e475dcfa10a50461ef0b740fb76","nodeId":"19ce9c7e-8c61-4685-ae9c-b7b36bfc9422","outcome":"completed","summary":"Both snippets match the tree and the JSON parses. The review is complete.\n\n**Outcome:** two low-severity findings, no critical or high, written to `.imd-findings.json`. No on-chain defect with a concrete failing input was found in the ten in-scope contracts under the Execution Trace, Periphery and First Principles passes.\n\n**Findings**\n\n- **Launch split re-sign path for the economy part** (`scripts/mainnet/lib.js:349`). The same-nonce re-sign that audit 7 M-1 introduced calls `build()` first, and for the economy part that sends a real `approve` from the deployer with an auto nonce and an unbounded `tx.wait()`. If the stuck transaction is still pending, the launch hangs with no message. If it was dropped, the approve consumes the saved nonce, so the re-signed transfer can never land and the run ends in the manual-check stop with a misleading cause. No double payment either way, but the resume does not work for the largest part, and the rehearsal only faults the liquidity part.\n- **OPEN-pool caps still reset at midnight UTC** (`v4/MSFT95AgentFarm.sol:647`). The per-agent turnover and swap count moved to a rolling 24 hours, but `_useCap` kept the calendar day. A keeper can spend the full pool cap at 23:59:59 and again at 00:00:01. On an OPEN pool this cap is the only aggregate bound against a stolen keeper key, so its real limit is double the documented one around midnight. MAINNET.md section 5 also still describes the old per-day wording.\n\n**What was checked and how**\n\n- All ten in-scope contracts, the test helpers, Create2Deployer, the generator and its data, the deploy and launch scripts, the voucher scripts, the docs and the v5 to v6 diff.\n- Verified by script that the three agent tier lists are an exact permutation of 0..3332 and match `agent-tiers.json`, that the generated file matches the template, and that the README SHA-256 hashes match the tree.\n- Traced the farm's `owed` conservation across deposit, withdraw, swap, add, remove, collect, credit and treasury paths, the v3 and v4 callback settlement, the rolling turnover window at its 24-hour edge, the hook's before and after swap fee deltas, and the router's quote decoding. None broke.\n\n**Limitations**\n\n- The Hardhat suites were not run: the package ships without `node_modules`, and installing them is outside a read-only review. No static analyzer ran.\n- No Foundry proof was written since no finding reached high or critical severity, and the repository has no Foundry configuration to compile against.","treeHash":null,"usage":{"cachedInputTokens":1469842,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":45836,"runtime":"claude","turns":36,"wallClockMs":689914}}],"verification":[]}