{"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":"56694dc1-7e9a-422c-92b2-0cec74a3e938","kind":"audit","nodes":[{"acceptedSubmissionHash":"581e8dfde7108e6c7add6a5a8d4802fee3e9ec8362801ba209a400bf9c24ad99","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"15b7d6ef41c261427f00346b3c2c0b79d81021a1428b5762186f6fd095fcfe9e","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"f70e84f6b32529f21d13e8edd9a5699bd479552b84222912ecee9ee442b64498","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"b89279670f5a00f5d26286edee00c97dd158b997031350a01cafe6e0329ad227","skillId":"audit-judge","subagents":4,"tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"b89279670f5a00f5d26286edee00c97dd158b997031350a01cafe6e0329ad227","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"b3f8c6de1a5e80610c5bbcb2f2baa40cdf179b99b285e475ba49d92604f6e2fc","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"fdfb59ea28b81b731bd7fd2ded5e66d64b9094d05cc1c6f5a5edd6a24bbfde2b","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","subagents":3,"tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"bc5bfc8455464381f2db18badb2b55cf8bec1e535c019349a0378fa990741171","skillId":"audit-specialist","state":"accepted"}],"objective":"Review Submission14/ only (read Submission14/AUDIT_README.md first; Submission13 to 5 AUDIT_README are the reference for everything unchanged). Not deployed. This is the remediation of audit 8b5de2de (6L 2I, all in the deployment tools) on the build intended for Ethereum mainnet. The contract did not change: runtime, creation code and ABI are identical to Submission13, metadata included. Changed: tools/deploy-mainnet.mjs and safe-batch.mjs; the diff is in build/tools-and-tests.Submission13-to-14.diff. Section 2 maps each finding to its fix, section 3 lists new code beyond the report, sections 5 and 7 say what is accepted and what is unverified.\n\n1. Signing first in send(): can a recorded hash no node ever held change a decision wrongly? Can a refusal remove a held hash?\n2. refusedSend and the words list: can an error count as a refusal when the node took the transaction? Can a refusal in a client's words be missed?\n3. queued and meetSeed: can a live transaction leave the record, can a resume repeat forever, can a step land twice?\n4. floor starts at 0: what stops a run at a spent nonce on a stale read? Is plan + steps + failed right for every record the tool can write?\n5. recordedReceipt: can a real reorg stick a run? Can a mined entry still be dropped?\n6. Fees at the ceiling: is the last bid always acceptable to a node? Can the ceiling branch repeat a bid?\n7. chainState's code check: can it stop a valid accept or open?\n8. Anything in audit 8b5de2de that is not actually fixed.\n\nReport severity, reproduction and fix. Not in scope: artwork, the Mint web page, World 3D.","parentJobId":null,"planHash":"2a3de27354c94e4c8427c4fd5c05b162c88985cfe1d7322bb6315b4a4f9743f9","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"56694dc1-7e9a-422c-92b2-0cec74a3e938","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":"51168","feedbackHash":"4e293302a8f0bc6980c3d4b042098c4f94d8154e1d58ce3ead6fdba806d1e157","nodeKey":"audit_economics","submissionHash":"581e8dfde7108e6c7add6a5a8d4802fee3e9ec8362801ba209a400bf9c24ad99","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51043","feedbackHash":"a467f8cef50f322653583cdf56ab8f08f3c5c0003c98414440d21c05f622cb41","nodeKey":"audit_flow","submissionHash":"15b7d6ef41c261427f00346b3c2c0b79d81021a1428b5762186f6fd095fcfe9e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51744","feedbackHash":"73c1b5dc6a14b6ff5bf170f297db4ea9a258c10c1fef2b4df89455459cbe234d","nodeKey":"audit_judge","submissionHash":"f70e84f6b32529f21d13e8edd9a5699bd479552b84222912ecee9ee442b64498","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52481","feedbackHash":"722cc3ad6f9ef6085867ca45f14ff7be2a8d6da42bb03b137456d04948d07115","nodeKey":"audit_math","submissionHash":"b3f8c6de1a5e80610c5bbcb2f2baa40cdf179b99b285e475ba49d92604f6e2fc","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52616","feedbackHash":"70d50a5c4d848b422421b18f3e6741e83eead01b5a6dee58c116ae89e9c6b8b5","nodeKey":"audit_permissions","submissionHash":"fdfb59ea28b81b731bd7fd2ded5e66d64b9094d05cc1c6f5a5edd6a24bbfde2b","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"d57d9f983011aec9a79f02a1f98ac02bbdca4b09e4baa0d591e8178a452d9f57","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2565f234b0a569e9","findings":[{"citation":"resolved","description":"Finding 2 of audit 8b5de2de is fixed by signing first and keeping the hash on every answer except a refusal. refusedSend (line 30) counts NONCE_EXPIRED and INSUFFICIENT_FUNDS as refusals that prove the node holds no such thing, and line 269 drops the hash at once. Neither code proves that on the RPC the tool is written for (a load-balanced or retrying gateway, README section 4 item 1). (a) NONCE_EXPIRED: ethers maps any 'nonce ... too low' text to it. A gateway that delivered the raw transaction to one node, lost the reply and retried after the transaction mined answers 'nonce too low' about the very transaction the record just added. The hash is spliced out, `hashes` is empty, `stableNonce()>nonce` holds and the run stops with NONCE_MOVED instead of waiting for the receipt that is one poll away. (b) INSUFFICIENT_FUNDS: a lagging node that has not yet seen the deployer's funding answers 'insufficient funds' while another node behind the same gateway already holds the transaction (the gateway fans the retry out). The hash is removed; the next attempt only recovers it if the market is unchanged so that the identical bytes are re-signed ('already known' re-adds it); with any fee move the record never watches the held transaction again. Consequences: the step lands with no hash in the record, is skipped by the gate on the next resume and listed in notInRecord with its gas missing from totalGas; the deploy step loses its creation hash (verify-etherscan needs it); if the held transaction reverts (setRevealCommit with an expired acceptance) the revert is not in `failed`, so plan+steps+failed undercounts and the meetSeed exception at line 245 ('this step's own last try reverted') is disabled for it, and a later resume meeting a queued transaction at that nonce reports RESUME_OUT_OF_ORDER until that transaction mines. The README's own rule for this path is 'anything else keeps it, and it is watched'; these two codes do not meet the standard of 'a node answered no to this transaction: it holds no such thing'. Fix: on NONCE_EXPIRED do not splice; first run firstReceipt([own], pollMs*visibleReads): a receipt means the hash is the one that spent the nonce (record it); no receipt means another transaction spent it and the hash can go, keeping the existing `!hashes.length` NONCE_MOVED gate. On INSUFFICIENT_FUNDS only drop the hash when two consecutive getBalance(owner,'latest') reads both fall below gasLimit*maxFeePerGas+value; otherwise keep the hash and treat the answer like a 502.","line":269,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"State: a fresh run on the local chain (any step, e.g. the 2nd, setRevealURIs). Input: wrap provider.broadcastTransaction so that for the 2nd raw transaction it calls the real broadcast, mines one block, then throws Object.assign(Error('nonce too low'),{code:'NONCE_EXPIRED'}) (what a retrying gateway returns for a transaction it delivered). Expected (per README section 2, finding 2, 'anything else keeps it, and it is watched'): the run goes on, d.steps.length===6, d.notInRecord deepEquals [] and d.steps[1].hash is the broadcast hash, exactly as the existing test 'audit 8b5de2de finding 2' asserts for TIMEOUT and SERVER_ERROR. Actual: line 269 removes the hash, line 277 sees hashes.length===0 and stableNonce()>nonce and throws 'NONCE_MOVED setRevealURIs (not locked)'; the checkpoint written has no pending and no hash for the step; the resume skips the step by the gate and returns notInRecord ['setRevealURIs (not locked)'] with its gas absent from totalGas. Same shape with code INSUFFICIENT_FUNDS thrown after the real broadcast and a fee move between attempts: the held transaction is never watched again and the run ends in NONCE_MOVED when it mines.","severity":"low","snippet":"    if(fresh&&refusedSend(e))hashes.splice(hashes.indexOf(own),1);","title":"send(): a NONCE_EXPIRED or INSUFFICIENT_FUNDS answer removes a hash a node may hold, so the step lands under a hash the record no longer has"},{"citation":"resolved","description":"Every other 'absent' decision the tool takes on a resume is repeated: notDone reads visibleReads passes, codeAbsent reads 10, recordedReceipt needs visibleReads matching block answers, because one request reaches one node. meetSeed is the guard that keeps a step whose gate read was stale from being sent at the nonce of another step's waiting transaction (RESUME_OUT_OF_ORDER). Line 244 lets the step through on a single null per hash. eth_getTransactionByHash for an unmined transaction is answered from the pool of whichever node the gateway picks; a node that has not received it by gossip yet, or a gateway that does not serve pending transactions by hash at all, answers null while another node holds it. The step is then signed and broadcast at that nonce at the market fee. If the broadcast lands on the node that holds the other transaction it is refused as underpriced (harmless, the bid climbs); if it lands on a node that does not, both are in the network at the same nonce and either may mine. When the sent step is a true stale-gate repeat (it already landed under a hash the record lost, which is exactly the case RESUME_OUT_OF_ORDER exists for) and it wins, the contract executes it again: addPaymentToken(USDC) and setRevealCommit revert (status 0, gas burned, entry in failed), setRevealURIs/transferOwnership succeed harmlessly, and the other step's transaction is dropped from the pool and must be resent at the next nonce. Nothing is unrecoverable, but gas is burned and a spurious failed entry is recorded, which the guard was written to prevent, and Submission 13's acceptance of 'a stale null here' covered only the TIP_CEILING branch where the consequence is a refused bid of the same step. Fix: treat 'no node holds it' as proved only after visibleReads consecutive all-null answers spaced pollMs apart (reuse the pattern of codeAbsent), and short-circuit to 'held' when getTransactionCount(owner,'pending') > nonce; keep the catch(()=>true) so an RPC error stays 'held'.","line":244,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"State: the second half of the existing test 'audit 8b5de2de review: a step whose tries reverted goes out at the nonce of a later waiting transaction...': a stoppedRun at stopBefore 2, record with queued [{name:USDC_GOLD,nonce:n,hashes:[fake],fee:FAKE_FEE}] and no failed entry for setRevealCommit. Input: provider.getTransaction answering {} for `fake` on the 1st call and null on the 2nd (a load-balanced RPC: one node has the transaction, the next does not), with the first resume's call order such that meetSeed's read is the null one; equivalently script the RPC server so eth_getTransactionByHash(fake) returns null on every 2nd request. Expected: RESUME_OUT_OF_ORDER setRevealCommit, as the test asserts when the first node answers, because a transaction one node showed is live. Actual: line 244 returns on the null, setRevealCommit is signed at nonce n and broadcast (srv.state.sent[0].nonce===n at the market tip), racing the queued transaction for the nonce. With the queued transaction being a real pending addPaymentToken (sendTransaction with automine off at nonce n, as the test 'a later transaction that lands right after the refill' builds it) and a stale eth_call for revealCommit() so the setRevealCommit gate says 'not done' although it already landed under a hash the record lost (the record's count is then one short, which is how nonce n is reached for it): the repeat setRevealCommit is mined, reverts (previous commit mismatch), gas is burned, d.failed gains an entry, and addPaymentToken is resent at n+1.","severity":"low","snippet":"   if(!(await Promise.all(seed.hashes.map(h=>provider.getTransaction(h).catch(()=>true)))).some(Boolean))return;","title":"meetSeed: one null eth_getTransactionByHash answer is taken as 'no node holds it' and a different step is broadcast at the nonce of a live transaction"},{"citation":"resolved","description":"The exception at line 245 is meant for 'this step's own last try having reverted'. It checks only that some failed entry carries the step's name. A failed entry never leaves the record (apart from the one-per-resume reorg drop), so once a step has reverted once, every later resume treats a 'not done' gate for that step as proof that it must go out, and bids over whatever other step waits at the nonce. That is wrong when the step did succeed after its revert and the success is simply not in steps: a success whose hash the record lost (finding 1 above, or any notInRecord case), with a lagging gate read. Then plan+steps+failed is one short, the gate says 'not done', the later step's transaction is held at that nonce, and line 245 lets the repeat through at 125% of the held fee. The repeat replaces the live transaction, executes again and reverts (setRevealCommit: previous mismatch; addPaymentToken: token exists), burning gas and adding a second failed entry, after which the displaced step is resent. The check should bind the revert to the current position: accept the exception only when the step's latest failed entry is the highest-nonce entry in steps+failed and that nonce is nonce-1 (nothing of this deployer landed after the revert); otherwise fall through to RESUME_OUT_OF_ORDER.","line":245,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Record: planNonce p; steps for deploy and setRevealURIs at p, p+1; failed [{name:'setRevealCommit', nonce p+2 (status 0)}]; setRevealCommit succeeded at p+3 under a hash the record does not hold (not in steps); queued [{name:'addPaymentToken(USDC, 1600 GOLD)', nonce p+4, hashes:[h], fee:{maxPriorityFeePerGas:'1200000000',maxFeePerGas:'500000000000'}}] with h really pending in the node's pool; eth_getTransactionCount answers p+4 (fresh) and the revealCommit() eth_call answers 0x0 for the first visibleReads passes (a lagging node). Expected: the stale read is caught, RESUME_OUT_OF_ORDER setRevealCommit at the nonce of addPaymentToken, resume again. Actual: notDone('setRevealCommit') returns true, send() runs meetSeed, line 244 sees h held, line 245 finds the old failed entry by name and does not throw, last=feeOf(seed.fee); the repeat setRevealCommit is signed at nonce p+4 with maxPriorityFeePerGas >= 1.5 gwei and broadcast; it replaces h, mines, reverts (previous commit mismatch), its gas is burned and recorded in failed; addPaymentToken is resent at p+5.","severity":"info","snippet":"   if(!failed.some(f=>f.name===name))throw Error('RESUME_OUT_OF_ORDER '+name+' at the nonce of '+seed.name+' ('+seed.hashes.join(',')+'): run the resume again');","title":"meetSeed's revert exception is keyed on the step name alone, so any historical failed entry lets a stale-gate repeat outbid a live later transaction"}],"hash":"15b7d6ef41c261427f00346b3c2c0b79d81021a1428b5762186f6fd095fcfe9e","nodeId":"a692c364-1153-4e22-8a3b-84cfcb6e216a","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds three findings, two Low and one Info, all in `Submission14/tools/deploy-mainnet.mjs`. No file other than the findings file was changed.\n\n**What I checked.** The Submission14 README, both changed tools, the full tools-and-tests diff, and the Submission13 README for the prior findings and accepted cases. I also fetched the Nethermind and Besu source strings for refused transactions. Bash was unavailable in this session (the sandbox failed to initialise), so nothing was executed. Every conclusion comes from reading the code and tracing the state machine by hand.\n\n**Findings.**\n\n- **Low, line 269.** A `NONCE_EXPIRED` or `INSUFFICIENT_FUNDS` answer removes the freshly signed hash at once. A retrying gateway answers \"nonce too low\" about the transaction it just delivered once it mines, and a lagging node answers \"insufficient funds\" while another node holds it. The hash is lost, the run stops with `NONCE_MOVED`, and the step ends up in `notInRecord` with its gas missing. A reverted one also escapes `failed`. The fix is to poll for the hash's own receipt before dropping it on `NONCE_EXPIRED`, and to confirm the balance over two reads before dropping on `INSUFFICIENT_FUNDS`.\n- **Low, line 244.** `meetSeed` decides \"no node holds it\" from a single null `eth_getTransactionByHash`, while every other absence decision on a resume takes several passes. On a load-balanced RPC that null is routine for a transaction another node holds, so a stale-gate repeat of a step can be broadcast at the nonce of a live transaction. If it wins, `addPaymentToken` or `setRevealCommit` re-executes, reverts, and burns gas. The fix is to require `visibleReads` consecutive null answers, and to treat a pending nonce count above `nonce` as held.\n- **Info, line 245.** The revert exception in `meetSeed` is keyed on the step name alone. A step that reverted once and later succeeded under a lost hash passes the exception on any later resume and outbids a live transaction of a later step.\n\n**Questions that came back clean.** The refusal words list covers geth, reth, Erigon, Nethermind and Besu. Nethermind's texts are \"transaction underpriced\", \"replacement transaction underpriced\", \"nonce too low\" and \"already known\". Besu's are \"Replacement transaction underpriced\", \"Known transaction\", \"Nonce too low\" and \"Gas price below configured minimum gas price\". Besu's \"Upfront gas cost exceeds account balance\" is unmatched, which keeps the hash, the safe direction. The ceiling branch never repeats a bid and every bid is at least 10% above the last on both fee fields. `recordedReceipt` only drops an entry on three consecutive answers showing another block without the hash. `plan + steps + failed` stays a lower bound for every record shape I could construct, with the stale-low read ending in a refused send and no gas. The `chainState` code check only stops `accept` or `open` when the local artifact is not the reviewed build or the record's `codeHash` is wrong, both intended. I found nothing in audit 8b5de2de left unfixed.\n\n**Limits.** No test was run, so the reproductions are described, not executed. The findings are reachable only through the gateway behaviours the README names as its threat model. On a single well-behaved node none of them triggers.","treeHash":null,"usage":{"cachedInputTokens":1505022,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":55519,"runtime":"claude","turns":26,"wallClockMs":821950}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"6208734cdf5317a1","findings":[{"citation":"resolved","description":"Review question 1. Since Submission 14 a hash is pushed into `hashes` at line 261 before `broadcastTransaction`, and line 269 removes it only when the same attempt's error is a refusal (`fresh&&refusedSend(e)`). A broadcast that fails for any other reason before any node received the bytes (a 502, a connection reset, ethers' own `getBlockNumber` failing inside `broadcastTransaction`, or 'the returned hash did not match') leaves a hash in `hashes` that no node holds. That phantom hash is then used as evidence at line 277: `!hashes.length` is false, so the spent-nonce stop `NONCE_MOVED` is never thrown, even though the design (comment at lines 275-276 and README section 3, item 5) says a stale low read 'sends at a spent nonce, which a node refuses (NONCE_MOVED)'. The loop instead runs all `attempts` (8 on mainnet), each time signing a fresh transaction, having it refused as 'nonce too low', and sleeping `pollMs*3`, and ends in `STEP_STUCK <step>: NONCE_EXPIRED` with the phantom hash written into `pending`. On the following resume, line 207 sees `s.hashes.length` and a count above `s.nonce` and waits the full `stepTimeoutMs` (5 minutes) for a receipt that cannot exist before moving on. The same phantom also forces the `TIP_CEILING` branch at line 273 into `sent=true` (5-minute waits per attempt) when `placed` is null. No gas is spent and the run still ends correct, so this is Low, but the stop the README names does not happen and the operator loses about 6 minutes plus a misleading error.","line":277,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"State: a resume record whose last step landed under a hash the record does not hold (the README's finding-2 gateway case, or a Submission 13 record), so plan+steps+failed is one below the real count; the balancer answers eth_getTransactionCount one below the real count for the first reads (the test 'audit 8b5de2de finding 2: a send a node refuses leaves no hash behind' scripts exactly this, with `COUNT` returning n-1 until the first eth_sendRawTransaction). Add one thing to that script: make the FIRST eth_sendRawTransaction of the stale step answer a transport error (throw Object.assign(Error('server response 502'),{code:'SERVER_ERROR'}) from the provider's broadcastTransaction without delivering the bytes), and let every later send answer the real node's 'nonce too low'. Expected (per README section 3 item 5 and the existing test): the run stops with NONCE_MOVED and `wrote.pending===undefined`. Actual: attempt 1 pushes H1 and fails with SERVER_ERROR (not a refusal, H1 kept); attempt 2 signs H2 (or H1 again when fees are flat, `fresh=false`), the node answers NONCE_EXPIRED, only a fresh H2 is spliced, `hashes=[H1]`, so line 277 is skipped; attempts 3..8 repeat; the run throws 'STEP_STUCK <step>: NONCE_EXPIRED (nonce too low)' and the checkpoint has `pending.hashes=[H1]` at the spent nonce. Running the resume on that record: line 207 waits stepTimeoutMs (5 min on mainnet) on H1 before continuing. Fix: when the answer is NONCE_EXPIRED, ask `provider.getTransaction(h)` for each remaining hash (as the TIP_CEILING branch at line 272 already does) and drop those no node holds before testing `!hashes.length`; equivalently, track per-hash whether a broadcast ever returned success and only let placed hashes block NONCE_MOVED.","severity":"low","snippet":"    if(e.code==='NONCE_EXPIRED'&&!hashes.length&&await stableNonce()>nonce){checkpoint(state());throw Error('NONCE_MOVED '+name+': the chain is past nonce '+nonce+'. Run the resume again');}","title":"send(): a signed hash no node ever held survives a 'nonce too low' answer and shields NONCE_MOVED; the next resume then waits stepTimeoutMs on it"},{"citation":"resolved","description":"Review question 3 (can a live transaction leave the record) and 4 (is plan+steps+failed right). In the no-receipt branch of `seedLanded` (lines 221-227) the nonce is raised to the highest count read. If the count passed the seed's nonce by more than one, the entries of `later` whose nonce is now below `nonce` are dropped by the `state()` filter at line 139 (`s.nonce>=nonce`) at the `checkpoint(state())` on line 229, and `trimSeed()` discards them at the next gate. Nobody fetched their receipts: the resume block only looked at them once (line 206, `pollMs*5`) before the 5-minute wait on the seed at line 216. A later transaction that the record held by hash and that landed in that window is therefore recorded nowhere: it is not in `steps`, its gas is not in `totalGas`, and it only reappears as a name in `notInRecord`. README section 5 accepts a mined entry leaving the record only after a reorg with a lagging receipt backend; this path needs no reorg. The gates still prevent a resend, so the run ends correct and the severity is Low.","line":226,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"State: resume record {steps: s0..s2 (nonces p..p+2), pending:{name:X, nonce:p+3, hashes:[hX]}, queued:[{name:Y, nonce:p+4, hashes:[hY]}]} where X landed at p+3 under a hash not in the record (hX itself is a resend no node mined) and hY is in a node's pool. Node answers: eth_getTransactionCount returns p+3 (stale) for every read in the resume block (lines 207 and 211) and for the first reads of the seedLanded loop, then p+5; eth_getTransactionReceipt(hX) is always null; X's gate (`revealCommit()` etc.) answers done; hY is mined at p+4 after the resume block's single 10-second look at line 206 and before the seedLanded count loop returns (the loop waits stepTimeoutMs on hX first at line 216, so a 5-minute window). Expected: Y is recorded in `steps` with hY's receipt and its gasUsed, as the resume block would have done one minute earlier. Actual: line 226 sets nonce=p+5, `nextSeed()` makes Y the seed, `checkpoint(state())` writes a record without Y (filter `s.nonce>=nonce` is false), Y's gate at line 345/347/349 returns done after `trimSeed()` discarded the seed, and the final record has Y only in `notInRecord` with totalGas short by Y's gas. Fix: in the else branch of `seedLanded`, before `nextSeed()`, run the same receipt lookup the resume block does (lines 205-209) over every `later` entry whose nonce is below the new `nonce`, and `record` those found; only then drop them.","severity":"low","snippet":"   nonce=Math.max(n,planNonce+steps.length+failed.length);","title":"seedLanded(): when the count jumps past a queued later transaction, that transaction leaves the record unrecorded although its hash was known and its receipt is available"},{"citation":"resolved","description":"Review question 7. The new code check compares the chain's code hash with `d.codeHash` without first checking that the record has one. Final records written by the Submission 13 and earlier tools, and the dry-run records, carry no `codeHash` (README section 6 says the Sepolia rehearsal record 'had its codeHash added by hand from the chain'). For such a record the comparison is `'0x…' !== undefined`, always true, and every mode including `status` stops with 'CODE_NOT_RECORDED_BUILD the code at … is not the code the record names', which tells the operator the chain's code is wrong when the record simply predates the field. The mainnet record this tool will write always has the field, so the impact is operator confusion on older records, not a wrong accept or open. The fix is one line: `if(!d.codeHash)throw Error('RECORD_HAS_NO_CODE_HASH: add codeHash from the deployment read-back')` before line 109, or fall back to the `runtimeMatches` check alone with a warning.","line":109,"path":"Submission14/tools/safe-batch.mjs","reproduction":"Input: `node safe-batch.mjs status <record>` where the record is any valid deployment record JSON without a `codeHash` key (delete the key from a record produced by the dry run, or use a Submission 13 final record). Expected: either the status output or an error naming the missing field. Actual: `holds()` retries `chainState` 10 times (about 40 seconds) and then throws 'CODE_NOT_RECORDED_BUILD the code at 0x… is not the code the record names'. The unit test 'audit 8b5de2de finding 8' only covers a wrong hash, not a missing one.","severity":"info","snippet":" if(ethers.keccak256(code)!==d.codeHash)throw Error('CODE_NOT_RECORDED_BUILD the code at '+d.contract+' is not the code the record names');","title":"chainState() reports a record that has no codeHash as CODE_NOT_RECORDED_BUILD, which blocks status/accept/open on every record written before Submission 14 with a misleading cause"}],"hash":"581e8dfde7108e6c7add6a5a8d4802fee3e9ec8362801ba209a400bf9c24ad99","nodeId":"5287327c-c468-486d-b466-e47697c8f752","outcome":"completed","summary":"The review is complete and the findings file is written. The shell sandbox failed to initialize for the whole session, so nothing was executed; every conclusion comes from reading the code, the diff, the README and the Nethermind sources.\n\n**Result: three findings, none above Low.** The eight audit-8b5de2de fixes are present and do what section 2 claims. Written to `.imd-findings.json`:\n\n- **Low, deploy-mainnet.mjs line 277.** A hash signed and pushed before a broadcast that fails without reaching any node (a 502, a reset, ethers' own block-number read failing) is kept. It then makes `!hashes.length` false, so a later \"nonce too low\" never raises `NONCE_MOVED`. The run burns all eight attempts, ends in `STEP_STUCK: NONCE_EXPIRED`, checkpoints the phantom hash, and the next resume waits the full 5-minute step timeout on it at line 207. No gas is lost. Fix: on `NONCE_EXPIRED`, drop hashes that `getTransaction` shows no node holds, as the `TIP_CEILING` branch already does.\n- **Low, deploy-mainnet.mjs line 226.** In `seedLanded`'s no-receipt branch, when the count jumps two past the seed, a `queued` entry that landed during the 5-minute wait is dropped by the `state()` filter without its receipt ever being fetched. It ends in `notInRecord` with its gas missing from `totalGas`. This needs no reorg, unlike the case section 5 accepts. Fix: look up `later` receipts before `nextSeed()`, as the resume block does.\n- **Info, safe-batch.mjs line 109.** A record without `codeHash` (any pre-Submission-14 record, or the dry-run output) fails every mode with `CODE_NOT_RECORDED_BUILD`, which blames the chain's code rather than the missing field.\n\n**Questions checked with no defect found.** Refusals cannot remove a held hash because a held hash is never `fresh`. The words list covers geth, reth, Erigon, Besu and current Nethermind, whose master source uses geth's exact strings. A resume cannot loop without progress unless a node is permanently stale. The ceiling branch cannot repeat a bid, and its last bid satisfies the 10% rule on both fee fields. `recordedReceipt` cannot stick on a real reorg because a moved transaction still answers a receipt. `chainState` cannot refuse a valid accept or open beyond the missing-field case above.\n\n**Not verified.** No test was run and no proof file was written, since the sandbox refused every command and the findings are in JavaScript tooling rather than Solidity. Older Nethermind releases answered with result names like `ReplacementNotAllowed`, which the regexes would miss. I could not pin the version where the wording changed.","treeHash":null,"usage":{"cachedInputTokens":1218504,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":54988,"runtime":"claude","turns":15,"wallClockMs":799703}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a4e285247c6f8aed","findings":[{"citation":"resolved","description":"refusedSend() (line 30) counts NONCE_EXPIRED as \"the node holds no such thing\", and line 269 removes the hash this attempt added. That is wrong for the case section 4 item 1 of AUDIT_README asks about: a gateway that retries internally. A retried eth_sendRawTransaction of a transaction the node already mined is answered \"nonce too low\" by geth, reth, Erigon and Nethermind (the mined transaction is no longer in the pool, so the validation reaches the nonce check), and ethers maps that to NONCE_EXPIRED. The hash removed is the one that landed.\n\nWhen it is the first attempt's hash, hashes becomes empty and line 277 ends the run as NONCE_MOVED; the resume finds the step on the chain and lists it in notInRecord (a lost gas entry, but the run ends right). When it is a replacement (attempt 2 or later), the earlier hash h1, replaced and never to be mined, stays in hashes, so line 277 never fires: every remaining attempt signs at the same nonce, is answered NONCE_EXPIRED again, and waits pollMs*3; the step ends in STEP_STUCK with pending {hashes:[h1]}. On the resume, the pending entry has no receipt and stableNonce() > its nonce, so line 207 waits the full stepTimeoutMs (5 minutes) on a hash that can never land, then drops it; the gate finds the step done and the step is reported in notInRecord with its gas missing from totalGas. If the mined replacement reverted (status 0), the failed entry is never recorded either, and planNonce+steps+failed is one short for every later resume (covered only by the nonce read).\n\nThis is a non-reorg way for a mined hash to leave the record, which section 5 lists only for two reorg cases. Nothing is sent twice (the nonce is spent), so the cost is time and an incomplete record.\n\nFix: on NONCE_EXPIRED do not treat the fresh hash as refused before looking for it: poll provider.getTransactionReceipt(own) (or getTransaction) over visibleReads passes and keep own when it is found; and when every hash of the step has been answered \"nonce too low\" and stableNonce() > nonce while firstReceipt(hashes, pollMs*visibleReads) finds nothing, stop as NONCE_MOVED even though hashes is not empty, instead of burning the remaining attempts.","line":269,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"State: step \"setRevealURIs (not locked)\" at nonce n. Attempt 1 broadcasts h1 (tip T), taken but not mined within stepTimeoutMs (automine off in the harness of tests/contract/deploy-mainnet-cli.test.mjs). Attempt 2 signs h2 at tip 1.25T+1. Wrap provider.broadcastTransaction so that for h2 it calls the real broadcast, mines it (evm_setAutomine true + evm_mine), then throws Object.assign(Error('nonce too low'),{code:'NONCE_EXPIRED'}) (what ethers returns for the gateway's retried send). Script eth_getTransactionReceipt(h1) = null (it was replaced).\nExpected: h2 stays in the record (the node took it, it is on the chain) and the step is recorded with hash h2, as the finding 2 test asserts for a lost reply.\nActual: line 269 removes h2; hashes=[h1]; attempts 3..8 each sign at nonce n, get NONCE_EXPIRED, remove their hash and wait pollMs*3; deployMainnet rejects with \"STEP_STUCK setRevealURIs (not locked): NONCE_EXPIRED\"; the last checkpoint has pending.hashes=[h1]. A resume with that record spends stepTimeoutMs at line 207 on h1, then skips the step: d.notInRecord = ['setRevealURIs (not locked)'] and d.totalGas lacks its gas. With h2 made to revert instead (gasLimit 30_000 via lowGasOnce), d.failed never lists it.","severity":"low","snippet":"    if(fresh&&refusedSend(e))hashes.splice(hashes.indexOf(own),1);","title":"send(): a \"nonce too low\" answer about a replacement the node just mined removes the mined hash; the stale first hash then blocks NONCE_MOVED, every remaining attempt is burned, and the resume waits s"},{"citation":"resolved","description":"AUDIT_README section 2 finding 3 says the Nethermind words were taken from \"the result names\" only, and section 7 says its constants were not read. Read on 2026-11-04: at release 1.31.0, src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs defines FeeTooLow = new(3, nameof(FeeTooLow)), FeeTooLowToCompete = new(4, nameof(FeeTooLowToCompete)), ReplacementNotAllowed = new(11, nameof(ReplacementNotAllowed)), AlreadyKnown = new(1, nameof(AlreadyKnown)), InsufficientFunds = new(6, nameof(InsufficientFunds)); only OldNonce is \"nonce too low\". EthRpcModule.SendTx returns acceptTxResult.ToString() (Code, or \"Code, Message\") as the JSON-RPC error message, so the error text is \"FeeTooLow\", \"ReplacementNotAllowed\", \"AlreadyKnown\" or \"InsufficientFunds\": no spaces. The regexes on lines 26 and 28 need \"fee too low\", \"could not replace\", \"already known\"; ethers maps INSUFFICIENT_FUNDS only on /insufficient funds/. So on such a node a bid refused as too low does not raise the next bid (last stays), a held transaction is not recognised as held, and an insufficient-funds refusal keeps the hash and is watched. Nothing is sent twice and no gas is spent; the step ends in STEP_STUCK after the attempts, and a resume behaves the same while the node answers that way, so a run behind a load balancer with such a node can stall on a held bid whose fee the record does not know (fee null, or a bid placed by an earlier run).\n\nFrom release 2.0.0 (published 2026-09-22) and in 2.2.2 (current, 2026-10-26) the same fields carry TxPoolErrorMessages constants: \"transaction underpriced\", \"replacement transaction underpriced\", \"already known\", \"nonce too low\" and TxErrorMessages.InsufficientFundsForGas, which the lists match. The gap is only for nodes still on 1.x. Fix: add /FeeTooLow|ReplacementNotAllowed/ to bidTooLow, /AlreadyKnown/ to mayHoldBid and /InsufficientFunds/ to refusedSend (ethers' own INSUFFICIENT_FUNDS mapping misses the bare name too), and record in section 7 that the 1.x strings were read.","line":26,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Using the finding 3 test shape in tests/contract/deploy-mainnet-cli.test.mjs: script eth_sendRawTransaction to throw Error('ReplacementNotAllowed') for the first three sends after a placed bid with fee null in the record (e.g. resume with pending {name:'setRevealCommit', nonce:n, hashes:[fake], fee:null} and provider.getTransaction(fake) = {}).\nExpected (the finding 3 contract): each refused bid is higher than the last. Actual: bidTooLow('ReplacementNotAllowed') is false, last stays null, the same market bid is sent each attempt and tips[i] === tips[i-1]; with the refusal persisting, the step ends STEP_STUCK and a resume repeats it. Same with Error('FeeTooLow') (bid not raised) and Error('AlreadyKnown') (not recognised as held: with placed null the bid is not raised either).","severity":"info","snippet":"const bidTooLow=e=>e.code==='REPLACEMENT_UNDERPRICED'||/underpriced|insufficient gas price to replace|below .*minimum|fee too low|could not replace/i.test(String(e.message));","title":"Refusal words: Nethermind before 2.0.0 answers the bare result names (FeeTooLow, ReplacementNotAllowed, AlreadyKnown, InsufficientFunds), which bidTooLow/mayHoldBid/refusedSend do not match"}],"hash":"b3f8c6de1a5e80610c5bbcb2f2baa40cdf179b99b285e475ba49d92604f6e2fc","nodeId":"80ed352b-d510-4661-827b-4e930ad4cda9","outcome":"completed","summary":"Review of Submission14 is complete. Two findings are recorded in `.imd-findings.json`, one Low and one Info. No contract code changed, so no Foundry proof applies. The deployment tool could not be executed here because `runtime.mjs` and the private artifacts file are not in the package, so reproductions are stated against the project's own test harness rather than run.\n\n**Findings**\n\n- **Low, `send()` line 269.** A \"nonce too low\" answer about a replacement the node just mined is counted as a refusal, and the mined hash is removed from the record. When it is a replacement bid, the earlier, replaced hash stays in `hashes`, so the NONCE_MOVED exit on line 277 never fires. Every remaining attempt burns, the step ends in STEP_STUCK, and the resume waits the full step timeout on a hash that can never land before skipping the step into `notInRecord`. A reverted replacement never reaches `failed`. This is a non-reorg way for a mined hash to leave the record, beyond the two reorg cases section 5 accepts. Fix: look for the hash's receipt before dropping it on NONCE_EXPIRED, and stop as NONCE_MOVED when the count has moved and no hash has a receipt.\n- **Info, refusal words line 26.** Nethermind 1.x answers bare names such as `FeeTooLow`, `ReplacementNotAllowed`, `AlreadyKnown` and `InsufficientFunds`, which none of the three regexes match. I read the 1.31.0 source and the 2.0.0 and 2.2.2 sources: from 2.0.0, published 2026-09-22, the strings are geth-compatible and matched. On an older node the bid is not raised and the run stalls in STEP_STUCK; nothing is sent twice.\n\n**Questions answered without a defect**\n\n- Signing first: a never-held hash only delays decisions, never causes a second send. A refusal removes only the hash its own attempt added.\n- `queued` and `meetSeed`: the paths where a live transaction leaves the record are the reorg cases section 5 already accepts. A step lands twice only in the accepted reorg case.\n- `floor` at 0: a stale low read sends at a spent nonce, a fresh node refuses it and no gas is spent. `plan + steps + failed` holds for every record the tool writes, with the deep reorg case accepted in Submission 13.\n- `recordedReceipt`: a real reorg that moved the transaction yields a receipt; one that evicted it yields null after three passes. Sticking needs the block read to fail persistently, which is a change of RPC, not a defect.\n- Fees: the ceiling bid raises both tip and fee cap by at least 10 percent over the last, keeps fee cap at or above tip, and cannot be repeated. Repeated identical bids only occur after a `last=null` reset and cost nothing.\n- `chainState`: the code check compares lowercase keccak values from the same tool and retries lagging reads, so it does not stop a valid accept or open.\n\n**Not verified**: Besu's wording is from memory per the README. I checked Besu's current `RpcErrorType` and its strings \"Replacement transaction underpriced\", \"Known transaction\", \"Nonce too low\" and \"Gas price below configured minimum gas price\" are all matched. \"Gas price below current base fee\" and \"Upfront gas cost exceeds account balance\" are not matched, but both keep the hash and fail safe.","treeHash":null,"usage":{"cachedInputTokens":1400804,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":66407,"runtime":"claude","turns":27,"wallClockMs":965704}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5950526089333555","findings":[],"hash":"c21bcce2a5d6b0efb2654d2be6783767cf82b581191087438ca4b4c3759580b9","nodeId":"5287327c-c468-486d-b466-e47697c8f752","outcome":"failed","summary":"API Error: Your account can't receive replies from Claude right now. You can find the next step in the Anthropic Verification Portal: https://portal.anthropic.com/…","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":3911}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"81314195390981c7","findings":[{"citation":"resolved","description":"Merged from audit_permissions #1, audit_math #1 and audit_flow #1 (same line, same root cause). refusedSend (line 30) counts e.code==='NONCE_EXPIRED' and 'INSUFFICIENT_FUNDS' as proof that no node holds the transaction, and line 269 then splices the hash this attempt signed. AUDIT_README section 4 item 1 names the case where that proof fails: a gateway that retries internally delivers the raw transaction, loses the reply, retries, and gets 'nonce too low' because the first copy is already mined. ethers maps any 'nonce ... too low' text to NONCE_EXPIRED. Nothing calls getTransaction or getTransactionReceipt on own before splicing it, so nothing recovers. (a) First attempt of a step: hashes becomes [], so at line 277 stableNonce()>nonce is true and the run throws NONCE_MOVED, and checkpoint(state()) writes no pending entry and no hash. The resume skips the step by its gate. The step then shows up in notInRecord with its gas left out of totalGas, and the deploy step loses its creation hash. If the delivered transaction reverted (setRevealCommit), record() never adds a failed entry, so planNonce+steps+failed comes out one short on every later resume. The meetSeed exception at line 245 is then also unavailable to that step. (b) Replacement attempt (a>=1): the earlier hash h1, which was replaced and will never mine, stays in hashes. Line 277 therefore never fires. Attempts a+1..7 each sign at the spent nonce, get NONCE_EXPIRED, splice their own hash and wait pollMs*3. The step ends in STEP_STUCK with pending.hashes=[h1]. On the resume, line 207 sees stableNonce()>s.nonce and waits the full stepTimeoutMs (5 min) on h1 before dropping it, and the step again ends in notInRecord. INSUFFICIENT_FUNDS from a lagging node behind a fan-out gateway has the same effect: the held hash is dropped, and it is only re-added if the fee is unchanged and the same bytes are signed again. Nothing is sent twice, because the nonce is spent, so the cost is an incomplete record, lost gas accounting and time. This is a non-reorg way for a mined hash to leave the record, which section 5 does not list.","line":269,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Traced by hand against the code. Bash would not start in this sandbox, so no harness was run. Use the harness of tests/contract/deploy-mainnet-cli.test.mjs. Wrap provider.broadcastTransaction so that for the setRevealURIs raw transaction it calls the real broadcast, mines it (automine), then throws Object.assign(Error('nonce too low'),{code:'NONCE_EXPIRED'}). Expected, per README section 2 finding 2 ('anything else keeps it, and it is watched') and the TIMEOUT/SERVER_ERROR test of finding 2: the step is recorded with the pre-computed hash, d.steps.length===6, d.notInRecord deepEquals []. Actual: line 269 splices own, hashes.length===0, line 277 throws 'NONCE_MOVED setRevealURIs (not locked)', and the checkpoint has no pending entry. --resume finishes with notInRecord ['setRevealURIs (not locked)'] and totalGas short by that step's gas. Variant (b): automine off, so attempt 1's h1 is pooled and not mined. Attempt 2's h2 is broadcast for real, mined, then answered NONCE_EXPIRED, and eth_getTransactionReceipt(h1)=null. Actual: 'STEP_STUCK setRevealURIs (not locked): NONCE_EXPIRED' with pending.hashes=[h1], and the resume spends stepTimeoutMs at line 207. With lowGasOnce on h2 (a status-0 receipt), d.failed never lists it.","severity":"low","snippet":"if(fresh&&refusedSend(e))hashes.splice(hashes.indexOf(own),1);","title":"send(): a 'nonce too low' (or 'insufficient funds') answer about bytes that were delivered removes the landed hash, so the step loses its hash and gas, a revert loses its failed entry, and a replaceme"},{"citation":"resolved","description":"From audit_economics #1. Since hashes are pushed at line 261 before broadcastTransaction, hashes may hold a hash that no node ever received: a 502, a reset, or ethers' internal getBlockNumber failing inside broadcastTransaction is not a refusal, so the hash is kept. README section 3 item 5 says a stale low read 'sends at a spent nonce, which a node refuses' (NONCE_MOVED). Line 277 checks only !hashes.length, though. One phantom hash keeps the stop from firing: every later attempt signs at the spent nonce, gets NONCE_EXPIRED (only its own fresh hash is spliced), and waits pollMs*3. The run ends with 'STEP_STUCK <step>: NONCE_EXPIRED' and pending.hashes=[phantom]. The next resume's line 207 sees stableNonce()>s.nonce and waits the full stepTimeoutMs on a hash that cannot have a receipt. The TIP_CEILING branch at line 272 already asks getTransaction for each hash; line 277 does not. No gas is spent and the run ends correct. The cost is about 6 minutes and a misleading error, in exactly the stale-read case the floor-at-0 change relies on this stop for.","line":277,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Traced by hand (no harness run; Bash unavailable). Start from the test 'audit 8b5de2de finding 2: a send a node refuses leaves no hash behind'. Its record is one short of the real count, and eth_getTransactionCount answers n-1 until the first send. Change the first eth_sendRawTransaction of the stale step to throw Object.assign(Error('server response 502'),{code:'SERVER_ERROR'}) without delivering. Every later send gets the node's real 'nonce too low'. Expected: NONCE_MOVED and wrote.pending===undefined, as in the existing test. Actual: H1 is kept (not a refusal). Attempts 2..8 get NONCE_EXPIRED, hashes stays [H1], line 277 is skipped, the error is 'STEP_STUCK <step>: NONCE_EXPIRED (nonce too low)' and the checkpoint has pending.hashes=[H1]. The resume on that record waits stepTimeoutMs at line 207.","severity":"low","snippet":"if(e.code==='NONCE_EXPIRED'&&!hashes.length&&await stableNonce()>nonce){checkpoint(state());throw Error('NONCE_MOVED '+name+': the chain is past nonce '+nonce+'. Run the resume again');}","title":"send(): NONCE_MOVED needs hashes to be empty, so a hash no node holds (a send that never left, or a replaced one) suppresses the stop on a spent nonce: all attempts are used up, then STEP_STUCK, and t"},{"citation":"resolved","description":"Merged from audit_permissions #2 and audit_flow #2. RESUME_OUT_OF_ORDER (line 245) is the guard against a stale gate sending a done step at the nonce of another step's waiting transaction. Line 244 bypasses it on a single null answer per hash. Every other 'absent' decision in this file needs repeated answers (notDone uses visibleReads, codeAbsent uses 10 reads, recordedReceipt uses visibleReads block answers), because one request reaches one node. A pending transaction is answered from the pool of whichever node is asked, and a node that has not received it by gossip answers null. The step then goes out at market (last stays null). If that bid is >=110% of the waiting one, or after the refusals have raised last by 25% per attempt, it replaces the later step's transaction. A done step then runs again on chain: setRevealCommit and addPaymentToken revert, burning gas and adding a spurious failed entry. The displaced step has to be resent at the next nonce. Preconditions: a record one short (e.g. after the line-269 finding), a stale gate over visibleReads reads, and a null from a node that lacks the pooled transaction.","line":244,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Traced by hand (no harness run). Take the second half of the existing test 'audit 8b5de2de review: a step whose tries reverted goes out at the nonce of a later waiting transaction': a record with queued [{name:'addPaymentToken(USDC, 1600 GOLD)', nonce:n, hashes:[fake], fee:FAKE_FEE}] and no failed setRevealCommit entry. That test expects RESUME_OUT_OF_ORDER when provider.getTransaction(fake) returns {}. Make it return {} on odd calls and null on even calls, so that meetSeed's call gets null. Expected: RESUME_OUT_OF_ORDER setRevealCommit. Actual: line 244 returns, and setRevealCommit is signed and broadcast at nonce n (srv.state.sent[0].nonce===n, market tip), racing the queued addPaymentToken. With a real pending addPaymentToken at n and a stale revealCommit() read, the repeated setRevealCommit can mine and revert (previous commit mismatch), and addPaymentToken is resent at n+1.","severity":"low","snippet":"if(!(await Promise.all(seed.hashes.map(h=>provider.getTransaction(h).catch(()=>true)))).some(Boolean))return;","title":"meetSeed takes one null getTransaction answer per hash as 'no node holds it' and sends a stale-gate step over another step's live transaction"},{"citation":"resolved","description":"Merged from audit_permissions #3 and audit_flow #3. The exception is meant for 'this step's own last try having reverted' (comment at line 240). It tests only whether some failed entry carries the step's name. record() (line 142) stores no nonce, and failed entries are never removed except by the reorg drop. setRevealCommit runs with revertRetries:2, so a revert at n followed by a success at n+1 is a normal path. If that success is not in steps (its hash was lost as in the line-269 finding, or seedLanded found no receipt) and the gate reads stale over visibleReads, the next step's held transaction at n+2 meets the exception. last=feeOf(seed.fee) is set, the guard is skipped, and the stale step bids 125% over and replaces the live transaction. Mined, the repeat reverts (REVEAL_COMMIT_INVALID: the previous commit no longer matches), burning gas and adding a second failed entry. The displaced step is resent.","line":245,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Traced by hand (no harness run). Record: planNonce p, with steps deploy@p and setRevealURIs@p+1, failed [setRevealCommit (status 0) @p+2]. setRevealCommit succeeded at p+3 under a hash not in the record. queued [{name:'addPaymentToken(USDC, 1600 GOLD)', nonce:p+4, hashes:[h], fee:{maxPriorityFeePerGas:'1200000000', maxFeePerGas:'500000000000'}}], with h pending in the pool. eth_getTransactionCount answers p+4, and revealCommit() answers 0x0 for the first visibleReads reads (lagging node). Expected: RESUME_OUT_OF_ORDER setRevealCommit. Actual: notDone returns true, meetSeed sees h held, line 245 finds the old failed entry, setRevealCommit is signed at nonce p+4 with tip >=1.5e9+1 and fee cap 625e9+1 and replaces h, and once mined it reverts.","severity":"low","snippet":"if(!failed.some(f=>f.name===name))throw Error('RESUME_OUT_OF_ORDER '+name+' at the nonce of '+seed.name+' ('+seed.hashes.join(',')+'): run the resume again');","title":"meetSeed's revert exception matches any failed entry with the step's name, including one followed by a success the record lost, so a stale gate outbids the next step's live transaction"},{"citation":"resolved","description":"From audit_economics #2. In the no-receipt branch, nonce is raised to the highest count read. If the count passed seed.nonce by more than one, every later entry below the new nonce disappears. nextSeed() promotes it, the state() filter at line 139 (s.nonce>=nonce) leaves it out of the checkpoint at line 229, and trimSeed() in its own notDone discards it. Its isDone then returns true and, since seed?.name no longer matches, seedLanded is not called. Its receipt was only looked for once in the resume block (line 206, pollMs*5), before the stepTimeoutMs wait on the seed at line 216. A later transaction that landed during that wait is therefore never recorded. It is missing from steps (or from failed, if it reverted, which makes planNonce+steps+failed one short) and only shows up in notInRecord. Section 5 accepts a live hash leaving the record only after a reorg. This path needs none. The gates prevent a resend, so the run ends correct.","line":226,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Traced by hand (no harness run). Resume record: steps at p..p+2, pending {name:'setRevealCommit', nonce:p+3, hashes:[hX]} with hX never mined (the commit landed at p+3 under a lost hash), and queued [{name:'addPaymentToken(USDC, 1600 GOLD)', nonce:p+4, hashes:[hY]}] with hY pooled. eth_getTransactionCount answers p+3 during the resume block, then p+5. hY mines after the 10-second look at line 206. Expected: addPaymentToken recorded in steps with hY's receipt and gas. Actual: in seedLanded, firstReceipt(hX) waits stepTimeoutMs and returns null. n becomes p+5 and nonce=p+5, nextSeed() makes Y the seed, checkpoint(state()) omits it, notDone('addPaymentToken...') trims it and returns false. The final record has addPaymentToken in notInRecord, addBlock is undefined, and totalGas is short.","severity":"low","snippet":"nonce=Math.max(n,planNonce+steps.length+failed.length);","title":"seedLanded(): when the count jumps past a queued later transaction, that transaction is dropped from the record without its receipt being read, although its hash is known"},{"citation":"resolved","description":"From audit_math #2. README section 2 finding 3 says only Nethermind's result names were read. Those names are written without spaces, and the regexes need spaces: /fee too low/ does not match 'FeeTooLow', there is no pattern for 'ReplacementNotAllowed', and /already known/ on line 28 does not match 'AlreadyKnown'. Nethermind 1.x returns the result name as the JSON-RPC error message (later releases use geth-style text). ethers maps NONCE_EXPIRED and INSUFFICIENT_FUNDS only from the spaced phrases, so 'OldNonce' and 'InsufficientFunds' are not refusals either. Effects on such a node: a bid refused as too low does not raise last, so the same market bid repeats; a held bid is not recognised; a refused hash is kept; and line 277's NONCE_MOVED never fires. The step ends in STEP_STUCK. Nothing is sent twice and no gas is spent. The code-level mismatch is certain. That Nethermind 1.x returns these exact strings is the specialist's reading and was not re-checked here.","line":26,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Evaluate the predicates from lines 26, 28 and 30 directly. bidTooLow({message:'FeeTooLow'}) is false, bidTooLow({message:'ReplacementNotAllowed'}) is false, and mayHoldBid({message:'AlreadyKnown'}) is false. Expected for a bid refusal: true, so that the next bid is raised. In the finding 3 test shape (resume with pending {name:'setRevealCommit', nonce:n, hashes:[fake], fee:null}, getTransaction(fake)={}), make eth_sendRawTransaction throw Error('ReplacementNotAllowed'). tips[i]===tips[i-1] on every attempt, then STEP_STUCK.","severity":"info","snippet":"const bidTooLow=e=>e.code==='REPLACEMENT_UNDERPRICED'||/underpriced|insufficient gas price to replace|below .*minimum|fee too low|could not replace/i.test(String(e.message));","title":"Refusal words do not match Nethermind 1.x's bare AcceptTxResult names (FeeTooLow, ReplacementNotAllowed, AlreadyKnown, OldNonce, InsufficientFunds)"}],"hash":"f70e84f6b32529f21d13e8edd9a5699bd479552b84222912ecee9ee442b64498","nodeId":"5d65a671-9f72-41bd-80d4-857f91234ea4","outcome":"completed","summary":"The review is finished: I kept 6 findings in `.imd-findings.json` (5 low, 1 info) and dropped 1 specialist finding. I couldn't run anything, though. Bash would not start in this sandbox (the bridge sockets failed), so I couldn't run Forge or Node, or even check that the JSON file parses. Every reproduction below is a hand trace against the code, and they all target the JavaScript tools, not Solidity. None of them is a run test. No finding is high or critical, so none carries a Foundry proof.\n\n**Kept:**\n1. **Low, line 269: a \"nonce too low\" or \"insufficient funds\" reply removes a hash that actually landed.** This merges three specialists' findings. A retrying gateway can deliver the transaction and then report \"nonce too low\". The tool deletes the hash without checking whether it is on chain. If it was the step's first send, the run stops with NONCE_MOVED. The step then shows up in `notInRecord` with its gas missing from `totalGas`. If that transaction reverted, its failed entry is lost, so `plan + steps + failed` comes out one short. If it was a replacement, the older hash keeps the run from stopping: it uses up every remaining attempt, ends in STEP_STUCK, and the resume waits 5 minutes. This answers README question 1: nothing recovers from that gateway case.\n2. **Low, line 277: a hash no node ever held blocks the NONCE_MOVED stop.** That stop only fires when the step has no hashes at all. A send that failed with a 502 before reaching any node leaves its hash behind. On a stale nonce read the run then burns all 8 attempts, ends in STEP_STUCK instead of NONCE_MOVED, and the resume waits 5 minutes on that hash.\n3. **Low, line 244: `meetSeed` trusts a single empty answer.** One null from one node is taken as proof that nobody holds another step's waiting transaction. A stale step can then be sent over that live transaction, replace it, and revert on chain.\n4. **Low, line 245: `meetSeed`'s revert exception matches by step name only.** Any old failed entry for the step lets a stale repeat outbid the next step's live transaction, even if the step succeeded after that revert. Failed entries store no nonce, so the tool can't tell the cases apart.\n5. **Low, line 226: `seedLanded` drops a queued later transaction without reading its receipt.** When the nonce count jumps past it, that transaction leaves the record even though its hash is known. No reorg is needed, unlike the case section 5 accepts.\n6. **Info, line 26: the refusal patterns miss Nethermind 1.x's unspaced names** (FeeTooLow, ReplacementNotAllowed, AlreadyKnown, OldNonce). The pattern mismatch is certain from the code. That 1.x nodes return exactly these strings is the specialist's claim, and I didn't check it.\n\n**Dropped:** the claim that `chainState` blocks records without a `codeHash`. Submission 13's mainnet tool and the dry run both write `codeHash`, and no mainnet record exists yet, so no record this tool writes can hit it.\n\n**Checked, no defect found:**\n- **Fees at the ceiling (question 6):** the last bid is always at least 10% over on both tip and fee cap, and the fee cap is never below the tip. The ceiling branch cannot repeat a bid, because the next raise throws TIP_CEILING.\n- **`recordedReceipt` (question 5):** a real reorg either finds the receipt again or proves the drop. I found no other way for it to get stuck beyond the RECEIPT_NOT_VISIBLE stop it is designed to give.\n- **`chainState`'s code check (question 7):** it compares the full code, immutables included, against the hash the deploy tool records. A valid `accept` or `open` passes.","treeHash":null,"usage":{"cachedInputTokens":366093,"inputTokens":14,"model":"claude-opus-5-5","outputTokens":14104,"runtime":"claude","turns":9,"wallClockMs":144738}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a7979f925df7a720","findings":[{"citation":"resolved","description":"send() signs first, records the hash, broadcasts, and on an error for which refusedSend(e) holds removes the hash this attempt added (line 269). NONCE_EXPIRED is treated as proof that no node took the bytes. A load-balanced gateway that retries internally gives exactly the answer README section 4.1 describes: the first try delivers the transaction and its reply is lost, and the retry is answered 'nonce too low' because the first copy is already mined or pooled at that nonce. ethers maps any message containing 'nonce' and 'too low' to NONCE_EXPIRED. The hash is spliced out. On the first attempt of a step, hashes is then empty, and once stableNonce() reads past the nonce, line 277 checkpoints state() with no pending entry and throws NONCE_MOVED. The record keeps no hash of the transaction that spent the nonce. Nothing checks own with getTransaction or getTransactionReceipt before deleting it, so nothing recovers it. The resume passes only through the gate: a successful step lands in notInRecord and its gas is left out of totalGas. If the delivered transaction reverted (setRevealCommit on a low limit), its failed entry is lost, so plan+steps+failed counts one nonce short. A landed step with no recorded hash is also the precondition for the two meetSeed findings below.","line":269,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Run deployMainnet over the test RPC with a provider whose broadcastTransaction forwards the raw transaction to the node, waits for it to be mined (automine), then throws ethers.makeError('nonce has already been used','NONCE_EXPIRED') for the setRevealURIs transaction only. Expected: the step is recorded with its hash (the bytes are on chain under the hash the tool computed before broadcasting). Actual: hashes.splice removes own, hashes.length is 0, stableNonce() is past the nonce, and the run stops with 'NONCE_MOVED setRevealURIs (not locked)'. The checkpoint holds no pending entry and no hash. A --resume finishes through the gate with notInRecord:['setRevealURIs (not locked)'] and that step's gas missing from totalGas. Fix: on NONCE_EXPIRED (and, more cheaply, on every refusal) keep own while getTransaction(own) or getTransactionReceipt(own) answers on any of visibleReads reads. Only splice it when none does. A node that returns the bytes has taken them, whatever the send reply said.","severity":"low","snippet":"    if(fresh&&refusedSend(e))hashes.splice(hashes.indexOf(own),1);","title":"A refusal of a delivered transaction removes its hash: a gateway's 'nonce too low' about bytes it just delivered drops the landed step from the record"},{"citation":"resolved","description":"When this step's nonce equals the nonce of another step's waiting transaction, the code normally means the gate read was stale, and meetSeed throws RESUME_OUT_OF_ORDER (line 245). The only bypass for a step with no failed entry is line 244: if one provider.getTransaction(h) call per recorded hash returns null, the step is sent at that nonce at market fee (last stays null). Everywhere else this file asks for visibleReads consecutive answers against lag (settled, codeVisible, notDone, recordedReceipt). This one decision, which protects against running a done step twice, rests on a single answer from whichever backend serves it. A backend that has not imported the pooled transaction, or a pool that dropped it locally while others keep it, answers null. If the market bid is at least 110% of the waiting bid (fees rose since the stop), the stale step replaces the later step's transaction outright. Otherwise the refusal raises last by 25% each attempt until it does. Either way the protected-against case happens: a done step runs again and the later step's transaction is evicted.","line":244,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Record (all reachable, for example after the NONCE_MOVED case above): steps=[deploy@n0, setRevealURIs@n0+1], setRevealCommit landed at n0+2 under a hash the record lacks, queued=[{name:'addPaymentToken(USDC, 1600 GOLD)',nonce:n0+3,hashes:[hA],fee:{tip 1 gwei}}], with hA pooled. On --resume, stableNonce() is n0+3. The setRevealCommit gate gets 3 stale reads (revealCommit()==0) from a lagging backend, and the single getTransaction(hA) is answered null by that backend. Expected: RESUME_OUT_OF_ORDER, as the same test does when getTransaction returns the tx (deploy-mainnet-cli.test.mjs:824). Actual: meetSeed returns, and setRevealCommit is signed at n0+3 at market. With market tip >=1.1 gwei it replaces hA. On chain it reverts (REVEAL_COMMIT_INVALID / REVEAL_ACCEPTANCE_INVALID: the commit is already set), burning gas, and revertRetries may send up to two more if the estimate again reaches the stale node. addPaymentToken must be resent. Fix: treat the seed as unheld only after visibleReads consecutive null answers for every hash, polled pollMs apart, and re-run the step's isDone gate before sending over it.","severity":"low","snippet":"   if(!(await Promise.all(seed.hashes.map(h=>provider.getTransaction(h).catch(()=>true)))).some(Boolean))return;","title":"meetSeed takes one null getTransaction answer per hash as 'no node holds it', so a stale gate goes out over another step's live transaction"},{"citation":"resolved","description":"Section 3 says a held waiting transaction of another step at this nonce is outbid when 'this step has a failed entry'. The test is failed.some(f=>f.name===name), true for any revert of the step ever recorded, whether or not the step later succeeded. setRevealCommit runs with revertRetries:2, so a revert at n followed by a success at n+1 is the normal path, and the failed entry stays. If the success at n+1 is not in steps (its hash was removed as in finding 1, or seedLanded recorded no receipt), the gate decides on a resume, and three stale reads say not done. meetSeed then finds the next step's held transaction at n+2 and a failed setRevealCommit entry. It takes last=feeOf(seed.fee), skips the RESUME_OUT_OF_ORDER guard and bids 25% over the later step's transaction, so the replacement is accepted by design. The entries store no nonce, so the check cannot tell 'my last try reverted just below this nonce' apart from 'I reverted long ago and succeeded since'.","line":245,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Record: failed=[setRevealCommit@n (reverted)], setRevealCommit succeeded at n+1 under a hash not in steps, queued=[{name:'addPaymentToken(USDC, 1600 GOLD)',nonce:n+2,hashes:[hA],fee:F}], with hA held (getTransaction returns it). On --resume, stableNonce() is n+2, and the commit gate's 3 reads come from a lagging backend that still shows revealCommit()==0. Expected: RESUME_OUT_OF_ORDER, the step being done. Actual: last=F, and setRevealCommit goes out at n+2 with tip and fee cap 125% of F+1. It replaces addPaymentToken in the pool and, once mined, reverts (REVEAL_COMMIT_INVALID: the commit is already held), burning gas. The retries can repeat that while the estimate reaches the stale node. Fix: record the nonce in every steps/failed entry. Take the exception only when the step's newest recorded entry is a failed one at nonce-1 (no success and no later attempt of the step), or re-read isDone over visibleReads fresh reads before outbidding.","severity":"low","snippet":"   if(!failed.some(f=>f.name===name))throw Error('RESUME_OUT_OF_ORDER '+name+' at the nonce of '+seed.name+' ('+seed.hashes.join(',')+'): run the resume again');","title":"meetSeed's failed-entry exception holds for any old revert of the step, so a stale gate on a step that later succeeded outbids the next step's live transaction"}],"hash":"fdfb59ea28b81b731bd7fd2ded5e66d64b9094d05cc1c6f5a5edd6a24bbfde2b","nodeId":"e7fd5672-3481-4fe8-bb28-a7697f297663","outcome":"completed","summary":"I've written three Low findings to `.imd-findings.json`, all in `Submission14/tools/deploy-mainnet.mjs`. Nothing here was run. The tests need `tools/runtime.mjs` and the build file under `artifacts/`, and neither is in the tree, so every reproduction is traced by hand from the code, the existing tests and how ethers v6 sorts node errors. None is higher than Low, so none carries a Foundry proof. All three fail safe: they cost a wasted or reverted transaction, or a gap in the record, and cannot deploy a second contract.\n\n**Findings**\n1. **Line 269 (questions 1 and 2):** a \"nonce too low\" answer removes a hash the node actually took. A gateway that retries internally can deliver the transaction, lose the reply, and answer \"nonce too low\" on its retry. ethers reports that as a refusal, so the hash is removed, and the run stops with `NONCE_MOVED` and no hash in the record. That is how the step ends up in `notInRecord` with its gas missing. If it reverted, its `failed` entry is lost and `plan + steps + failed` counts one short. The resume only recovers through the gate. **Fix:** before removing the hash, check with `getTransaction` or `getTransactionReceipt` whether a node has it.\n2. **Line 244 (question 3):** in `meetSeed`, one `null` answer from `getTransaction` per hash counts as \"no node holds it\". That bypasses `RESUME_OUT_OF_ORDER`, so a step whose gate read was stale goes out at market over another step's live transaction. That step lands twice: a repeat `setRevealCommit` reverts and burns gas, and the evicted step has to be resent. **Fix:** require `visibleReads` consecutive `null` answers and read the gate again first.\n3. **Line 245 (question 3):** the failed-entry exception (`failed.some(f=>f.name===name)`) also holds when that revert was long ago and the step succeeded afterwards. For `setRevealCommit` (a revert, then a success the record doesn't hold, then a stale gate), the tool outbids the next step's live transaction on purpose, and the repeat commit reverts on chain. **Fix:** record the nonce in each entry and allow the exception only when the step's newest entry is a revert at the nonce just below.\n\n**Questions checked with nothing found**\n- **Refusal words (2):** Nethermind's refusals are matched. It formats them as \"FeeTooLow, transaction underpriced\" (I read its `AcceptTxResult` source), and the \"underpriced\" pattern catches that. Besu's are still from memory, as the README says.\n- **Stale nonce (4):** a send at a spent nonce gets \"nonce too low\" and costs no gas. The tool's records can only undercount `plan + steps + failed`, and `max()` with the chain's count covers that.\n- **`recordedReceipt` (5):** a real one-block reorg doesn't stick the run. A node stuck on a side fork can still drop a mined entry, which fails safe.\n- **Fees at the ceiling (6):** the ceiling bid always clears the 10% replacement rule on both fee fields, its fee cap is never under the tip, and the next bid always stops at `TIP_CEILING`, so no bid repeats.\n- **`chainState` (7):** I found no way for the code check to stop a valid `accept` or `open`. The testnet tool also writes `codeHash`.\n- **Audit `8b5de2de` (8):** apart from finding 1, which touches the fix for its finding 2, nothing came up showing a fix incomplete.\n\nI covered the access control, trust gap and asymmetry passes myself, with no helpers, and went no further: the rest of the read-back and `safe-batch.mjs` outside `chainState` only got a skim.","treeHash":null,"usage":{"cachedInputTokens":608840,"inputTokens":17,"model":"claude-opus-5-5","outputTokens":41983,"runtime":"claude","turns":11,"wallClockMs":462350}}],"verification":[]}