{"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":"2c8d4004-f484-4fd9-a842-f81f56f738eb","kind":"audit","nodes":[{"acceptedSubmissionHash":"265f6299b649c5c7dca374b8ee03346c593a706da64503cb9ce7e28d957a3abe","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":"7133c79a8c84ebd4989e35aa0aadc1c824b90f0db8247567fe0a545e976336c5","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":"80d8246c5a7c94f0f6dcd1ba26ca71df91dc827a63bd9f109fd495aa077b3acb","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":"0fa63f48101c4fbb8ee4c2aab084c42f1c027139222b90ded742ede19881eb08","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":"9d3fc1a42485299eda4926c8deec7fc2b6539b711328df1b5008163561d25d58","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":"Audit only the msft95-contracts-audit-v5/ folder. These are MSFT95’s Robinhood Chain mainnet contracts after six automated review rounds; docs/AUDIT-6.md lists the latest fixes. Look hardest at:\n(1) scripts/mainnet/lib.js split, which records transfers by transaction hash. Find any stop-and-resume point where a part is paid twice or skipped, including when wallets are shared or kept by the deployer. Also check the vesting replacement and how much it carries over.\n(2) The overflow guard in MSFT95Staking.sync. Can sync or _accrue still overflow, or can the guard be used to hold back a legitimate reward?\n(3) The full-fill check in MSFT95RewardHook.afterSwap, for exact-in and exact-out swaps in both token orders. Can it refuse a legitimate full fill?\n(4) MSFT95AgentFarm.skim. Can it ever take tokens owed to agents or the treasury, including during callbacks, fee collection or removals?\n(5) The farm’s daily turnover limit (_useTurnover). Can the keeper get around it, or can it block a holder’s exit?\n(6) Re-check earlier fixes: the openTrading position gate, the vault’s separate house sweep window, MSFT being sweep-only in the vault, and that a compromised keeper can only cause bounded loss.\nGive concrete reproductions, and don’t modify or deploy anything.","parentJobId":null,"planHash":"ec893138eddb249d133847e887a0c44ef9cb3dbc0256752886e36117ea9faf92","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"2c8d4004-f484-4fd9-a842-f81f56f738eb","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":"52337","feedbackHash":"246360bd9dfacbdc9d531528a5b0278d7221f866670e47950bd4c3e833004075","nodeKey":"audit_economics","submissionHash":"265f6299b649c5c7dca374b8ee03346c593a706da64503cb9ce7e28d957a3abe","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51199","feedbackHash":"98b65412ce29be5f16220f0914640f03b6d5065fbee54bfd314701c7667a427c","nodeKey":"audit_flow","submissionHash":"7133c79a8c84ebd4989e35aa0aadc1c824b90f0db8247567fe0a545e976336c5","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52082","feedbackHash":"867c324e0c42a98799b03f0dfdeb943576b362cd1e55500434e9c4ddcff8dd1c","nodeKey":"audit_judge","submissionHash":"80d8246c5a7c94f0f6dcd1ba26ca71df91dc827a63bd9f109fd495aa077b3acb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52685","feedbackHash":"d22d476878813194a19e6e98f0dbeba6ece857ac37fec9917f19c7a55d9e7af1","nodeKey":"audit_math","submissionHash":"0fa63f48101c4fbb8ee4c2aab084c42f1c027139222b90ded742ede19881eb08","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51483","feedbackHash":"01ad22b0f810419a8691ed86e0a8e295b5498382a85fd5592029f04c39908a72","nodeKey":"audit_permissions","submissionHash":"9d3fc1a42485299eda4926c8deec7fc2b6539b711328df1b5008163561d25d58","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"7398af78ebf1f79d1527f5254eb8e700ba0cebf6f70128029310d138f233ae10","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"776c025db24644e8","findings":[{"citation":"resolved","description":"The audit-6 fix records each split transfer by transaction hash, but the hash is only written to the record after `fn()` resolves, i.e. after eth_sendRawTransaction has returned. The transaction is signed with whatever nonce the node reports at that moment and nothing about it (nonce, hash) is persisted before the broadcast. Two realistic failure modes therefore re-pay a part on resume: (a) the RPC request times out or the process is killed between the broadcast and `save()` although the sequencer accepted the transaction; (b) the record holds the hash, but on resume `getTransactionReceipt` and `getTransaction` both return null because the RPC endpoint (a load balancer over several nodes, or a node that has not yet seen the sequencer feed) does not know the transaction yet, so `once` logs 'was dropped' and sends again. In both cases the second send asks the node for a fresh nonce; if the first transaction has by then been mined (or is known to the node for nonce purposes) the second gets nonce+1 and both are mined. For `treasury`/`liquidity` the Safe receives 250,000,000 or 200,000,000 twice; for `economy` 250,032,300 goes into the vault bankroll twice. The post-check `left < expected` only detects this afterwards and throws; the tokens have already moved, and getting 250M MSFT95 back out of the vault needs the Safe to raise the MSFT95 dailyCap (100,000/day by default) and sweep. The rehearsal (section 10) only tests a resume with the record intact and all receipts present, so neither path is exercised. Fix: make the record authoritative before anything leaves the key: pin the nonce (read `getTransactionCount(me,'latest')` once, store it per key), sign the transaction locally (`signer.signTransaction(populated)`), compute `keccak256(raw)` as the hash, `save()` both, then `provider.broadcastTransaction(raw)`. On resume a recorded nonce that is already below `getTransactionCount(me,'latest')` means the part was paid (look the hash up), and a re-send with the same pinned nonce can never be mined twice.","line":278,"path":"msft95-contracts-audit-v5/scripts/mainnet/lib.js","reproduction":"State: mainnet launch, TREASURY and LP_WALLET unset (both default to SAFE). Run `split`: `economy` succeeds and is recorded. For `treasury`, `m.transfer(SAFE, 250000000e18)` is broadcast; the RPC call times out client-side (or the process is killed) after the sequencer has accepted it, so `fn()` rejects/never returns and `sent.treasury` is never written. Re-run the launch: `rec.steps.split` is unset, `sent.economy` has a successful receipt and is skipped, `sent.treasury` is undefined, so `m.transfer(SAFE, 250000000e18)` is sent again with nonce+1 and mined. Expected: the Safe holds 450,000,000 after the split and the deployer holds exactly AGENT_STAKE. Actual: the Safe holds 700,000,000, the deployer holds 249,967,700 - 250,000,000 < 0 => the `liquidity` transfer reverts (insufficient balance) or, if ordering differs, the final `left < expected` check throws 'after the split the deployer holds ... less than ...'. The same happens through the 'was dropped' branch: with `sent.treasury` recorded but `getTransactionReceipt(h)` and `getTransaction(h)` both null on a load-balanced RPC that has not yet indexed the transaction, `once` logs 'was dropped: sending again' and the transfer is repeated.","severity":"low","snippet":"        const tx = await fn();\n        sent[key] = tx.hash; save();","title":"split: a part is paid twice on resume when the send is accepted by the chain but its acknowledgement is lost (hash recorded only after the RPC answers)"},{"citation":"resolved","description":"`_useTurnover` is keyed per (agent, token) and each token's budget is the balance at its first swap of the UTC day. The keeper cannot exceed one turnover per token, and holder exits (`withdraw`, `holderRemove`) never pass through it, so the limit works as intended for a USDG <-> stock round trip. But the documented bound ('a round trip USDG -> stock -> USDG now costs an agent at most about 2 x slipBps of its cash per day', AUDIT-6.md item 7 and the contract header) only holds when two tokens are involved. Every listed token has its own budget, so a sandwiching keeper can chain one leg per distinct token: with a PEG pool (deploy-farm.js lists PEG_POOLS with slipBps 30) and a stock listed against both dollar tokens, USDG -> USDC -> AAPL -> USDG is three legs (each sandwiched to the guard's edge) and a second stable or stock adds a fourth, up to SWAPS_PER_DAY = 8. In addition `today = block.timestamp / 1 days` resets every budget at 00:00 UTC, so legs at 23:59:59 and 00:00:01 use two days' budgets within seconds, and `remove` before the first swap puts the whole LP position (not only cash) into the base. None of this breaks the stated invariant that the keeper cannot move money out of the agent, and the holder can freeze the keeper, but the operator-facing bound should read 'about (distinct tokens swapped) x slip per UTC day, two budgets around midnight', and the deployer should keep PEG pools out of the list (or set their slip to 0) if the 2 x slip figure is relied upon.","line":432,"path":"msft95-contracts-audit-v5/v4/MSFT95AgentFarm.sol","reproduction":"Setup (as in farm.attack.test.js base()): ORACLE pool AAPL/USDG with slipBps 150, ORACLE pool AAPL/USDC with slipBps 150, PEG pool USDG/USDC with slipBps 30 (farm.listV3/V4 with Guard.PEG as deploy-farm.js does), agent 1 holds 10,000 USDG, keeper is the pools' LP and sandwiches each leg. Day D: swap(1, pegIdx, USDG->USDC, 10,000e6) sets turnover[1][USDG] = {base 10,000e6, used 10,000e6}; swap(1, aaplUsdcIdx, USDC->AAPL, ~9,970e6) sets turnover[1][USDC] used = base; swap(1, aaplUsdgIdx, AAPL->USDG, all AAPL) sets turnover[1][AAPL] used = base. Expected per AUDIT-6: at most 2 legs / about 2 x slip. Actual: 3 legs succeed (a 4th swap of any of the three tokens reverts OverCap), total loss about 0.3% + 1.5% + 1.5% = 3.3% instead of 3.0%; with evm time set to 23:59:50 UTC before the first leg and the same three legs repeated after 00:00:00 UTC, 6 legs succeed within a minute and the loss is about 6.6% of the agent's cash.","severity":"info","snippet":"        if (t.day != today) { t.day = today; t.base = balanceOf[agentId][token]; t.used = 0; }","title":"Farm turnover limit bounds a day's loss at (number of distinct listed tokens) x slip, not 2 x slip, and two full budgets fit in a few seconds across the UTC day boundary"},{"citation":"resolved","description":"The audit-6 L-2 fix replaces a vesting contract that started before it was fully funded. The replacement deploys a fresh schedule with `start = now + VEST_DELAY`, then sends an approve and a `fund()` in two further transactions. `MSFT95Vesting.fund` reverts with `AfterStart` once `block.timestamp >= start`. `config()` accepts any VEST_DELAY, including 0 or a few seconds, and the vesting step has no check that `start` is still in the future when `fund()` is sent. With a too-small value the step fails, and each resume pushes the previous (empty) contract into `addrs.vestingStale` and deploys another one, forever; nothing is lost (the 50,000,000 stay with the deployer) but the launch cannot pass this step and leaves a growing list of empty vesting contracts on chain. Fix: reject VEST_DELAY below a sane floor (e.g. 10 minutes) in `config()`, and before `fund()` re-read `start` and compare to the latest block timestamp with a margin.","line":62,"path":"msft95-contracts-audit-v5/scripts/mainnet/lib.js","reproduction":"env VEST_DELAY=0 (or 1). Run the launch to the `vesting` step. Expected: 50,000,000 MSFT95 funded into one vesting contract. Actual: `deploy(MSFT95Vesting, ..., start = now, ...)` succeeds, `fund(50000000e18)` reverts AfterStart because the funding block's timestamp >= start, the step throws; on every resume `t0 + have < amt && now >= start` is true, the empty contract is pushed to `vestingStale`, a new one is deployed with start = now, and `fund` reverts again. `addrs.vestingStale` grows by one address per attempt and the step never records.","severity":"info","snippet":"    VEST_DELAY: Number(env.VEST_DELAY || 3600), VEST_DAYS: Number(env.VEST_DAYS || 365),","title":"Launch: VEST_DELAY is not validated, and a delay shorter than the deploy->approve->fund sequence makes the vesting step replace the schedule on every resume without ever funding it"},{"citation":"resolved","description":"config() runs addr() (isAddress + checksum) on SAFE, HOOK_KEEPER, STAKE_KEEPER, SIGNER, COSIGNER, GUARDIAN and MSFT_FEED so that a bad setting fails before any transaction (audit 4, L-4). The four wallet overrides that receive supply are taken verbatim: OPS becomes the vesting beneficiary (constructor arg, step `vesting`), TREASURY and LP_WALLET receive 250,000,000 and 200,000,000 in `split`, TEAM and LP_WALLET go into the MSFT95Agents constructor. A value that is not an address (a typo, a trailing space, an ENS-style name) is only rejected by ethers when that step runs, after the token, hook, pool, router and staking contracts exist and the deployer still holds the whole supply and ownership: exactly the half-way stop the audit-4 fix was meant to rule out. It is recoverable (fix the env and resume) and funds are not lost, but the deployer key holds 1,000,000,000 MSFT95 longer than intended. The deployer-kept case (`TREASURY` or `LP_WALLET` equal to the deployer) is handled correctly by `mine()`/`expected`. Fix: in config(), run `addr(k, SAFE)` for OPS, TREASURY, LP_WALLET and TEAM when not dry, so they are validated and checksummed before the first transaction.","line":57,"path":"msft95-contracts-audit-v5/scripts/mainnet/lib.js","reproduction":"env: SAFE, keepers, signer keys and MSFT_FEED valid; TREASURY='0x5fc5360D0400a0Fd4f2af552ADD042D716F1d16' (one hex digit short). Expected (per the audit-4 L-4 comment at lib.js:46): config() throws before any transaction. Actual: config() returns, steps chain, token, hook, pool, route, feed, router, staking, vesting and vault all execute (about a dozen transactions, the token minted to the deployer), and `split` throws 'invalid address' from ethers at `m.transfer(parts.treasury[0], ...)` after `economy` has moved 250,032,300 into the vault. The run stops with the deployer holding 699,967,700 MSFT95 and the token/hook ownership until the operator corrects the env and resumes.","severity":"info","snippet":"    OPS: env.OPS || SAFE, TREASURY: env.TREASURY || SAFE, LP_WALLET: env.LP_WALLET || SAFE, TEAM: env.TEAM || SAFE,","title":"Launch pre-flight does not validate TREASURY, LP_WALLET, OPS or TEAM, so a malformed value stops the launch half way (at vesting or split) despite the audit-4 L-4 'check everything before the first tr"}],"hash":"0fa63f48101c4fbb8ee4c2aab084c42f1c027139222b90ded742ede19881eb08","nodeId":"af470c1d-be70-4d65-9c12-297c8ccaa87d","outcome":"completed","summary":"Review complete. The findings file at the repository root holds 4 substantiated findings, all snippets verified against the tree. No contract or script was modified and nothing was deployed.\n\n**Outcome by focus area**\n\n- **Split (lib.js)**: the hash-based resume is correct when the record is intact, but the hash is persisted only after the RPC acknowledges the send and the nonce is never pinned. A lost acknowledgement or an RPC that does not yet see the transaction re-sends the part, and both get mined. Reported as Low with the exact sequence and a fix (sign locally, record nonce and hash, then broadcast). Shared wallets and deployer-kept parts are handled correctly. The vesting replacement carries over exactly the stale contract's `total()`, so the 50,000,000 holds, but an unvalidated short `VEST_DELAY` makes the step replace the schedule forever without funding (Info). The pre-flight check also skips TREASURY, LP_WALLET, OPS and TEAM, so a typo stops the launch mid-way (Info).\n- **Staking sync guard**: no overflow path found. `rate * remaining` is bounded by the admitted `total`, the `rate += d / rest` branch keeps `rate * rest <= left + d`, and the guard can only fire for a token reporting more than about 1e41 units, so it cannot hold back a real reward.\n- **Hook afterSwap full-fill check**: correct for exact-in and exact-out in both token orders. The pool's specified delta equals the requested amount whenever the swap does not hit the price limit, including the sub-wei fee edge in v4's SwapMath, so a legitimate full fill is never refused.\n- **Farm skim**: `owed` is never understated at rest, and every window where balance temporarily exceeds `owed` sits inside a `nonReentrant` call that `skim` shares, including v3 callbacks and v4 unlock callbacks. Rounding leaves dust in `owed`, which skim cannot take.\n- **Turnover limit**: no bypass of the per-token rule, and holder exits never pass through it. The documented \"2 x slip a day\" is only true for two tokens. With PEG pools (which deploy-farm.js lists) each distinct token adds a leg, and the UTC day boundary allows two budgets within seconds (Info, with a 3-leg and 6-leg reproduction).\n- **Earlier fixes**: the openTrading position gate, the separate house sweep window, MSFT as sweep-only (maxWithdrawal 0 with NO_COSIGN) and the keeper loss bounds (hook: feed floor; stakes: unlocked only; farm: slip, caps, turnover) all hold as described.\n\nNo Critical or High findings, so no Foundry proof files were required and none were written. Tool limitation: the project has no foundry.toml or installed dependencies, and I ran no Hardhat or Foundry tests; all conclusions come from source review against the Uniswap v4 core semantics.","treeHash":null,"usage":{"cachedInputTokens":1013250,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":52048,"runtime":"claude","turns":20,"wallClockMs":837606}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"da7ed792c3bf5b80","findings":[{"citation":"unresolved","description":"In split()'s resumable `once(key, fn)` helper the transaction is submitted by `await fn()` (its hash is known and it is already in the mempool) BEFORE `sent[key] = tx.hash; save()` persists the record. If the deployer process is killed in that window and the transaction is later mined, the next resume finds `sent[key]` unset and re-runs `fn`, submitting the same economy/treasury/liquidity transfer a second time. This is the exact 'a part is paid twice' class the audit-6 M-1 fix set out to remove; the fix hardened the common case (hash recorded, receipt checked on resume) but the record-after-submit ordering leaves this narrow redo window. Impact is bounded and non-silent: a double economy fundBankroll or double treasury/liquidity transfer leaves the deployer short of `expected`, so the post-split balance check at line 290 throws ('the deployer holds ... less than it should') and the launch halts rather than losing funds silently. It is a deploy-time JS script (not deployed Solidity), so it carries no Foundry proof and is reported as informational.","line":278,"path":"scripts/mainnet/lib.js","reproduction":"State: launch resumed where the economy fundBankroll tx was submitted on the prior run (line 278) but the process died before line 279 persisted its hash, and the tx subsequently mined with status 1. On resume sent['economy'] is undefined, so once('economy', ...) re-approves and re-calls V.fundBankroll(msft95, 250_032_300e18). Expected: the already-paid economy part is detected and skipped. Actual: fundBankroll is sent again; the deployer's balance falls below `expected` (= AGENT_STAKE when TREASURY/LP_WALLET are the Safe) and line 290 reverts, stopping the launch. No concrete on-chain exploit by an external party; a resume-robustness gap caught by the balance guard.","severity":"info","snippet":"        const tx = await fn();\n        sent[key] = tx.hash; save();\n        const r = await tx.wait();","title":"Launch split records a transfer's tx hash only after submission, so a crash in the submit→save window re-sends it on resume"}],"hash":"265f6299b649c5c7dca374b8ee03346c593a706da64503cb9ce7e28d957a3abe","nodeId":"076351a8-8693-4486-98e4-fd828ffc7599","outcome":"completed","summary":"The findings file is valid and the snippet matches the source exactly (lines 278–280).\n\n## Summary\n\nI audited the `msft95-contracts-audit-v5/` folder in depth against the economic-security, invariant, and flow-gap checklists, covering all six assigned areas before anything outside them:\n\n**(1) Launch split (`scripts/mainnet/lib.js`)** — The audit-6 M-1 rewrite (record each transfer by tx hash, skip succeeded/wait-pending/resend-reverted on resume, and verify the deployer's residual balance) is sound. Several parts going to the same wallet (TREASURY/LP_WALLET both = Safe) no longer cause a skip because balances aren't consulted. Vesting replacement correctly carries over the stale contract's `total()` into `have` so the funded total stays exactly 50M. I did find one concrete **informational** resume gap: the tx hash is persisted *after* submission (lines 278–279), so a crash in that narrow window re-sends a transfer on resume — but it is caught by the post-split balance guard (line 290 reverts) rather than silently double-paying, and it's a deploy-time JS script.\n\n**(2) `MSFT95Staking.sync`/`_accrue` overflow guard** — `_room(left,back,d)/ACC` correctly bounds `fresh*ACC + back + d + left` below 2²⁵⁶; `rate*(end−last)` cannot overflow because `rate=total/dur` is self-consistent with the clamped stream. The guard only triggers for pathological tokens (supply ≈10⁴¹), never holds back a legitimate reward.\n\n**(3) `MSFT95RewardHook.afterSwap` full-fill check** — Verified all four cases (exact-in/out × USDG-specified both orders): a true full fill yields `amt` exactly equal to `named−f` / `named+f`, so `PartialFill` cannot refuse a legitimate full fill.\n\n**(4) `MSFT95AgentFarm.skim`** — `owed[token]` is ≥ agent balances + `treasuryOwed` + collected-uncredited fees at all times (the 1-wei credit buffer makes it over-count, not under-count); skim is `nonReentrant` and can't be re-entered via pool callbacks. It cannot take agent/treasury funds.\n\n**(5) `_useTurnover`** — Per-(agent,token,UTC-day) base set at first swap caps a round trip at ~2×slip; deposits mid-day don't raise the base; it's not applied to `withdraw`/`holderRemove`, so it can't block a holder's exit, and the keeper has no bypass.\n\n**(6) Earlier fixes** — `openTrading` position gate (full-range, ≥minLiquidity, launch price), the vault's separate `houseWindow` sweep window, MSFT listed sweep-only (`maxWithdrawal 0`, `NO_COSIGN`), and the bounded-loss keeper model all hold.\n\nThe contracts are heavily hardened across six rounds; I could not substantiate an exploitable on-chain defect in any assigned area. The findings file records the review honestly with the one concrete observation.","treeHash":null,"usage":{"cachedInputTokens":337638,"inputTokens":32,"model":"claude-opus-5-5","outputTokens":11562,"runtime":"claude","turns":25,"wallClockMs":847153}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4fcac33561ba733d","findings":[{"citation":"resolved","description":"`once(key, fn)` writes the transaction hash to the record only after `fn()` resolves, i.e. after ethers' eth_sendRawTransaction call has returned a response. If the node accepts the signed transaction but the response is lost (RPC timeout, connection reset, the operator pressing Ctrl-C while the call hangs, the process being killed), `fn()` throws or never resolves, nothing is recorded, and the transaction still mines. On resume `sent[key]` is empty, so `once` builds a brand-new transaction (with the next nonce) and pays the same part a second time. The same thing happens when a recorded hash is reported as unknown by the RPC (lines 274-276, `getTransaction(h)` returns null for a transaction the sequencer has accepted but this node has not indexed yet): it is treated as dropped and a new transaction with a new nonce is sent, and both can land. Transactions are identified by hash only, never by sender nonce, so a nonce that the chain has already used is not noticed. The post-split balance check (line 290) only reports this after the duplicate has been paid. AUDIT-6 M-1 says the fix means \"nothing is paid twice\", but that only covers stops after the hash is saved.\n\nImpact: with the default TREASURY = LP_WALLET = SAFE, a duplicated treasury transfer sends 500,000,000 MSFT95 to the Safe. The 200,000,000 liquidity transfer then reverts (the deployer holds 199,967,700), and every later resume stops at the same point. The launch cannot finish (no liquidity part, and the agents' stake is short) until the Safe signs a transfer back by hand. A duplicated `economy` part sends a second 250,032,300 to the vault. The house can then take it out only through `sweep`, which is capped at the MSFT95 dailyCap of 100,000 a day unless the timelocked admin raises it.\n\nFix: sign before broadcasting. Use `populateTransaction`, fix the nonce, call `signer.signTransaction`, compute `keccak256(raw)`, and save the hash, nonce and raw transaction. Only then broadcast. On resume, if there is no receipt, re-broadcast the same raw transaction rather than building a new one. If the account's mined nonce is already past the recorded nonce and there is still no receipt for the hash, stop and ask for a manual check. Do not send again.","line":278,"path":"msft95-contracts-audit-v5/scripts/mainnet/lib.js","reproduction":"State: steps chain/token/staking/vault done, vesting funded (deployer holds 950,000,000 MSFT95), SAFE set, TREASURY and LP_WALLET unset (both = SAFE). Run 1: economy is paid and recorded. For the treasury transfer, `m.transfer(SAFE, 250M)` reaches the node but the eth_sendRawTransaction response times out, so ethers throws, the script exits, and rec.split has only {economy}. The transfer mines. Run 2 (resume): `once(\"treasury\")` finds no record and sends 250M again, which succeeds. The Safe now holds 500,000,000 and the deployer 199,967,700. `once(\"liquidity\")` then reverts with \"ERC20: transfer amount exceeds balance\". Run 3 and later: same revert, so the launch is stuck. Expected: a resumed split pays each part once (Safe 450,000,000, deployer 249,967,700). Actual: the Safe has 500,000,000, the deployer 199,967,700, and the step can never complete. Reproduced by driving the unmodified lib.js split() with an in-memory provider whose treasury broadcast is accepted and then throws. Output: `run 1: stopped: timeout ... | Safe 250,000,000 | deployer 449,967,700 | record {economy}` / `run 2 (resume): stopped: ERC20: transfer amount exceeds balance | Safe 500,000,000 | vault 250,032,300 | deployer 199,967,700` / run 3: identical.","severity":"medium","snippet":"        const tx = await fn();\n        sent[key] = tx.hash; save();","title":"split() records a transfer only after its broadcast returns: a lost RPC response or Ctrl-C mid-broadcast makes the resume pay the part again"},{"citation":"resolved","description":"AUDIT-6 (M-1) says: \"After the split, the deployer must hold exactly the agents' stake (plus any part it keeps itself), or the step stops with the numbers.\" The code throws only when `left < expected`. When `left > expected`, which is exactly the symptom of M-1 (a part was skipped and the deployer kept it), it logs a hint that blames an outside sender, and `step()` then records `split` as done. The next steps fund the agents' stake from that balance, so the skipped part stays with the deployer key for good. Parts can be skipped because `once` trusts any successful receipt stored under `rec.split[key]`. It never checks that the receipt is a Transfer of A().msft95 to the intended wallet for the intended amount. Also, `rec.split` is a top-level key, outside `rec.steps`, so it survives when steps are re-run (for example, deleting `steps.token`…`steps.split` to redeploy a token with corrected settings keeps the old hashes).","line":291,"path":"msft95-contracts-audit-v5/scripts/mainnet/lib.js","reproduction":"State: deployments/rhMainnetLaunch.json holds `split: {economy: h1, treasury: h2, liquidity: h3}`, where h1..h3 are successful transactions from an earlier attempt, for example against a token that was later redeployed after the operator removed `steps.token`…`steps.split` but kept the file. Run `STEPS=split` (or the full launch). All three parts log \"sent before\" and no transfer is made. `left` = 950,000,000 against `expected` = 249,967,700. Only the \"!! … move it by hand\" line prints, and `rec.steps.split` is saved as done. The agents step then takes 249,967,700, and the deployer hot key keeps 700,032,300 MSFT95 (economy + treasury + liquidity). Expected per AUDIT-6: the step stops. Fix: throw when `left !== expected`, and in `once` check the receipt's Transfer log (token, to, value) before accepting it as 'sent before'.","severity":"low","snippet":"      if (left > expected) log(`    !! the deployer holds ${ethers.formatEther(left - expected)} MSFT95 more than it should (sent to it from outside the launch): move it by hand`);","title":"Post-split check stops only when the deployer holds too little; a skipped part (deployer holds too much) is logged and the step is marked done"},{"citation":"resolved","description":"`start` is the latest block's timestamp plus VEST_DELAY, and `MSFT95Vesting.fund` reverts with `AfterStart` when `block.timestamp >= start`. The deploy, approve and fund transactions all land in later blocks, so with `VEST_DELAY=0` (accepted by config, since `Number(\"0\")` is 0), or any delay shorter than the time those three confirmations take, `fund` always reverts. On every resume, the audit-6 L-2 replacement branch sees an unfunded, started contract, pushes it to `vestingStale` and deploys another one, which fails the same way. The step can never finish, and each attempt leaves another empty vesting contract on record. Nothing is lost, because `have` counts the stale totals (0), but the launch is blocked at `staking` until the operator works out why. Fix: require `VEST_DELAY` to exceed a safe minimum (for example 10 minutes) in `config()`, or set `start` from the timestamp of the block the deployment mined in plus a margin, and check `now < start` before approving.","line":217,"path":"msft95-contracts-audit-v5/scripts/mainnet/lib.js","reproduction":"VEST_DELAY=0, all other settings valid. Run 1: vesting deployed with start = T (latest timestamp). approve mined at T+1, then fund() at T+2 reverts with AfterStart and the script exits. Run 2: A().vesting has total 0 and now >= start, so it is moved to vestingStale and a new one is deployed with start = T'. fund() mines after T' and reverts with AfterStart again. Run N: same. Expected: one funded 50,000,000 schedule. Actual: vestingStale grows by one on each run and the step never completes.","severity":"low","snippet":"        const start = (await now()) + cfg.VEST_DELAY;","title":"Vesting replacement loops forever when VEST_DELAY is 0 (or shorter than deploy-to-fund latency): every run deploys a new schedule whose fund() reverts AfterStart"},{"citation":"resolved","description":"`_useTurnover` (and `_swapDay` at line 324) reset at `block.timestamp / 1 days`, not over a rolling 24h. A keeper can therefore use a full day's turnover at 23:59:59 UTC and a second full turnover one second later. That is up to 2 round trips, about 4 × slipBps of the agent's holdings, within seconds, against the documented \"at most about 2 × slipBps a day\". The base is also the cash balance at the moment of the first swap, and the keeper alone chooses that moment. It can first call `remove` to turn the agent's whole farm position into cash, so the budget covers the agent's entire holdings, not just 'its cash'. Neither path blocks a holder's exit: `withdraw` and `holderRemove` never touch the counters. This is a looser bound than documented, not a bypass. The farm doc block (lines 60-64) should state the per-calendar-day and full-holdings semantics, or the counter should use a rolling window as the vault's `_spend` does, if 2 × slip per 24h is meant as a hard bound.","line":432,"path":"msft95-contracts-audit-v5/v4/MSFT95AgentFarm.sol","reproduction":"Agent 7 holds 10,000 USDG cash plus a farm position in an ORACLE pool with slipBps = 150. At t = 86400·D − 5 the keeper calls remove(7, idx, allShares) and then sandwiches swap(7, idx, usdg→stock, all USDG) and swap(7, idx, stock→usdg, all stock) at the floor. Both pass, with base equal to the post-remove balances. At t = 86400·D the keeper repeats the round trip, and both swaps pass again because turnover[7][token].day changed. The agent loses about 4 × 1.5% in under 10 seconds. Expected per the doc: at most about 2 × slipBps in a day.","severity":"info","snippet":"        if (t.day != today) { t.day = today; t.base = balanceOf[agentId][token]; t.used = 0; }","title":"Farm turnover and swap-count limits reset on the UTC day boundary, and the turnover base is snapshotted after the keeper can pull the agent's liquidity into cash"}],"hash":"7133c79a8c84ebd4989e35aa0aadc1c824b90f0db8247567fe0a545e976336c5","nodeId":"2d64c15b-5382-4ac6-b6eb-69308285348f","outcome":"completed","summary":"I found four defects and wrote them to `.imd-findings.json`: one medium, two low, one info. All four snippets were checked against the tree. None is critical or high, so no Foundry proofs were needed. I changed no repository files; the reproduction harness lived in /tmp and has been deleted.\n\n**What I found**\n\n1. **Medium: `split` can pay a part twice** (`scripts/mainnet/lib.js:278`). A transfer's hash is saved only after the broadcast call returns. Sometimes the node accepts the transaction but the response is lost: a timeout, Ctrl-C, or the process being killed. The transfer still goes through, but nothing is recorded, so the resume sends it again with a new nonce. The \"dropped\" branch at lines 274–276 can do the same.\n   - **Reproduced** by running the unmodified `lib.js` against a fake in-memory chain. With TREASURY and LP_WALLET defaulting to the Safe, the Safe ends up with 500,000,000 MSFT95 instead of 250,000,000. The deployer is left with 199,967,700, the 200M liquidity transfer reverts on every later resume, and the launch is stuck until the Safe sends the extra back by hand.\n   - **Fix:** sign the transaction first and save its hash, nonce and raw bytes before broadcasting. On resume, re-send those same bytes rather than building a new transaction.\n2. **Low: the post-split check only stops in one direction** (`lib.js:291`). AUDIT-6 says the step stops unless the deployer holds exactly the agents' stake. In the code it stops only when the deployer holds less; when it holds more (a part was skipped) it just logs a message. A skip happens if `rec.split` still holds successful hashes from an earlier attempt, because `once` never checks what the recorded transaction actually transferred.\n   - **Reproduced:** with such a record, `split` finishes, is marked done, and the deployer keeps 950M. After the agents step that would leave the deployer key holding 700,032,300.\n3. **Low: vesting replacement can loop forever** (`lib.js:217`). With `VEST_DELAY=0`, or any delay shorter than the time the deploy, approve and fund transactions take, `fund()` always reverts with `AfterStart`. Each resume then sets that contract aside as stale and deploys another one that fails the same way. Nothing is lost, but the step never finishes. The carry-over arithmetic itself is correct: stale totals are counted, so the overall total stays at 50,000,000.\n4. **Info: the farm's daily turnover limit resets at UTC midnight** (`MSFT95AgentFarm.sol:432`). It is not a rolling 24 hours. A keeper can do two full round trips seconds apart across midnight, roughly 4× `slipBps` instead of the documented 2×. It can also withdraw the agent's liquidity into cash before the day's first swap, so the budget covers everything the agent holds, not just its cash. Holder withdrawals and removals never touch this limit.\n\n**Areas traced with nothing reportable**\n\n- **Staking `sync` overflow guard:** after the guard, `fresh·1e36 + idle + dust + left` stays within uint256. Overflowing the per-share counter would take about 10¹⁸ maximum-size arrivals. Holding back a legitimate reward would need a prior arrival of about 10⁴¹ wei, far beyond any listed token's supply.\n- **Hook full-fill check:** for exact-in and exact-out swaps in both token orders, a full fill moves exactly `named − fee` (exact-in) or `named + fee` (exact-out) of USDG, so a legitimate full fill is never refused.\n- **Farm `skim`:** every change to `owed` matches a real token movement in deposits, swaps, fee collection, adds, removals and the treasury's cut. `skim` is `nonReentrant`, so it can't run during the farm's own pool callbacks.\n- **Earlier fixes (item 6):**\n  - the `openTrading` position and price gate holds;\n  - the vault's house-sweep window is separate from players' withdrawals;\n  - MSFT can't be paid out by voucher (`maxWithdrawal` is 0) but can still be swept;\n  - each keeper's damage is bounded by price feeds, the unlock schedule, or the farm's per-day limits, with one exception: under `ALLOW_NO_F","treeHash":null,"usage":{"cachedInputTokens":1826558,"inputTokens":34,"model":"claude-opus-5-5","outputTokens":40593,"runtime":"claude","turns":19,"wallClockMs":442869}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"401c4383398af47c","findings":[{"citation":"resolved","description":"AUDIT-6 M-1 says each split transfer is recorded by its hash 'the moment it is sent', so that 'nothing is paid twice'. In once(), the hash is written (line 279) only after fn() has resolved, which means after eth_sendRawTransaction has answered. Nothing is saved before the signed transaction leaves the key: no hash, no nonce, no raw tx. Two resume paths therefore pay a part again. (a) The node accepts the transaction but the call throws or never returns (RPC timeout or 5xx, connection reset, Ctrl-C or kill while it hangs). sent[key] stays empty, and the next run builds a new transfer. (b) The hash was saved, but on resume getTransactionReceipt(h) and getTransaction(h) both return null, for example on a load-balanced RPC whose node hasn't indexed the transaction yet. Lines 274-276 then log 'was dropped: sending again'. In both cases the new transaction gets the next nonce, so the first one can't stop it and both are mined. The post-split check (lines 289-290) runs only after the tokens have left. Also unverified: hardhat-ethers v3's sendTransaction keeps polling getTransaction(hash) until the node returns the transaction, and only then returns. That would make window (a) longer than a single RPC round trip. node_modules is not in the tree, so I could not confirm this. Impact with the defaults (TREASURY = LP_WALLET = SAFE): a duplicate treasury part sends 500,000,000 MSFT95 to the Safe. The 200,000,000 liquidity transfer then reverts, because the deployer holds 199,967,700. Every later resume stops at the same place, and the launch can't finish until the Safe signs 250,000,000 back by hand. A duplicate economy part puts a second 250,032,300 into the vault bankroll. The house can take MSFT95 back out only through sweep, at the 100,000 a day dailyCap, unless the Safe raises it with setToken. Shared and deployer-kept wallets otherwise behave correctly: mine() leaves out a part the deployer keeps, and `expected` counts it. The vesting step doesn't have gap (a), because it reads on-chain total() rather than a saved hash. It only has a narrow window where a fund() is still pending when the resume reads total(). Fix: sign before broadcasting. Use populateTransaction with an explicit nonce, then signer.signTransaction, then keccak256(raw), and save {hash, nonce, raw} before provider.broadcastTransaction(raw). On resume with no receipt, rebroadcast the same raw transaction. If getTransactionCount(me,'latest') is already past the saved nonce and there is still no receipt, stop for a manual check instead of sending a new transaction.","line":278,"path":"msft95-contracts-audit-v5/scripts/mainnet/lib.js","reproduction":"Ran the unmodified scripts/mainnet/lib.js (copied byte-identical, checked with cmp) outside the repo, with its split() driven against an in-memory chain. Start state: the deployer holds 950,000,000 MSFT95 after the vesting step, SAFE is set, and TREASURY and LP_WALLET are unset. Expected after split: Safe 450,000,000, vault 250,032,300, deployer 249,967,700. Scenario (a): the treasury transfer is applied by the node, but the send call throws 'timeout'. Output: `run 1: stopped: timeout ... | Safe 250,000,000 | vault 250,032,300 | deployer 449,967,700 | record.split [economy]`; `run 2 (resume): stopped: ERC20: transfer amount exceeds balance | Safe 500,000,000 | deployer 199,967,700 | record.split [economy,treasury]`; run 3 gives the same result as run 2. So the treasury part is paid twice and the step can't complete. Scenario (b): run 1 is killed while waiting for the treasury receipt, after its hash was saved. On resume, the RPC returns null for both getTransactionReceipt and getTransaction of that hash. Output: `run 2 (resume via lagging RPC node): stopped: ERC20: transfer amount exceeds balance | Safe 500,000,000 | vault 250,032,300 | deployer 199,967,700`. Actual: the Safe gets 500,000,000 and the deployer is 50,000,000 short of the agents' stake. Expected: each part is paid once.","severity":"medium","snippet":"        const tx = await fn();\n        sent[key] = tx.hash; save();","title":"split(): a part's tx hash is saved only after the broadcast call returns, so a lost RPC answer or a lagging RPC node makes the resume pay the same part again with a new nonce (merges 4 specialist repo"},{"citation":"resolved","description":"_useTurnover is keyed per (agent, input token) and resets on the calendar-day number. The contract header (lines 60-64) and AUDIT-6 #7 say a keeper can now cost an agent 'at most about 2 x slipBps of its cash per day'. (1) Both turnover and the swap counter _swapDay (line 324) reset at block.timestamp / 1 days, not over a rolling 24h like the vault's _spend. So a round trip at 23:59:5x and another at 00:00:0x each pass, about 4 x slipBps within seconds. (2) Every token has its own budget, its balance at that token's first swap. So each extra listed token in a path adds a leg. deploy-farm.js lists PEG stable/stable pools (slipBps 30) next to the stock pools, so USDG -> USDC -> stock -> USDC is 3 legs a day. If the admin lists a stock against both stables, USDG -> A -> USDC -> B -> USDG is 4 legs. (3) The base is read when the keeper first swaps, and the keeper can call remove() just before that, so the budget covers the agent's LP position too, not just its cash. The limit doesn't block holders: withdraw, holderRemove, claimFees and setFrozen never touch turnover. So this is a loss bound looser than documented, not a way to move funds out. Fix: use a rolling 24h window, a start time plus 24h as in the vault's _spend, for both turnover and _swapDay. If 2 x slip is meant as a hard bound, count one budget per agent, not per token, for example the dollar value at the day's first swap. Otherwise document the real bound.","line":431,"path":"msft95-contracts-audit-v5/v4/MSFT95AgentFarm.sol","reproduction":"Traced by hand against the code. No farm test was executed: the tree has no node_modules or v4-core, so the farm can't be compiled here. Pool: AAPL/USDG ORACLE with slipBps 150 (as deploy-farm.js lists it), active and past usableAt. Agent 1 holds 10,000 USDG and isn't frozen. The keeper is the counterparty and sandwiches each leg down to the floor. At 23:59:50 UTC, swap(1, idx, USDG->AAPL, 10,000e6) sets turnover[1][USDG] to {day D, base 10,000e6, used 10,000e6}. Then swap(AAPL->USDG, all AAPL) sets turnover[1][AAPL] to used = base. A third USDG swap would revert OverCap here, as intended. At 00:00:05 UTC, t.day is D but today is D+1, so line 432 resets base to the remaining ~9,700e6 USDG with used 0, and _swapDay restarts its count at 1. The same two legs pass again. Expected per the doc: about 2 x 1.5% = 3% in a day. Actual: about 4 x 1.5%, roughly 5.9% of the agent's cash, within 15 seconds. With a PEG pool USDG/USDC (slipBps 30) and a TSLA/USDC pool, a day also allows USDG->USDC->TSLA->USDC: 3 legs, about 3.3%, and about 6.6% across midnight.","severity":"info","snippet":"        uint64 today = uint64(block.timestamp / 1 days);\n        if (t.day != today) { t.day = today; t.base = balanceOf[agentId][token]; t.used = 0; }","title":"Farm turnover limit resets at 00:00 UTC and is kept per token: a sandwiching keeper fits about 4 x slipBps into a few seconds around midnight, and a PEG pool adds a leg per day, against the documented"},{"citation":"resolved","description":"config() takes VEST_DELAY as Number(env.VEST_DELAY || 3600), so '0' gives 0 and isn't rejected. start is the latest block's timestamp plus that delay. MSFT95Vesting.fund reverts when block.timestamp >= start (MSFT95Vesting.sol line 60). The fund transaction mines in a later block than `latest`, so its timestamp is >= start and it always reverts. On each resume, the audit-6 L-2 branch (lines 210-214) sees total 0 with now >= start. It pushes the contract to vestingStale and deploys another one, which fails the same way. Nothing is lost: have counts the stale totals, which are 0. But the launch can't pass the vesting step, and each attempt leaves another empty vesting contract on chain. Fix: in config(), reject a VEST_DELAY below a safe minimum (for example 600 s). Before approve and fund, check that start is still at least a margin ahead of the latest block timestamp.","line":217,"path":"msft95-contracts-audit-v5/scripts/mainnet/lib.js","reproduction":"Traced against the code. With env VEST_DELAY=0 and the launch at step vesting: deploy(MSFT95Vesting, ..., start = T) mines at T' >= T, approve mines, and fund(50,000,000e18) mines at T'' >= T, so it reverts AfterStart and the step throws. Next run: A().vesting total() = 0 and now >= start, so the address goes into vestingStale and a new schedule is deployed with start = now, and fund reverts again. Expected: one schedule funded with 50,000,000. Actual: vestingStale grows by one address per run, and the step is never recorded.","severity":"info","snippet":"        const start = (await now()) + cfg.VEST_DELAY;","title":"Vesting step can never finish with VEST_DELAY=0 (or any delay shorter than deploy+approve+fund): every resume marks the empty schedule stale and deploys another whose fund() reverts AfterStart (merges"},{"citation":"resolved","description":"config() runs addr() (isAddress plus checksum) on SAFE, HOOK_KEEPER, STAKE_KEEPER, SIGNER, COSIGNER, GUARDIAN and MSFT_FEED before any transaction (comment at line 46: 'everything the launch needs is checked before the first transaction'). The four wallets that receive supply or roles are copied as given. A typo is only caught when ethers uses the value. OPS fails at the vesting constructor. TREASURY and LP_WALLET fail in split, after economy has already moved 250,032,300 into the vault. TEAM fails at the agents step. By then the deployer key still holds the rest of the supply and owns the token and the hook. The launch can be recovered by fixing the env and resuming, and no funds are lost. Fix: when not dry, call addr(k, SAFE) for OPS, TREASURY, LP_WALLET and TEAM inside config().","line":57,"path":"msft95-contracts-audit-v5/scripts/mainnet/lib.js","reproduction":"Traced against the code. Set TREASURY=0x5fc5360D0400a0Fd4f2af552ADD042D716F1d16 (one hex digit short) with every other setting valid. Expected per the audit-4 L-4 comment: config() throws before the first transaction. Actual: config() returns, because it never calls isAddress on TREASURY. Steps token through vault run. In split, once('economy') pays 250,032,300 to the vault, and then m.transfer(parts.treasury[0], ...) throws from ethers ('invalid address' or an ENS lookup error). mine() compares lower-cased strings and doesn't validate either.","severity":"info","snippet":"    OPS: env.OPS || SAFE, TREASURY: env.TREASURY || SAFE, LP_WALLET: env.LP_WALLET || SAFE, TEAM: env.TEAM || SAFE,","title":"Launch pre-flight doesn't validate OPS, TREASURY, LP_WALLET or TEAM, so a malformed value stops the launch half way, after token, hook, pool, staking and part of the split, which audit-4 L-4 was meant"}],"hash":"80d8246c5a7c94f0f6dcd1ba26ca71df91dc827a63bd9f109fd495aa077b3acb","nodeId":"e2845f7c-7640-42c7-80c6-2f9fbbeef201","outcome":"completed","summary":"I kept 4 findings out of 11 specialist reports: 1 medium and 3 info, all written to `.imd-findings.json`. Only the medium one was actually run; the other three I checked by tracing the code by hand. Nothing in the repo was changed (`git status` is clean apart from the ignored findings file).\n\n| # | Severity | Where | Issue |\n|---|---|---|---|\n| 1 | medium | `scripts/mainnet/lib.js:278` | `split` can pay a part twice when a stopped launch is resumed |\n| 2 | info | `v4/MSFT95AgentFarm.sol:431` | The farm's daily turnover limit allows more loss than the documented 2 × slipBps |\n| 3 | info | `scripts/mainnet/lib.js:217` | With `VEST_DELAY=0` the vesting step can never finish |\n| 4 | info | `scripts/mainnet/lib.js:57` | The pre-launch checks skip `OPS`, `TREASURY`, `LP_WALLET` and `TEAM` |\n\n**1. Split can pay twice (reproduced).** The transaction hash is saved only after the send call returns, and nothing pins the nonce. A resume therefore builds a fresh transaction when:\n- the node accepted the transfer but its answer was lost (timeout, Ctrl-C); or\n- the hash was saved but the RPC node returns nothing for it yet (\"was dropped\").\n\nI ran the unmodified `lib.js` (copied outside the repo, identical by `cmp`) against an in-memory mock chain. Both cases left the Safe holding 500,000,000 instead of 450,000,000. The deployer was left with 199,967,700, so the liquidity transfer reverts on every later resume and the launch is stuck. Shared wallets and wallets the deployer keeps are otherwise handled correctly. The vesting step doesn't have the lost-answer gap, because it reads what is actually on chain; only a short pending window remains.\n\n**2. Turnover limit (traced).** The budget resets at 00:00 UTC rather than over a rolling 24 hours, and each token has its own budget. A keeper sandwiching its own swaps can do two round trips about 15 seconds apart across midnight, costing roughly 4 × slip. A stablecoin-to-stablecoin pool, which `deploy-farm.js` lists, adds a third leg per day. Holder withdrawals and removals never touch the limit, so a keeper cannot use it to block an exit.\n\n**3. `VEST_DELAY=0` (traced).** Funding is only allowed before the start time, which is now plus the delay. With no delay, every funding call reverts. Each resume marks the empty contract stale and deploys a new one, forever.\n\n**4. Unchecked wallets (traced).** A mistyped `TREASURY` passes the pre-launch checks and fails mid-split, after 250,032,300 has already gone to the vault. That is the halfway stop the earlier audit-4 fix was meant to prevent.\n\n**Merged and dropped specialist reports**\n- The four reports of the split double payment (one had a wrong path) are merged into finding 1. The three turnover reports are merged into 2, and the two `VEST_DELAY` reports into 3.\n- I dropped \"a skipped part is only logged, not stopped\". It needs a reused or hand-edited launch record to trigger. The code does differ from AUDIT-6's wording there (\"must hold exactly … or the step stops\").\n- I also re-checked the 4-leg turnover chain one specialist described. `deploy-farm.js` lists one deepest pool per stock, so I based finding 2's reproduction on the midnight reset and the stablecoin pool instead.\n\n**Areas you asked about with nothing to report** (traced, not executed):\n- **(2) Staking overflow guard:** the guard keeps every later sum within range, and the `_accrue` amount is capped by the stream's total. Holding back a real reward would need a balance above about 1e41 base units.\n- **(3) Hook full-fill check:** an exact-in full fill moves exactly the named amount minus the fee, and an exact-out full fill exactly the amount plus the fee. That holds for both directions and both token orders, so I found no way for it to refuse a real full fill.\n- **(4) Farm `skim`:** every way tokens enter or leave updates `owed`. Every path that holds tokens mid-operation is reentrancy-locked, so `skim` can't run during a callback, and uncollected pool fees aren't in the farm's balance.\n- **(6","treeHash":null,"usage":{"cachedInputTokens":1847611,"inputTokens":32,"model":"claude-opus-5-5","outputTokens":43343,"runtime":"claude","turns":18,"wallClockMs":474925}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f96a0c89c039c22a","findings":[{"citation":"resolved","description":"once() writes sent[key] only after fn() has returned, and fn() returns only after eth_sendRawTransaction answers (line 277-279: `const tx = await fn(); sent[key] = tx.hash; save();`). If the RPC accepts the transaction but the call throws (timeout, 5xx, the process is killed), no hash is saved, and the next run sends the part again. A saved hash that the RPC node does not return (getTransaction null: a load-balanced RPC on a node that hasn't seen it yet, or a node that evicted it from its mempool while others kept it) is treated as dropped and re-sent with a NEW nonce, so the original can still be mined too. Nothing pins or reuses the nonce. The vesting step has the same gap: `need = amt - have - (await v.total())` reads total() while an earlier fund() may still be pending, so a resume approves and funds 5% a second time. The final balance check (line 289-290) catches this only after the tokens have left. Fix: Before sending, record the intent and the deployer's pending nonce. Send with that explicit nonce, and on resume re-send only with the same nonce (a replacement), so at most one of the two can be mined. Or, before resending, refuse to continue while getTransactionCount(me,'pending') > getTransactionCount(me,'latest'). Do the same in the vesting step before reading total().","line":275,"path":"msft95-contracts-audit-v5/scripts/mainnet/lib.js","reproduction":"Mainnet launch, TREASURY=0xT (not the deployer). In `split`, the treasury `m.transfer(0xT, 250M)` is broadcast and the RPC times out before ethers returns the hash (or the run is killed with Ctrl-C between broadcast and save()). rec.split.treasury is never written. Resume: once('treasury') sees no hash and sends another 250M with nonce n+1. Both mine. The deployer started split holding 950M (economy 250.03M + treasury 250M + liquidity 200M + agent stake 249.97M), so the liquidity transfer still succeeds and leaves 0. Then line 290 throws 'holds 0, less than 249,967,700'. Expected: no part is paid twice, and the agents' stake stays with the deployer. Actual: 0xT gets 500M, and the agents step cannot be funded until someone returns 250M by hand. Vesting variant: the run is killed after v.fund(50M) is broadcast but before it is mined. On resume, total()==0 gives need=50M, and approve and fund are sent again. Both fund calls mine (nonce order: fund#1, approve#2, fund#2), 100M is locked in the year-long vesting, and the split later throws because the deployer is 50M short.","severity":"low","snippet":"          log(`    ${key}: ${h} ${r ? \"reverted\" : \"was dropped\"}: sending again`);","title":"split/vesting resume can pay a part twice: a send whose hash was never saved, or a hash the RPC no longer knows, is re-sent with a new nonce"},{"citation":"resolved","description":"The budget is per calendar day (block.timestamp / 1 days), not a rolling 24h window. AUDIT-6 #7 says a compromised keeper can cost an agent at most about 2 x slip a day. That holds per UTC day, but within any 24-hour stretch the keeper can do 4 legs. Fix: Use a rolling window (start time + 24h, as the vault's _spend does), or document that the bound is per UTC day.","line":432,"path":"msft95-contracts-audit-v5/v4/MSFT95AgentFarm.sol","reproduction":"ORACLE pool with slipBps 150. An agent holds 10,000 USDG. At 23:59:50 UTC the keeper swaps USDG->stock (all of it), then stock->USDG, sandwiching both: about 3% lost. At 00:00:05 the next day, turnover resets with base = the remaining ~9,700, and the keeper repeats: about 6% lost in 15 seconds. Expected per the doc: about 2 x slip in a day. Actual: about 4 x slip within seconds. The holder's withdraw/holderRemove are never blocked by turnover (checked), so this is about the loss bound only.","severity":"info","snippet":"        if (t.day != today) { t.day = today; t.base = balanceOf[agentId][token]; t.used = 0; }","title":"Farm turnover limit resets at the UTC day boundary, so a keeper can run two days' budget back to back"}],"hash":"9d3fc1a42485299eda4926c8deec7fc2b6539b711328df1b5008163561d25d58","nodeId":"b5001bff-c31a-4023-aaf1-880fad4bed53","outcome":"completed","summary":"I found one low and one info issue, and nothing high or critical. The findings are in `.imd-findings.json`. I changed and deployed nothing, and ran no tests. Neither finding carries a Foundry proof, since proofs are only required for high or critical findings. Everything below comes from reading the code.\n\n**Findings**\n- **Low: resuming the launch can pay a part twice (`scripts/mainnet/lib.js:275-281`).** The script saves a transfer's transaction hash only after the node has confirmed receiving it.\n  - **Lost hash:** the node accepts the transfer but the call times out, or the run is killed before the save. The next run then sends the same part again.\n  - **Unknown hash:** the RPC node no longer knows a saved hash (a load-balanced or evicting node). The script treats it as dropped and sends again with a new nonce, so the original can still be mined as well.\n  - **Example:** the 250M treasury transfer gets paid twice. The liquidity transfer then uses up the deployer's last 200M, and the end-of-split balance check fails only after the agents' 249,967,700 has already gone to the treasury.\n  - **Vesting:** a resume while `fund()` is still pending reads `total()` as 0 and funds the 5% again, so 100M ends up locked in vesting.\n  - **Fix:** record the nonce before sending and only ever resend with that same nonce, or stop while the deployer has pending transactions.\n- **Info: the farm's daily turnover limit (`MSFT95AgentFarm.sol:432`) resets at the UTC day boundary, not over a rolling 24 hours.** A compromised keeper can do two round trips in seconds around midnight, losing about 4× the slip allowance instead of the 2× a day that AUDIT-6 #7 claims. The holder's exits are never blocked by it.\n\n**No defects found in the other areas**\n- **Vesting replacement:** stale schedules are counted, so the total stays at 50M. The only gap is the pending-`fund()` case above.\n- **`MSFT95Staking.sync` / `_accrue`:**\n  - **Overflow:** the guard covers the largest product (`fresh*1e36 + back + d + left`). `rate * time` can't exceed that total. Pushing `accPerShare` or `held` to overflow would take an impossible number of hourly streams.\n  - **Holding back rewards:** the guard only trips above about 1.15e41 wei of a single token, which no listed token comes close to. When it trips, it only holds back that token.\n- **`MSFT95RewardHook.afterSwap` full-fill check:** v4 passes the pool's own swap amount to `afterSwap`. A full exact-in fill moves exactly `named − fee` and a full exact-out fill moves exactly `named + fee`, in either token order. So it can't refuse a real full fill.\n- **`MSFT95AgentFarm.skim`:** `owed` covers agents' balances, collected fees and the treasury's cut. `skim` shares the reentrancy lock with every function that changes balances, so it can't run during a callback, a fee collection or a removal.\n- **Turnover limit:** apart from the day-boundary issue above, the keeper's only way round it is depositing its own money into the agent. Withdrawals and `holderRemove` never touch the limit.\n- **Earlier fixes, re-checked:**\n  - `openTrading` checks the named full-range position and that the price is still the launch price.\n  - The vault's `houseWindow` is separate from the players' withdrawal window.\n  - MSFT is listed with `maxWithdrawal 0` and deposits off, so only the Safe's sweeps can move it.\n  - A compromised keeper's loss is bounded by the price-feed floor, the daily pool cap and the turnover limit. The day-boundary issue above weakens that last bound slightly.\n\n**Not covered in depth:** `MSFT95AgentStakes`, `MSFT95Agents`, the router and the token itself.","treeHash":null,"usage":{"cachedInputTokens":952125,"inputTokens":30,"model":"claude-opus-5-5","outputTokens":13127,"runtime":"claude","turns":16,"wallClockMs":169025}}],"verification":[]}