{"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":"14066658-72c7-46f8-a91c-8a569722919a","kind":"audit","nodes":[{"acceptedSubmissionHash":"00c086f45074ee4df763263c1678a2e369de30d25ce7b4979095880f8dfbeac7","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":"9c3d0189615e0bd0184287f41044b366f5cbc5ed1982a7445010a698883ff1f2","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":"9eb3eea32a26cb71da0f4799f65391131fb80b36699d7076795ffeff405d6d35","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":"442b683c4d8e5abc3a347dcff0926e3d9151e5f64b36070b051c9e9ae039d9b3","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":"6e477b8d06eee63373996793c74ebaad973306a6a58f5e7c786358a04d375a5c","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":"14066658-72c7-46f8-a91c-8a569722919a","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":"52625","feedbackHash":"cc1a00ac4325ed7cabe9a1b1f4183ba12f50338896a4fd3eedaac30db9fc1b28","nodeKey":"audit_economics","submissionHash":"00c086f45074ee4df763263c1678a2e369de30d25ce7b4979095880f8dfbeac7","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52623","feedbackHash":"2aacd51202e1f4fc0e4bf1e79289528dd47c1ff113c08aea6bcfd4ef9da37993","nodeKey":"audit_flow","submissionHash":"9c3d0189615e0bd0184287f41044b366f5cbc5ed1982a7445010a698883ff1f2","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52178","feedbackHash":"113afb0d85de76d6a2a8352ab44f7c6ef153e57ec8beb110df2e3bf968e315b8","nodeKey":"audit_judge","submissionHash":"9eb3eea32a26cb71da0f4799f65391131fb80b36699d7076795ffeff405d6d35","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"55247","feedbackHash":"74f236d2881f06b3f47e5fa0294c351519298ff21b4d813d23309754b9dc54ab","nodeKey":"audit_math","submissionHash":"442b683c4d8e5abc3a347dcff0926e3d9151e5f64b36070b051c9e9ae039d9b3","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52622","feedbackHash":"8eed8280f3740c7ab168a9e0302e6f6ed6af085307aa846f86e24be13625a587","nodeKey":"audit_permissions","submissionHash":"6e477b8d06eee63373996793c74ebaad973306a6a58f5e7c786358a04d375a5c","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"0a9ea88b6824f056b85b455995f2923e29780e7cb8857684a7a2b71652dfd990","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"96ece23c63b791a1","findings":[{"citation":"resolved","description":"record() (line 142) stores rc.blockNumber/rc.blockHash from the receipt firstReceipt returned. settled() (lines 164-171) checks only that some receipt for rc.hash exists and that the head is at or above rc.blockNumber. It never checks that the receipt is still in block rc.blockNumber. The run then goes on for five more steps. The only later re-check is on --resume, and it looks only at the single highest entry `top` (line 193). So if a 1-block reorg drops the block holding the deploy or addPaymentToken and the same transaction is re-included at the next height, the entry keeps the old height and nothing corrects it. The read-back builds its expected values from that height. expect.startBlock=String(deployBlock) (line 364) is compared against the immutable startBlock=block.number the constructor wrote at the real height (contract line 246). usdcUsableAt=blockTime(addBlock)+PAYMENT_TOKEN_DELAY (line 356) is compared against the contract's block.timestamp+2 days at the real block (line 382). Both mismatch on a valid deployment. A resume does not fix this: `top` is the later transferOwnership entry, so the stale deploy/add entry is never re-read, and every resume returns the same readBackMismatch. The partial file stays (line 475), the CLI exits 2, and safe-batch accept/open refuse with RECORD_HAS_MISMATCH (safe-batch.mjs line 199). The Safe cannot take ownership through the tool until someone edits the record by hand, while the deployer key keeps every owner power. This is the 'can a real reorg stick a run' case of section 4.5. It is not in section 5's accepted list, which covers only an entry that leaves the record, not one that stays with a wrong height. Fix: in the read-back, take deployBlock and addBlock from fresh getTransactionReceipt reads of the recorded hashes (or compare against genesis.startBlock() and the PaymentTokenAdded event block), not from the stored blockNumber. Or, on resume, re-check every steps/failed entry with recordedReceipt and rewrite blockNumber/blockHash from the receipt, not only the top one.","line":353,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"State: the deploy lands in block B, and firstReceipt returns rc.blockNumber=B. During settled(), or any time in the next five steps, a 1-block reorg replaces block B with B' (B' does not include the deploy), and the deploy is mined again at B+1. Its hash is unchanged, so getTransactionReceipt(hash) keeps answering and settled() passes. The run finishes all six steps. Read-back: stepBlock('deploy EmberEvoGenesis')=B, so expect.startBlock='B', while genesis.startBlock()=B+1. All 10 passes give readBackMismatch=['startBlock']. Expected: the record follows the chain (deployBlock=B+1, no mismatch). Actual: exit code 2, the partial file is kept, and `--resume` re-verifies only the transferOwnership entry (highest blockNumber) and gives the same mismatch every time. `safe-batch.mjs accept <record>` throws RECORD_HAS_MISMATCH startBlock. The same thing happens with addPaymentToken moved from height A to A+1 when the two slots' timestamps differ (12 s): paymentTokens(USDC).usableAt mismatches.","severity":"low","snippet":" const deployBlock=stepBlock('deploy EmberEvoGenesis')??Number(await retry(()=>genesis.startBlock()));","title":"A reorg that moves the deploy or addPaymentToken transaction to a later block leaves a stale blockNumber in the record; the read-back then mismatches on every resume, and safe-batch refuses the record"},{"citation":"resolved","description":"In send(), sent=true is set only when broadcastTransaction resolves (line 265). An error matched by mayHoldBid's held words (\"already known\", \"known transaction\", \"already imported\", \"same hash\") means the node holds this very transaction. This includes the README section 4.1 case of a gateway that delivered the transaction, retried internally and got the duplicate answer. On that error the code sets last=fee and keeps the hash, but sent stays false. Line 279 then waits only firstReceipt(hashes,pollMs*3) (6 s at the mainnet pollMs=2000) instead of stepTimeoutMs (5 min). The next attempt beats `last` by 25% (fees(), line 150), so a transaction a node holds and that may be in the next block is replaced at 125% of its fee, about 6 s after it went out. The tool's rule is to wait stepTimeoutMs before raising the bid, and this path skips it. The same happens one attempt after any broadcast whose reply was a non-refusal error (502, 429) although the node took it. That attempt has sent=false and leaves `last` unchanged, so the next attempt signs the same bytes and gets \"already known\". The cost is a tip 25% higher than needed for each such cycle, up to the TIP_CEILING (3x today's tip), plus 2 of the 8 attempts per cycle. Each wasted attempt brings STEP_STUCK nearer. Fix: when the error is one of mayHoldBid's held words (or any non-refusal error after a fresh hash went out), set sent=true so the receipt wait is stepTimeoutMs, the same as for a taken bid. Keep last=fee so a later replacement still clears 10%.","line":274,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Mainnet timing (pollMs=2000, stepTimeoutMs=300000). Attempt 1 of 'approvePaidPrice': fees(null) gives tip T. The node takes the transaction, but the gateway retries the same eth_sendRawTransaction internally and returns {code:-32000,message:\"already known\"}. In the tool: refusedSend is false, so the hash is kept; mayHoldBid is true, so last=fee; sent=false, so firstReceipt waits 6 s. Attempt 2: fees(last) gives tip max(T,1.25T+1), a fresh hash at the same nonce, a replacement the node takes. Expected: wait up to 5 min for the held transaction at tip T before replacing it. Actual: replaced about 6 s later at 1.25T. If the first transaction was not yet mined, the run pays 25% more tip for this step. Same sequence when attempt 1 gets HTTP 502 after delivery: attempt 2 re-signs identical bytes (same fee, same estimated limit), gets 'already known', and attempt 3 replaces at 1.25x.","severity":"low","snippet":"    }else if(fee&&mayHoldBid(e))last=fee;   // the next attempt must beat it, or it would be refused the same way","title":"A node answering that it already holds the exact transaction (\"already known\") is treated as not sent: the step waits only pollMs*3 and then outbids its own held transaction by 25%"},{"citation":"resolved","description":"Section 3 gives meetSeed three outcomes for another step's waiting transaction at this nonce. The choice between 'no node holds it, go out at market' and the other two rests on one getTransaction read per hash, and `.catch(()=>true)` turns any failed read into 'held'. If no failed entry has this step's name, the run aborts with RESUME_OUT_OF_ORDER, which tells the operator the read was stale. On the revert-retry path (line 284) failed always has this step's name, since the revert was just recorded, so the branch sets last=feeOf(seed.fee). The retry then bids at least 25% over a fee no node holds. If that passes the tip ceiling, fees() throws TIP_CEILING. With hashes=[], the handler (line 273) sets sent=true and the step burns all 8 attempts into STEP_STUCK. Both outcomes are fail-safe and recoverable by another resume, so the impact is a misleading stop and, at worst, an unneeded fee raise on setRevealCommit's retry. Fix: retry a failed getTransaction read (the file's retry() helper) and decide only on answers; keep 'held' for a non-null answer.","line":244,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"State: send('setRevealCommit') reverts at nonce n, revertRetries>0, so nonce becomes n+1. The record's queued seed is another step at n+1 whose hashes no node holds; getTransaction would answer null. The single getTransaction request returns HTTP 429, so catch gives true and some(Boolean) is true. failed contains 'setRevealCommit', so last=feeOf(seed.fee). Expected: the 'not held' outcome, a market bid. Actual: a bid of max(market, 1.25*seed.fee+1), or TIP_CEILING then STEP_STUCK when that tip passes max(3*tip, 2 gwei). On the first call of a step with no failed entry, the same 429 throws RESUME_OUT_OF_ORDER although no node holds the seed.","severity":"info","snippet":"   if(!(await Promise.all(seed.hashes.map(h=>provider.getTransaction(h).catch(()=>true)))).some(Boolean))return;","title":"meetSeed counts an RPC error on getTransaction as 'a node holds it': one 429/502 turns the 'no node holds it, go at market' outcome into RESUME_OUT_OF_ORDER, or, on the revert retry, into a bid over a"}],"hash":"00c086f45074ee4df763263c1678a2e369de30d25ce7b4979095880f8dfbeac7","nodeId":"88d8976c-755b-4373-9544-b8f29a689468","outcome":"completed","summary":"I found three defects in Submission14's deployment tools, all Low or Info, and nothing Critical, High or Medium. They are in `.imd-findings.json`, and each finding's file, line and snippet match the tree. This was a read-only review: I traced the code by hand and did not run it, because `runtime.mjs` and the private artifacts the tools load at start-up are not in the package. None of the findings is a Solidity issue at High or above, so none carries a Foundry proof.\n\n## Findings\n\n1. **Low: a real reorg can stick a run (question 5).** At `deploy-mainnet.mjs:353`. `record()` stores the block number from the first receipt it sees. `settled()` checks that a receipt exists, not that it is still in that block. Later, a resume re-checks only the newest entry.\n   - **Reproduction:** a 1-block reorg moves the deploy from block B to B+1 after it was recorded. The record keeps B, but the contract's `startBlock` is B+1, so the read-back reports a `startBlock` mismatch on every pass and on every resume.\n   - **Effect:** the run exits with code 2 and keeps the partial file. `safe-batch accept`/`open` refuse with `RECORD_HAS_MISMATCH` until someone edits the record by hand, and until then the deployer key keeps every owner power. `addPaymentToken` moved to another height breaks the same way, through USDC's `usableAt`.\n   - **Fix:** take the deploy and add blocks from fresh receipt reads in the read-back, or re-check every entry on resume.\n\n2. **Low: the bid rises 25% over a transaction a node says it already holds (questions 1, 2, 6).** At `deploy-mainnet.mjs:274/279`. An \"already known\" answer sets `last=fee` but leaves `sent=false`.\n   - **Reproduction:** a gateway delivers the transaction, retries internally and gets \"already known\". Or the first broadcast gets a 502 after delivery; the next attempt then re-signs identical bytes and gets \"already known\".\n   - **Effect:** the tool waits only 6 s instead of 5 min, then replaces its own held transaction at 125% of the fee. That can repeat up to the tip ceiling, and each round uses up 2 of the 8 attempts.\n   - **Fix:** treat the \"already held\" answers, and any non-refusal error after a new hash went out, as sent.\n\n3. **Info: one failed RPC read counts as \"a node holds it\" in `meetSeed` (questions 1, 3).** At `deploy-mainnet.mjs:244`, `.catch(()=>true)` turns any failed `getTransaction` read into \"held\".\n   - **Effect:** a single 429 turns the \"nobody holds it, send at market\" case into `RESUME_OUT_OF_ORDER`. On `setRevealCommit`'s retry after a revert, it instead makes the tool bid over a fee nobody holds, which can end in `TIP_CEILING` and then `STEP_STUCK`. Both stop safely and another resume recovers.\n   - **Fix:** retry the read, and decide only on actual answers.\n\n## Checked with no defect found\n\n- **Fees at the ceiling (6):** the last bid always clears the 10% replacement rule on both tip and max fee. The ceiling branch cannot place the same tip twice: the bid after a ceiling bid throws `TIP_CEILING`.\n- **Refusal words (2):** a geth, reth or Erigon refusal message is not missed in any case I found, and no non-refusal message accidentally matches the patterns.\n- **\"Nonce too low\" from a gateway that delivered (1):** it ends in `NONCE_MOVED` or `STEP_STUCK`. The resume then skips the step via its on-chain check and lists it in `notInRecord`. This fails safe, and the step's gas is missing from the total.\n- **`floor` at 0 (4):** a resend at an already-spent nonce can never be mined, so it costs no gas, only waiting.\n- **`plan + steps + failed` (4):** I found no way it can over-count except a reorg that leaves an older, non-top entry stale (the same root cause as finding 1).\n- **`queued` and `meetSeed` (3):** in the cases I traced, at most one of the two transactions at a nonce lands, and repeated resumes get further each time.\n- **`chainState`'s code check (7):** nothing in it stops a valid `accept` or `open`. A lagging node only causes retries.\n- **Audit 8b5de2de (8):** findings 1 to 8 are","treeHash":null,"usage":{"cachedInputTokens":917797,"inputTokens":23,"model":"claude-opus-5-5","outputTokens":51526,"runtime":"claude","turns":15,"wallClockMs":540427}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8ec0eeb0fd2a5504","findings":[{"citation":"resolved","description":"send() retries a reverted setRevealCommit up to twice (revertRetries:2, line 344) and never re-checks whether the step is already done. The contract reverts any second setRevealCommit with the same commit: _heldBefore(commit) is true once revealCommit==commit (EmberEvoGenesisCandidate.sol:312/340), so REVEAL_COMMIT_INVALID. If the first send happened because the gate read was stale, which the tool allows on a resume once the record lost the step (README section 5: a mined entry can leave the record after a reorg while receipts lag, and notDone then decides from 3 reads), the first send reverts and both retries revert the same way. Submission 14 makes this worse: the retry path now calls meetSeed() right after record() has pushed this step into `failed`. So `failed.some(f=>f.name===name)` (line 245) is always true on that path, RESUME_OUT_OF_ORDER can no longer fire there, and the retry takes `last=feeOf(seed.fee)` and bids over a live queued transaction of a later step at the new nonce. That replaces a valid transaction with one that is certain to revert. Fix: before each revert retry, run the step's done check again (the same isDone, visibleReads reads) and return if it now says done. Alternatively, retry only when the revert reason is REVEAL_ACCEPTANCE_EXPIRED or REVEAL_ACCEPTANCE_INVALID (decode the revert data from the receipt or an eth_call at the failing block). Key meetSeed's override on a failed entry recorded before this send, not on the one this attempt just wrote.","line":284,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"State: the resume record has steps [deploy, setRevealURIs] and queued [{name:'addPaymentToken(USDC, 1600 GOLD)', nonce:N+1, hashes:[H], fee:{maxFeePerGas:'30000000000', maxPriorityFeePerGas:'2000000000'}}]. On chain, setRevealCommit landed at N but the record lost it (README section 5 reorg case), and H is held in the pool. The notDone('setRevealCommit') reads go to a lagging backend and return revealCommit()==0 three times. Expected: no transaction is sent, or the run stops once it sees the commit is set. Actual: send('setRevealCommit') goes out at N and reverts REVEAL_COMMIT_INVALID (failed entry). Retry 1: meetSeed finds seed H at N+1, held, failed has setRevealCommit, so last = H's fee. The retry bids over H, replaces addPaymentToken at N+1 and reverts again. Retry 2 reverts at N+2. Then TX_FAILED: 3 reverted transactions (gas burned, about 3x the revert cost), addPaymentToken's waiting transaction is gone and must be resent.","severity":"low","snippet":"    if(revertRetries-->0){hashes=[];last=placed=null;await meetSeed();continue;}","title":"setRevealCommit revert retry re-sends without re-reading the gate: a stale 'not done' read costs three reverted transactions, and the retry's meetSeed now always outbids a held later step"},{"citation":"resolved","description":"Everywhere else the tool treats an absent answer as unproven until it repeats (codeAbsent 10 reads, notDone visibleReads, recordedReceipt visibleReads consecutive passes; the header comment at line 99 says nothing trusts one read). meetSeed decides 'no node holds the waiting transaction of another step at this nonce' from a single eth_getTransactionByHash per hash, and a null there means the step goes out at market at that nonce. Mempools are per node and not shared, so on a load-balanced RPC one backend that never received H answers null while H is pending on others. This is exactly the case the guard exists for (README: 'normally this step's read was stale'). The step that should have stopped with RESUME_OUT_OF_ORDER is sent instead. If its market bid is at least 10% over H, it replaces H. If the step was already done (the stale-gate case), it reverts (addPaymentToken twice and setRevealCommit twice revert) or lands a second time. Line 272-273 (TIP_CEILING) uses the same single read: one null sets last=null, so the next bid goes out at market below the held bid, is refused as underpriced, and climbs back to the ceiling, using up the 8 attempts.","line":244,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"State: the record has steps through setRevealCommit. addPaymentToken landed at N-1, but its entry left the record (README section 5 reorg case). queued = [{name:'approvePaidPrice', nonce:N, hashes:[H], fee:{maxFeePerGas:'20000000000', maxPriorityFeePerGas:'1000000000'}}], with H pending in the pool. The addPaymentToken gate reads 'not done' on all 3 resume reads from a lagging backend. The RPC routes eth_getTransactionByHash(H) to a backend whose pool lacks H, so it returns null. Expected: RESUME_OUT_OF_ORDER (H is held), the same as when the read reaches a backend that has H. Actual: meetSeed returns at line 244, and addPaymentToken is signed at nonce N with fees(null) = market (for example 30 gwei / 2 gwei). That is above 1.1x H, so it replaces approvePaidPrice in the pool, and if addPaymentToken was already set it lands as a revert (failed entry, gas burned). The deciding read differs only in which backend answered. Fix: decide 'not held' only after visibleReads consecutive passes in which every hash answers null (like recordedReceipt), and treat any non-null answer or error as held. Apply the same rule to the TIP_CEILING held check at line 272.","severity":"low","snippet":"   if(!(await Promise.all(seed.hashes.map(h=>provider.getTransaction(h).catch(()=>true)))).some(Boolean))return;","title":"'No node holds it' rests on one getTransaction per hash: on a load-balanced RPC a backend without the transaction in its pool sends a step over another step's live transaction (meetSeed), or drops the"},{"citation":"resolved","description":"waiting() writes fee:feeText(placed). At the first checkpoint (line 263) the new hash is in `hashes` but placed is still the previous bid. After a broadcast that the node took but whose answer was an error that is not a refusal (TIMEOUT, a 502, a gateway error), the hash stays (correct), but `placed` is never raised (line 265 is skipped). So the STEP_STUCK checkpoint at line 289 records a fee lower than the held bid. On resume, line 249 sets last=placed=that older fee, and fees() bids max(market, old*1.25+1). That is exactly the bid the node already holds whenever the market has not moved. For setRevealCommit, whose request changes on every resend (a fresh block and acceptance), the new hash with the same fee is refused 'replacement transaction underpriced', wasting one attempt. README section 5 accepts only the fee:null case (a death between checkpoints on the first bid). This case has a non-null but stale fee and also happens without a process death.","line":238,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Attempt 1 goes out with placed P = {maxFee 20 gwei, tip 1 gwei}. Attempt 2 bids F = {25000000001, 1250000001} and is delivered, but the RPC answers SERVER_ERROR (502): the hash is kept, last and placed stay P. Attempts 3 to 8 fail the same way (for example RPC flapping), then STEP_STUCK writes pending.fee = P. The resume sets last=P, and fees(P) with the market unchanged returns F again. setRevealCommit signs a new acceptance, so the hash is new and the fee equals the held F. Expected: the bid clears the held F by 10%. Actual: refused as underpriced, and only the next attempt bids high enough. Fix: when the error is not a refusal and the hash was kept, treat the bid as possibly held (`last=placed=fee` for record purposes), or record the highest signed fee of any kept hash in pending.fee.","severity":"info","snippet":"  const waiting=()=>({pending:{name,nonce,hashes,fee:feeText(placed),gasLimit:knownLimit===null?null:knownLimit.toString()}});","title":"The record's fee lags the transaction a node holds: pre-broadcast checkpoint and non-refusal errors record the previous bid, so a resume can re-bid exactly the held fee and be refused"}],"hash":"442b683c4d8e5abc3a347dcff0926e3d9151e5f64b36070b051c9e9ae039d9b3","nodeId":"619eac92-563a-469e-acda-8b39dfa41f6d","outcome":"completed","summary":"The background search for ethers' error-matching code was stopped when it hit its time limit, and it produced no output. I won't restart it: I took that error matching from memory of ethers v6 instead of reading the installed copy. None of the findings rests on the exact patterns.\n\nThe review stands as reported. `.imd-findings.json` holds two low findings and one info finding, all in `Submission14/tools/deploy-mainnet.mjs`:\n\n- **Low, line 284:** when setRevealCommit reverts, the retry sends it again without checking whether the step is already done. On a stale read that means three reverted transactions, and the retry can replace a later step's waiting transaction.\n- **Low, line 244 (the same pattern at line 272):** \"no node holds this transaction\" is decided from a single lookup per hash. Behind a load-balanced RPC, one node that never saw it can make a step go out over another step's live transaction.\n- **Info, line 238:** the fee written to the record can be lower than the bid a node actually holds, so a resume can repeat that exact bid and be refused once.","treeHash":null,"usage":{"cachedInputTokens":91939,"inputTokens":4,"model":"claude-opus-5-5","outputTokens":381,"runtime":"claude","turns":1,"wallClockMs":886065}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"91d3f9816008ce9f","findings":[{"citation":"resolved","description":"refusedSend treats NONCE_EXPIRED as 'the node holds no such thing'. That is false for the transaction that spent the nonce. ethers maps any eth_sendRawTransaction message matching /nonce/ and /too low/ to NONCE_EXPIRED. A gateway or load-balanced RPC that retries internally (README section 4 item 1) can deliver the signed bytes and then answer 'nonce too low' about them once they are mined. Line 269 then deletes the only record of a mined transaction, and nothing checks getTransaction/getTransactionReceipt(own) first. Two paths follow. (a) hashes becomes empty: line 277 throws NONCE_MOVED and checkpoint(state()) writes a record without the hash. The step's receipt, gas and, for a revert, its `failed` entry are gone for good, so totalGas and plan+steps+failed undercount. (b) An earlier bid X is still in hashes (the delivered one Y was its replacement): NONCE_MOVED cannot fire because hashes.length>0. last is not raised (NONCE_EXPIRED is not mayHoldBid). Each later attempt signs another bid at the spent nonce, which is refused and removed in turn, and firstReceipt([X]) never returns because X was replaced. After 8 attempts the run stops STEP_STUCK with pending.hashes=[X], even though the step landed. The next resume spends stepTimeoutMs (5 min) waiting on X, drops it, and the step shows up only in notInRecord. If Y reverted (setRevealCommit), its `failed` entry and gas are lost too. So the answer to section 4 item 1 is: only a manual resume recovers, and the record is permanently incomplete. It fails safe: no second contract, because the nonce never moves unread. Fix: for NONCE_EXPIRED, delete `own` only when getTransactionReceipt(own) and getTransaction(own) both return null over visibleReads reads. If a receipt exists, treat it as this step's receipt: settled(), record() and nonce++. The bidTooLow and INSUFFICIENT_FUNDS refusals can keep the current removal.","line":269,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Do the setRevealCommit step at nonce n. Attempt 1 broadcasts X (fee F) and is accepted. firstReceipt times out, so attempt 2 signs Y at 1.25F. The RPC proxy forwards Y, Y is mined at n, and the proxy's retried forward returns {code:-32000,message:'nonce too low'}. Expected: Y stays in hashes, firstReceipt finds Y's receipt, and Y is recorded (in steps, or in failed with its gas). Actual: line 269 removes Y. hashes=[X], so line 277 is skipped. Attempts 3 to 8 sign new bids at n and each gets NONCE_EXPIRED and is removed. The run throws 'STEP_STUCK setRevealCommit: NONCE_EXPIRED' with pending {nonce:n,hashes:[X]}, and Y's gas never appears in the record. With attempt 1 as the delivered one (hashes empty), it throws NONCE_MOVED and the record holds no hash for nonce n.","severity":"low","snippet":"    if(fresh&&refusedSend(e))hashes.splice(hashes.indexOf(own),1);","title":"send(): a 'nonce too low' answer removes the hash of the transaction that spent the nonce, so a landed step is lost from the record and the run stops STEP_STUCK or NONCE_MOVED"},{"citation":"resolved","description":"RESUME_OUT_OF_ORDER is the guard for this case: a stale gate read says an already-landed step is not done, at the nonce where another step's transaction is waiting. Since Submission 14 the guard is skipped when a single eth_getTransactionByHash answer is null for every seed hash. A null from one backend of a load-balanced RPC shows only that this node's pool lacks the transaction. The tool applies visibleReads elsewhere for this reason (gates, codeVisible, recordedReceipt, settled), but not here. Under the README's own lag model, a stale gate followed by one empty answer sends the already-done step again at the seed's nonce at a market bid. If that bid is refused as underpriced, last=fee and the next attempt bids 125% over it and replaces the other step's waiting transaction. The landed step then runs a second time. addPaymentToken or approvePaidPrice repeated reverts on chain: a `failed` entry, burned gas, and a TX_FAILED stop, because those steps have revertRetries=0. setRevealURIs or transferOwnership repeated costs an extra transaction. The replaced step is resent later. It fails safe (read-back), but it burns gas and stops the run, which is exactly what the guard existed to prevent. Fix: require the null answer on visibleReads consecutive reads, spaced pollMs apart, before treating the seed as unheld. Any non-null answer or RPC error resets the count. Keep the current RESUME_OUT_OF_ORDER throw otherwise.","line":244,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Record: steps=[deploy@n0, setRevealURIs@n0+1], and setRevealCommit landed at n0+2 but is not in the record (the top entry was dropped by recordedReceipt after a reorg moved it to a later block: README section 5, accepted, 'the gate recovers it'). pending={name:'addPaymentToken(USDC, 1600 GOLD)',nonce:n0+3,hashes:[H],fee:F}, and H is in the network mempool. Resume with a load-balanced RPC: stableNonce=n0+3. The setRevealCommit gate reads a lagging node 3 times and says not done. meetSeed's single getTransaction(H) hits a backend whose pool lacks H and returns null. Expected: RESUME_OUT_OF_ORDER, and nothing is sent. Actual: setRevealCommit is signed and sent at n0+3 and replaces H after one underpriced refusal. It lands and either reverts (the previous commit is read from the lagging node, giving a failed entry and a retry at n0+4) or repeats the commit. H is lost, addPaymentToken is resent at a later nonce, and extra gas is burned.","severity":"low","snippet":"   if(!(await Promise.all(seed.hashes.map(h=>provider.getTransaction(h).catch(()=>true)))).some(Boolean))return;","title":"meetSeed(): one getTransaction read per hash decides that 'no node holds' another step's waiting transaction, which bypasses RESUME_OUT_OF_ORDER on a lagging/load-balanced RPC"},{"citation":"resolved","description":"plan+steps+failed is a valid lower bound only while every recorded entry is still mined. recordedReceipt (line 193) checks just `top`, the entry with the highest blockNumber. A reorg two or more blocks deep that removes the top entry and the one below it drops only the top. The lower entry stays, so plan+steps+failed is one above the real count. With floor 0 and a correct stableNonce, Math.max picks the too-high value and the step is signed at a nonce with a gap below it. Nodes keep such a transaction queued and it never mines, so firstReceipt times out on every attempt and the run ends STEP_STUCK. Every resume repeats this, because the stale entry is never re-checked, until the reorged transaction is re-mined. If it is never re-mined (dropped from pools, or invalid after the reorg), the run cannot progress without hand-editing the record. This answers 'is plan+steps+failed right for every record the tool can write': not when a non-top entry was reorged out. Mainnet reorgs of 2 or more blocks are rare, so this is info. Fix: on resume, run recordedReceipt over every steps/failed entry, not only the top, or at least over every entry whose block is within a reorg window of the head. Alternatively, use plan+steps+failed only after confirming that the receipt of the entry at the highest nonce is still visible.","line":211,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Record: steps=[deploy@n0 (block B), setRevealURIs@n0+1 (block B+1), setRevealCommit@n0+2 (block B+2, top)]. A 2-block reorg replaces B+1 and B+2, and neither transaction is re-mined (for example, both are left under the base fee and evicted). Resume: recordedReceipt(top) sees another block at B+2 without its hash on 3 reads and drops setRevealCommit. The setRevealURIs entry is never checked. stableNonce()=n0+1, but planNonce+steps.length+failed.length = n0+2, so nonce=n0+2. Expected: setRevealURIs (gate: not done) and then setRevealCommit go out starting at n0+1. Actual: the setRevealURIs gate is skipped because stepBlock finds the stale entry. setRevealCommit is signed at n0+2 behind an empty n0+1, sits in the queue, and never mines. Each attempt times out, the run ends STEP_STUCK, and every resume repeats this.","severity":"info","snippet":"  nonce=Math.max(await stableNonce(),floor,planNonce+steps.length+failed.length);","title":"Resume re-checks only the highest record entry: an entry below it that a deeper reorg removed keeps plan+steps+failed above the chain's count and puts the run at a gap nonce"}],"hash":"6e477b8d06eee63373996793c74ebaad973306a6a58f5e7c786358a04d375a5c","nodeId":"97075c61-84f5-4c42-8ce9-87260a5e21c9","outcome":"completed","summary":"I wrote three findings to `.imd-findings.json`: two Low and one Info. All are in `Submission14/tools/deploy-mainnet.mjs`, none affects the contract, and each one stops the run rather than leaving a wrong deployment. I couldn't run anything: the package has no `node_modules` and no `tools/runtime.mjs`, and these are JavaScript tools, so a Foundry test doesn't apply. Every finding comes from reading the code against a concrete state, and none has been reproduced by running it.\n\n## Findings\n\n1. **Low, line 269 (`send`): a \"nonce too low\" answer can delete the hash of the transaction that used the nonce.** The tool counts \"nonce too low\" as a refusal, but that is also what a retrying gateway says about a transaction it just delivered and that is already mined. Nothing checks the hash before deleting it. Section 4 asked whether anything recovers from this.\n   - **If it was the only hash:** the run stops with `NONCE_MOVED` and saves a record with no hash for that nonce. The gas is lost, and so is the `failed` entry if it reverted.\n   - **If an earlier bid is still recorded:** `NONCE_MOVED` can't fire. Every later attempt is refused, so after 8 attempts the run stops with `STEP_STUCK` even though the step landed.\n   - **Recovery:** only a manual resume, which waits 5 minutes and leaves the step in `notInRecord`.\n   - **Fix:** before deleting, ask for the hash's receipt and transaction over several reads, and record the receipt if one exists.\n\n2. **Low, line 244 (`meetSeed`): one empty `getTransaction` answer skips the `RESUME_OUT_OF_ORDER` guard.** Everywhere else the tool requires several matching reads; here a single read from one backend decides that no node holds another step's waiting transaction. Under the README's own lagging-node model, a step that already landed is sent again at that nonce and outbids the waiting transaction. That costs gas. For steps that can't be repeated, such as `addPaymentToken`, it reverts and stops with `TX_FAILED`.\n   - **Fix:** require the empty answer on several consecutive reads before acting on it.\n\n3. **Info, line 211: `plan + steps + failed` can be too high.** On resume only the newest recorded entry is checked against the chain. If a reorg two or more blocks deep also removes an older entry, that entry stays in the record. The next nonce is then one too high, so the transaction waits behind a gap and never mines. The run ends `STEP_STUCK` on every resume until the missing transaction is mined again. This answers question 4: the sum is not right for every record the tool can write.\n   - **Fix:** check every recorded entry on resume, or at least those within a reorg window of the head.\n\n## Questions with no finding\n- **Q1 and Q2:** apart from finding 1, a hash that no node ever held only causes slower recovery, never a wrong decision. Nethermind's current error messages are the same strings geth uses, checked against its source, so the words list catches them.\n- **Q3:** apart from the cases section 5 already accepts, I found no way for a live transaction to leave the record or for a step to land twice.\n- **Q5:** apart from the moved-transaction case section 5 accepts, a real reorg doesn't stick a run.\n- **Q6:** the bid at the ceiling meets geth's rule of at least 10% more on both fee fields. The next round always stops with `TIP_CEILING` rather than repeating a bid, except an identical resend of the same signed transaction, which is harmless.\n- **Q7:** I found no path where `chainState`'s code check blocks a valid `accept` or `open`. The only failures are a record without `codeHash` and a changed local build, and the second is intended.\n- **Q8:** findings 1–8 of audit 8b5de2de look addressed, apart from the gaps above and the limits section 5 accepts.\n\n## Coverage\nI reviewed the Access Control, Trust Gap and Asymmetry passes in my own reading; no helpers ran. Some areas got less depth:\n- `seedLanded` under combined stops and reorgs.\n- The full set of resume scenarios beyond the ones traced above","treeHash":null,"usage":{"cachedInputTokens":696561,"inputTokens":19,"model":"claude-opus-5-5","outputTokens":36231,"runtime":"claude","turns":12,"wallClockMs":379628}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a800aae85243bc87","findings":[{"citation":"resolved","description":"The comment on line 25 says the list covers Nethermind, but Nethermind's eth_sendRawTransaction (EthRpcModule.SendTx) rejects with `ResultWrapper.Fail(acceptTxResult.ToString(), TransactionRejected)`. AcceptTxResult.ToString() is `Message is null ? Code : $\"{Code}, {Message}\"`, so the JSON-RPC message is the CamelCase result name. UnderpricedReplacementFilter returns a bare `ReplacementNotAllowed`, LowNonceFilter a bare `OldNonce`, BalanceTooLowFilter a bare `InsufficientFunds` for remote txs, the fee filter `FeeTooLow, ...`, and a duplicate `AlreadyKnown`. ethers v6 getRpcError maps sendRawTransaction errors only on /insufficient funds/, /nonce/+/too low/ and /replacement transaction/+/underpriced/. None of these match, so each one surfaces as UNKNOWN_ERROR whose message holds the CamelCase word. That word matches no alternative in bidTooLow (line 26), mayHoldBid (line 28) or refusedSend (line 30). In send() (lines 267-277) this means: (a) a refused replacement leaves `last` where it was, so the next attempt signs the same bid again, and the bid never climbs. This is the failure finding 3 of audit 8b5de2de describes, still present for this client. (b) A refused hash is not removed (line 269), so a never-held hash stays in the record. (c) A send at a spent nonce is never NONCE_EXPIRED, so the NONCE_MOVED stop on line 277 never fires. Section 3 item 5 relies on that stop for a stale low read now that floor starts at 0. Instead the step burns all attempts and ends STEP_STUCK, recording never-held hashes as pending. The next resume then waits stepTimeoutMs (5 min) on them (line 207) before it moves on. Besu (RpcErrorType.java at main) shows the same gap on a smaller scale. Its insufficient-funds refusal is 'Upfront gas cost exceeds account balance' (-32004), which ethers does not map to INSUFFICIENT_FUNDS, and 'Gas price below current base fee' matches nothing in the list. Its 'Nonce too low', 'Replacement transaction underpriced' and 'Known transaction' are matched. Fix: match on the JSON-RPC error text case-insensitively, without depending on spaces, and include Nethermind's codes: /ReplacementNotAllowed|FeeTooLow(ToCompete)?/ in bidTooLow, /OldNonce/ and /InsufficientFunds|upfront gas cost exceeds/ in refusedSend (or map them to NONCE_EXPIRED / INSUFFICIENT_FUNDS before line 277 tests e.code), and /AlreadyKnown/ in mayHoldBid. Add CLI tests whose stand-in RPC answers with these exact Nethermind and Besu strings.","line":26,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Verified with ethers 6.17.0 and the three predicates copied from lines 26/28/30: provider.getRpcError({method:'eth_sendRawTransaction',...},{error:{code:-32010,message:M}}) gives code UNKNOWN_ERROR and refusedSend=false, bidTooLow=false, mayHoldBid=false for M = 'ReplacementNotAllowed', 'OldNonce', 'InsufficientFunds', 'AlreadyKnown' and 'FeeTooLow, FeePerGas needs to be higher than 5'. The geth strings 'replacement transaction underpriced' and 'nonce too low' are classified correctly. Run scenario: RPC_URL backed by Nethermind. At step setRevealURIs, a run signs T1 (tip 1 gwei, maxFee F), checkpoints the hash with fee null, the node takes T1, and the process dies before the second checkpoint (section 5 case). The base fee then rises above F. On resume, last=placed=null, so the tool bids at market with the same 1 gwei tip. Nethermind answers 'ReplacementNotAllowed'. Expected: the bid is classed as too low, its hash removed and last raised, so the next bid is 25% higher and replaces T1 (what happens with geth's words). Actual: the hash is kept, last stays null, and all 8 attempts re-sign the same market bid and get the same refusal. The step ends STEP_STUCK ... UNKNOWN_ERROR, and every resume repeats it while T1 stays unmineable. Second scenario: a resume on a stale low count sends at a spent nonce. Nethermind answers 'OldNonce', so NONCE_MOVED never fires and the run ends STEP_STUCK after 8 attempts.","severity":"low","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":"Finding 3 not fixed for Nethermind: none of its refusals (ReplacementNotAllowed, OldNonce, InsufficientFunds, FeeTooLow, AlreadyKnown) match bidTooLow / refusedSend / mayHoldBid"}],"hash":"9c3d0189615e0bd0184287f41044b366f5cbc5ed1982a7445010a698883ff1f2","nodeId":"e89b039d-fc7e-4abb-83f8-0278b637d00b","outcome":"completed","summary":"I found one reproducible defect, rated Low. It is written to `.imd-findings.json`, and the file is valid JSON. No repository files were changed. There is no Foundry proof: the contract didn't change and the finding is in the JavaScript tools.\n\n## Finding: audit finding 3 is still not fixed for Nethermind (Low)\n**Where:** `Submission14/tools/deploy-mainnet.mjs:26`, plus lines 28 and 30.\n\n- **Cause:** Nethermind rejects a transaction with a bare result name such as `ReplacementNotAllowed`, `OldNonce`, `InsufficientFunds`, `FeeTooLow, …` or `AlreadyKnown`. I read this from its source at main (`SendTx`, `AcceptTxResult.ToString()` and the filter files). ethers v6 maps none of these to an error code, so each one arrives as `UNKNOWN_ERROR`. None of the three word lists (`bidTooLow`, `mayHoldBid`, `refusedSend`) matches them, because they look for spaced words like \"fee too low\".\n- **Checked:** I installed ethers 6.17.0 and ran its own `getRpcError` on each string. All give `UNKNOWN_ERROR` and all three predicates return false. geth's \"replacement transaction underpriced\" and \"nonce too low\" are still classified correctly.\n- **What breaks:**\n  - **The bid never climbs.** Example: a run dies after a send is taken but before the second checkpoint (the README section 5 case), and the base fee then rises above that bid. On resume, Nethermind answers `ReplacementNotAllowed`. The tool keeps the refused hash, does not raise its bid, and signs the same bid 8 times. It ends `STEP_STUCK`, and every resume does the same until the fee market falls. This is exactly what audit finding 3 described.\n  - **The stale-nonce stop never fires.** `OldNonce` is never treated as \"nonce too low\", so the `NONCE_MOVED` stop at line 277 never fires. The \"floor starts at 0\" change relies on that stop. On Nethermind a stale low read instead ends `STEP_STUCK`, with hashes no node holds left in the record, and the next resume waits 5 minutes on them.\n- **Besu, smaller gap:** its insufficient-funds text \"Upfront gas cost exceeds account balance\" and \"Gas price below current base fee\" are also missed. Its nonce, replacement and known-transaction strings are matched.\n- **Fix:** add the Nethermind result names and the Besu funds text to the three lists, or map them to the right error codes before line 277 checks them. Add CLI tests whose stand-in RPC answers with these exact strings.\n\n## The other questions: nothing I could reproduce\n1. **Signing first.** A hash no node ever held changes only which error the run stops with and how long a resume waits; I found no unsafe action. A refusal can remove a held hash if a gateway reports \"nonce too low\" about a transaction it delivered. That leads to `NONCE_MOVED` or `STEP_STUCK`, and on resume the step's check of chain state sees it done. A deploy is caught by the code check or `PLANNED_NONCE_USED`.\n2. **Refusal words.** Apart from the finding, I found no error that counts as a refusal when the node took the transaction. A timeout, a 502, or ethers failing on its extra head read after sending all keep the hash.\n3. **`queued` and `meetSeed`.** Every case I traced either converged or matched what section 5 already accepts.\n4. **Floor at 0.** A send at a spent nonce is refused, so it costs no gas. The count `plan + steps + failed` can only run high if a reorg drops a non-top entry and its transaction disappears from every pool. I couldn't build that from what the tool writes.\n5. **`recordedReceipt`.** A real reorg ends either in the new receipt or in the entry being dropped, never in a stuck run. A node sitting briefly on a losing fork can drop a mined entry, but that fails safe.\n6. **Fees at the ceiling.** The ceiling bid always clears geth's 10% rule on both tip and fee cap, and the branch cannot repeat a bid.\n7. **`chainState`'s code check.** `codeHash` comes from the same read-back pass whenever that pass reports no mismatch. A lagging node only causes retries, so I found no way for a valid `accept` or `open` to be blocked.\n","treeHash":null,"usage":{"cachedInputTokens":1369606,"inputTokens":33,"model":"claude-opus-5-5","outputTokens":49593,"runtime":"claude","turns":21,"wallClockMs":554793}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f768e94767a9dde3","findings":[{"citation":"resolved","description":"refusedSend (line 30) counts NONCE_EXPIRED as 'the node holds no such thing', and line 269 deletes the hash this attempt added. That is wrong for the transaction that spent the nonce. ethers 6.17.0 maps any eth_sendRawTransaction error whose message matches /nonce/ and /too low/ to NONCE_EXPIRED (verified with provider.getRpcError). A gateway that retries internally (README section 4 item 1) can deliver the signed bytes and, once they are mined, answer 'nonce too low' about them. The author's test for finding 2 covers only a resend of the same bytes (fresh=false). For a fresh bid Y (a replacement after the first bid timed out, or any setRevealCommit resend, whose request changes every attempt) the hash is removed and nothing reads getTransactionReceipt(own) first. Two outcomes. (a) hashes empty: line 277 throws NONCE_MOVED; the resume finds the step by its gate (steps) or resends it (a revert), but the receipt, gas and any `failed` entry of Y are gone from the record for good and plan+steps+failed undercounts. (b) An earlier bid X is still in hashes: NONCE_MOVED cannot fire, `last` is not raised (NONCE_EXPIRED is not mayHoldBid), every later attempt signs another bid at the spent nonce, is refused and removed, firstReceipt([X]) never answers because X was replaced, and after 8 attempts the run stops STEP_STUCK with pending.hashes=[X]. The next resume waits stepTimeoutMs (5 min) on X at line 207, then seedLanded waits another stepTimeoutMs, before moving on; the step appears only in notInRecord. It fails safe (no second contract, no double send) but the record is permanently incomplete and the run stops and waits for nothing. Fix: on NONCE_EXPIRED for a fresh hash, read getTransactionReceipt(own) (and getTransaction(own)) over visibleReads reads before removing it; if a receipt exists treat it as this step's receipt (settled, record, nonce++). bidTooLow and INSUFFICIENT_FUNDS can keep the removal.","line":269,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Mainnet timing (pollMs 2000, stepTimeoutMs 300000, attempts 8). Step setRevealCommit at nonce n. Attempt 1 broadcasts X at fee F, accepted (sent=true, last=placed=F). firstReceipt times out. Attempt 2 signs Y (fresh block, fresh acceptance) at 1.25F+1; broadcastTransaction is forwarded by the gateway, the node takes Y, Y is mined at n, the gateway's retried forward returns {code:-32000,message:'nonce too low'} and ethers throws NONCE_EXPIRED. Expected: Y stays in hashes, firstReceipt returns Y's receipt, Y is recorded in steps (or in failed with its gas). Actual: line 269 removes Y; hashes=[X] so line 277 does not fire; firstReceipt([X],6 s) is null; attempts 3..8 each sign a new acceptance at 1.25F+1 at nonce n, get NONCE_EXPIRED, are removed; the run throws 'STEP_STUCK setRevealCommit: NONCE_EXPIRED' with pending={nonce:n,hashes:[X]}. With attempt 1 as the delivered one (hashes empty after removal and stableNonce()=n+1) it throws NONCE_MOVED instead and the checkpoint holds no hash for nonce n.","severity":"low","snippet":"    if(fresh&&refusedSend(e))hashes.splice(hashes.indexOf(own),1);","title":"send(): a 'nonce too low' answer about a fresh transaction a node already delivered removes the only hash of a mined step; the run then burns all attempts (STEP_STUCK) or stops NONCE_MOVED with the st"},{"citation":"resolved","description":"Everywhere else the tool treats an absent answer as unproved until it repeats (codeAbsent 10 reads, notDone visibleReads, recordedReceipt visibleReads consecutive passes; line 100 says nothing trusts one read). meetSeed decides 'no node holds the waiting transaction of another step at this nonce' from one getTransaction per hash, and a null means this step goes out at market at that nonce. Mempools are per node: on a load-balanced RPC a backend that never received H, or a lagging one, answers null while H is pending elsewhere. That is exactly the case the guard exists for (line 239: 'normally this step's read was stale'). The step goes out; if its market bid is at least 10% over H it replaces H, and if the step was in fact done (the stale-gate case the README's reorg exception creates) it lands as a revert (addPaymentToken and setRevealCommit refuse a repeat; revertRetries is 0 for addPaymentToken so the run stops TX_FAILED) or as a second transaction. The opposite direction is also one read: `.catch(()=>true)` turns a 429/502 into 'held', which on the first call throws RESUME_OUT_OF_ORDER for a transaction no node holds, and on the revert-retry path (line 284, where failed always contains this step's name) raises last to the seed's fee so the retry bids at least 25% over a fee nobody holds; if that passes the ceiling fees() throws TIP_CEILING, hashes is empty, sent=true, and 8 attempts later the step throws. Line 272 (TIP_CEILING held check) uses the same single read: one null sets last=null, the next bid goes out at market below the held bid, is refused as underpriced, and climbs back to the ceiling through the remaining attempts. Fix: decide 'not held' only after visibleReads consecutive reads, spaced pollMs apart, in which every hash answers null; retry a failed read with the file's retry() helper and decide only on answers; apply the same rule at line 272.","line":244,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Record: steps through setRevealCommit; addPaymentToken landed at nonce N-1 but its entry left the record (README section 5, accepted reorg case); queued=[{name:'approvePaidPrice',nonce:N,hashes:[H],fee:{maxFeePerGas:'20000000000',maxPriorityFeePerGas:'1000000000'},gasLimit:null}] with H pending in the pool. Resume: stableNonce()=N. The addPaymentToken gate (paymentTokens(USDC)!=0) is read 3 times from a lagging backend and says 0. send('addPaymentToken') calls meetSeed: seed.nonce===N, name differs; the single getTransaction(H) is routed to a backend whose pool lacks H and returns null; some(Boolean) is false; meetSeed returns. Expected: RESUME_OUT_OF_ORDER, nothing sent (what the same resume does when the read reaches a backend that has H). Actual: addPaymentToken is signed at nonce N with fees(null) = market (e.g. 30 gwei/2 gwei, above 1.1x H), replaces approvePaidPrice in the pool, and reverts on chain because USDC is already added: a `failed` entry, burned gas, TX_FAILED stop, and H must be resent later. Error direction: after a reverted setRevealCommit at nonce n with revertRetries left, nonce=n+1, queued seed of another step at n+1 whose hashes no node holds; getTransaction returns HTTP 429, catch gives true, failed contains 'setRevealCommit', so last=feeOf(seed.fee) and the retry bids max(market,1.25*seed.fee+1) instead of market.","severity":"low","snippet":"   if(!(await Promise.all(seed.hashes.map(h=>provider.getTransaction(h).catch(()=>true)))).some(Boolean))return;","title":"meetSeed: one eth_getTransactionByHash answer per hash decides whether another step's waiting transaction is held, so a single null from a lagging/load-balanced backend bypasses RESUME_OUT_OF_ORDER an"},{"citation":"resolved","description":"record() (line 142) stores rc.blockNumber from the receipt firstReceipt returned. settled() (lines 164-171) only checks that some receipt for rc.hash exists and the head is at or above rc.blockNumber; it never checks the receipt is still in that block, and record() keeps the old rc. The run then goes on for up to five more steps. The only later re-check is on --resume, and it re-reads only the single highest entry `top` (line 193). So when a 1-block reorg drops the block holding the deploy or addPaymentToken and the same transaction is re-included at the next height, the entry keeps the old height and nothing corrects it. The read-back builds its expectations from that height: expect.startBlock=String(deployBlock) (line 364) against the immutable startBlock=block.number the constructor wrote at the real height (contract line 246), and usdcUsableAt=blockTime(addBlock)+PAYMENT_TOKEN_DELAY (line 356) against block.timestamp+2 days at the real block (contract line 423). Both mismatch on a valid deployment, in all 10 passes, since expect is computed once. A resume does not fix it: top is the transferOwnership entry, so the stale entry is never re-read and every resume returns the same readBackMismatch. The partial file stays (line 475), the CLI exits 2, and safe-batch.mjs accept/open throw RECORD_HAS_MISMATCH (safe-batch.mjs line 199). The Safe cannot take ownership through the tool until the record is edited by hand, while the deployer key keeps every owner power. This answers README section 4 item 5 ('can a real reorg now stick a run'): yes, and section 5 lists only an entry that leaves the record, not one that stays with a wrong height. Fix: in the read-back take deployBlock and addBlock from fresh getTransactionReceipt reads of the recorded hashes (or compare against genesis.startBlock() and the PaymentTokenAdded event block), or on resume re-check every steps/failed entry with recordedReceipt and rewrite blockNumber/blockHash from the receipt, not only the top one.","line":353,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"State: the deploy lands in block B; firstReceipt returns rc with blockNumber=B. During settled() or any of the next five steps a 1-block reorg replaces B with B' (without the deploy) and the deploy is mined again at B+1 under the same hash, so getTransactionReceipt(hash) keeps answering and settled() passes. The run finishes all six steps. Read-back: stepBlock('deploy EmberEvoGenesis')=B, expect.startBlock='B', genesis.startBlock()=B+1 (constructor stores block.number). Expected: no mismatch (the record follows the chain, deployBlock=B+1). Actual: readBackMismatch=['startBlock'] in all 10 passes, exit code 2, partial file kept; `--resume` re-verifies only the transferOwnership entry and gives the same mismatch; `safe-batch.mjs accept <record>` throws 'RECORD_HAS_MISMATCH startBlock'. Same with addPaymentToken moved from height A to A+1: blockTime(A)+172800 differs from the stored usableAt by the 12 s slot difference, so paymentTokens(USDC).usableAt mismatches.","severity":"low","snippet":" const deployBlock=stepBlock('deploy EmberEvoGenesis')??Number(await retry(()=>genesis.startBlock()));","title":"A reorg that moves the deploy or addPaymentToken transaction to a later block leaves a stale blockNumber in the record; the read-back then mismatches startBlock or usableAt on every resume and safe-ba"},{"citation":"resolved","description":"sent=true is set only when broadcastTransaction resolves (line 265). An error matched by mayHoldBid's held words ('already known', 'known transaction', 'already imported', 'same hash') means the node holds exactly this transaction (verified: ethers 6.17.0 gives UNKNOWN_ERROR for 'already known' and 'Known transaction', so refusedSend is false and the hash is kept, which is right). This is the README section 4 item 1 case of a gateway that delivered the transaction, retried internally and got the duplicate answer, and also the attempt after any broadcast that errored with a non-refusal (502, 429) although the node took it: that attempt leaves `last` unchanged, so the next one signs the same bytes and gets 'already known'. On that error the code sets last=fee but sent stays false, so line 279 waits firstReceipt(hashes,pollMs*3) instead of stepTimeoutMs. The next attempt beats `last` by 25% (fees(), line 150), so a transaction a node holds and that may be in the next block is replaced about 6 s after it went out. The tool's rule is to wait stepTimeoutMs before raising a bid; this path skips it. Cost: a tip 25% higher than needed per such cycle, up to TIP_CEILING (3x today's tip), and 2 of the 8 attempts per cycle, each bringing STEP_STUCK nearer. Fix: when the error is one of mayHoldBid's held words (not a bidTooLow refusal), set sent=true so the receipt wait is stepTimeoutMs, the same as for a taken bid; keep last=fee.","line":274,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Mainnet timing (pollMs 2000, stepTimeoutMs 300000). Attempt 1 of approvePaidPrice: fees(null) gives tip T. The node takes the transaction, the gateway retries the same eth_sendRawTransaction and returns {code:-32000,message:'already known'}. In send(): refusedSend false (hash kept), mayHoldBid true (last=fee), sent=false, firstReceipt waits 6 s and returns null, continue. Attempt 2: fees(last) gives tip max(T,1.25T+1), a fresh hash at the same nonce, taken as a replacement. Expected: wait up to 5 min for the held transaction at tip T before replacing it. Actual: replaced about 6 s later at 1.25T; if the first one was not yet mined the step pays 25% more tip. Same sequence when attempt 1 gets HTTP 502 after delivery: attempt 2 re-signs identical bytes (fresh=false), gets 'already known', waits 6 s, and attempt 3 replaces at 1.25x.","severity":"low","snippet":"    }else if(fee&&mayHoldBid(e))last=fee;   // the next attempt must beat it, or it would be refused the same way","title":"send(): a node answering 'already known' (it holds this very transaction) is treated as not sent: the step waits only pollMs*3 (6 s on mainnet) instead of stepTimeoutMs and then replaces its own held "},{"citation":"resolved","description":"Line 25 says the list covers Nethermind; README section 7 admits its constants were not read. Checked against the sources: in Nethermind 1.38.1 (AcceptTxResult.cs at that tag) the results are AlreadyKnown=new(1,nameof(AlreadyKnown)), FeeTooLow=new(3,nameof(FeeTooLow)), InsufficientFunds=new(6,nameof(InsufficientFunds)), ReplacementNotAllowed=new(11,nameof(ReplacementNotAllowed)), with ToString() => Message is null ? Code : $\"{Code}, {Message}\"; EthRpcModule.SendTx returns ResultWrapper.Fail(acceptTxResult.ToString(), ErrorCodes.TransactionRejected); TxPool.AddCore returns the bare ReplacementNotAllowed, FeeTooLowFilter the bare FeeTooLow, BalanceTooLowFilter the bare InsufficientFunds for remote transactions. (OldNonce is already 'nonce too low' at 1.38.1, so NONCE_MOVED does work there; the flow specialist's OldNonce claim does not hold.) Commit 693ab2b9 of 2026-06-30 ('align AcceptTxResult rejection messages with Geth canonical phrases'), first shipped in 1.39.0 on 2026-07-08, changed them to 'replacement transaction underpriced', 'transaction underpriced', 'already known', which the list matches; every earlier release, which an RPC provider may still run four months later, answers with the CamelCase words. Verified with ethers 6.17.0 getRpcError and the three predicates copied from lines 26/28/30: 'ReplacementNotAllowed', 'FeeTooLow, FeePerGas needs to be higher than 5', 'AlreadyKnown', 'InsufficientFunds' all give UNKNOWN_ERROR with refusedSend=bidTooLow=mayHoldBid=false. In send() that means: a refused replacement leaves `last` where it was, so the next attempt signs the same bid and the bid never climbs (the failure finding 3 describes); the refused hash is kept (line 269) as a never-held hash in the record, which the next resume waits stepTimeoutMs on at line 207; 'AlreadyKnown' does not raise `last`, so a resend of the same bytes loops 8 times at 6 s each. Besu (RpcErrorType.java at main) has a smaller gap: 'Upfront gas cost exceeds account balance' (-32004) is not mapped to INSUFFICIENT_FUNDS by ethers and matches nothing, and 'Gas price below current base fee' matches nothing, while its 'Nonce too low', 'Replacement transaction underpriced', 'Known transaction' and 'Gas price below configured minimum gas price' are matched. Fix: add /ReplacementNotAllowed|FeeTooLow/ to bidTooLow, /AlreadyKnown/ to mayHoldBid, /InsufficientFunds|upfront gas cost exceeds/ to refusedSend (case-insensitive, no spaces assumed), and add CLI tests whose stand-in RPC answers with these exact strings.","line":26,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Predicate check (ethers 6.17.0): provider.getRpcError({method:'eth_sendRawTransaction',params:['0x02f8']},{error:{code:-32010,message:M}}) for M in 'ReplacementNotAllowed', 'FeeTooLow, FeePerGas needs to be higher than 5', 'AlreadyKnown', 'InsufficientFunds' gives code UNKNOWN_ERROR and refusedSend=false, bidTooLow=false, mayHoldBid=false; the geth strings and Nethermind 1.39+ strings classify correctly. Run scenario (README section 5's accepted case): RPC_URL backed by Nethermind 1.38.x. At step setRevealURIs the run signs T1 (tip 1 gwei, maxFee F), checkpoints the hash with fee null, the node takes T1, the process dies before the second checkpoint; the base fee rises above F. Resume: seed.name===name, last=placed=null, attempt 1 bids fees(null) = market with the same 1 gwei tip; Nethermind answers 'ReplacementNotAllowed'. Expected (what geth's words give): bidTooLow, the fresh hash removed, last raised, attempt 2 bids 25% more and replaces T1. Actual: the hash is kept, last stays null, attempts 2..8 re-sign the same market bid and get the same refusal, the step ends 'STEP_STUCK setRevealURIs (not locked): UNKNOWN_ERROR (ReplacementNotAllowed)' with the never-held hashes in pending, and every resume repeats it (waiting 5 min on them first) while T1 stays unmineable.","severity":"low","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":"Audit 8b5de2de finding 3 is not fixed for Nethermind releases before 1.39.0 (July 2026): their refusals 'ReplacementNotAllowed', 'FeeTooLow', 'AlreadyKnown' and 'InsufficientFunds' match none of bidTo"},{"citation":"resolved","description":"send() retries a reverted setRevealCommit up to twice (revertRetries:2, line 344) and never re-checks isDone. The contract refuses any second setRevealCommit with the same commit: _heldBefore(commit) is true once revealCommit==commit (EmberEvoGenesisCandidate.sol lines 312 and 339-340, checked before the acceptance), so every retry reverts REVEAL_COMMIT_INVALID whatever `previous` the retry reads. The first send happens on a stale gate once the record lost the step (README section 5: a mined entry can leave the record after a reorg while receipts lag, and notDone then decides from 3 reads of a possibly lagging node), so the first revert is a cost the README accepts; the two retries are pure waste. Submission 14 adds meetSeed to the retry path right after record() pushed this step into `failed`, so failed.some(f=>f.name===name) (line 245) is always true there: RESUME_OUT_OF_ORDER can never fire on a retry, and when another step's transaction waits at the new nonce the retry takes last=feeOf(seed.fee) and replaces that live transaction with one that is certain to revert. Fails safe (the contract refuses the repeat, the replaced step is resent later) but burns gas three times and removes a valid waiting transaction. Fix: before each revert retry run the step's done check again (same isDone, visibleReads reads) and return if it now says done; or retry only when the revert reason is REVEAL_ACCEPTANCE_EXPIRED or REVEAL_ACCEPTANCE_INVALID (eth_call at the failing block). Key meetSeed's override on a failed entry that existed before this send, not on the one this attempt just wrote.","line":284,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Resume record: steps [deploy, setRevealURIs], queued [{name:'addPaymentToken(USDC, 1600 GOLD)',nonce:N+1,hashes:[H],fee:{maxFeePerGas:'30000000000',maxPriorityFeePerGas:'2000000000'},gasLimit:null}]. On chain setRevealCommit landed at N but the record lost it; H is held in the pool. notDone('setRevealCommit') reads revealCommit()==0 three times from a lagging backend. Expected: nothing sent, or a stop once the commit is seen set. Actual: send('setRevealCommit') goes out at N and reverts REVEAL_COMMIT_INVALID (failed entry, nonce N+1). Retry 1: meetSeed finds seed H at N+1, getTransaction(H) non-null, failed has 'setRevealCommit', last=H's fee; the retry bids 1.25x over H, replaces addPaymentToken at N+1 and reverts again. Retry 2 reverts at N+2. Then TX_FAILED setRevealCommit: three reverted transactions' gas, addPaymentToken's waiting transaction gone and resent later.","severity":"low","snippet":"    if(revertRetries-->0){hashes=[];last=placed=null;await meetSeed();continue;}","title":"setRevealCommit's revert retries re-send without re-reading the gate: a stale 'not done' read costs three reverted transactions, and the retry's meetSeed (failed always holds this step's name there) o"},{"citation":"resolved","description":"plan+steps+failed is a valid lower bound only while every recorded entry is still mined. recordedReceipt checks just `top`. A reorg of two or more blocks that removes the top entry and the one below it (neither re-mined) drops only the top; the lower entry stays and the count is one too high. With floor 0 and a correct stableNonce, Math.max at line 211 picks the too-high value, the lower step's gate is skipped (stepBlock finds the stale entry, line 306), and the next step is signed at a nonce with a gap below it. Nodes queue such a transaction and never mine it, so every attempt's firstReceipt times out: 8 attempts x stepTimeoutMs (40 min) with the bid climbing, then STEP_STUCK. The specialist's claim that every resume repeats this is wrong: on the second resume the stale entry is now `top`, recordedReceipt drops it, nonce falls back to the real count, the lower step is resent and the queued transaction is handled as the seed. The cost is one wasted cycle and bids raised toward the ceiling for nothing; no gas is spent. Mainnet reorgs of 2+ blocks are rare. Fix: on resume run recordedReceipt over every steps/failed entry within a reorg window of the head, or use plan+steps+failed only after the receipt of the entry at the highest nonce is confirmed visible.","line":193,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Record: steps=[deploy@n0 (block B), setRevealURIs@n0+1 (B+1), setRevealCommit@n0+2 (B+2, top)]. A 2-block reorg replaces B+1 and B+2 and neither transaction is re-mined. Resume: recordedReceipt(top) sees another block at B+2 without its hash on 3 reads, drops setRevealCommit; setRevealURIs is never checked. stableNonce()=n0+1, planNonce+steps.length+failed.length=n0+2, nonce=n0+2. Expected: setRevealURIs (gate not done) resent at n0+1, then setRevealCommit at n0+2. Actual: the setRevealURIs gate is skipped because stepBlock finds the stale entry; setRevealCommit is signed at n0+2 behind an empty n0+1 and never mines; 8 attempts time out and the run ends STEP_STUCK. The second resume drops the stale setRevealURIs entry and recovers.","severity":"info","snippet":"  const top=[...steps,...failed].reduce((a,b)=>!a||b.blockNumber>=a.blockNumber?b:a,null);","title":"Resume re-checks only the highest record entry: an entry below it that a 2-block reorg also removed keeps plan+steps+failed one too high, so the next step is signed at a gap nonce and the run spends a"},{"citation":"resolved","description":"waiting() writes fee:feeText(placed). At the first checkpoint (line 263) the new hash is in `hashes` but `placed` is still the previous bid. After a broadcast the node took but whose answer was a non-refusal error (TIMEOUT, 502), the hash stays (right) but `placed` is never raised (line 265 is skipped), so the STEP_STUCK checkpoint at line 289 records a fee lower than the held bid. On resume line 249 sets last=placed=that older fee and fees() bids max(market, old*1.25+1), which is exactly the held bid whenever the market has not moved. For setRevealCommit, whose request changes on every resend, the new hash with the same fee is refused 'replacement transaction underpriced' and one attempt is lost; for other steps the same bytes get 'already known' and lose an attempt the same way. README section 5 accepts only the fee:null case. Within one run the same thing self-corrects after one wasted attempt. Fix: record the highest signed fee of any kept hash in pending.fee (or treat a kept hash after a non-refusal error as possibly held: placed=fee for record purposes).","line":238,"path":"Submission14/tools/deploy-mainnet.mjs","reproduction":"Attempt 1 goes out with placed P={maxFee 20 gwei, tip 1 gwei}. Attempt 2 bids F={25000000001,1250000001}, is delivered, but the RPC answers SERVER_ERROR 502: hash kept, last and placed stay P. Attempts 3..8 fail the same way (RPC flapping), STEP_STUCK writes pending.fee=P. Resume: last=P, fees(P) with the market unchanged returns F. setRevealCommit signs a new acceptance, so the hash is new and the fee equals the held F. Expected: the bid clears the held F by 10%. Actual: refused as underpriced (bidTooLow), last=F, and only the next attempt bids high enough.","severity":"info","snippet":"  const waiting=()=>({pending:{name,nonce,hashes,fee:feeText(placed),gasLimit:knownLimit===null?null:knownLimit.toString()}});","title":"pending.fee lags the bid a node holds: the pre-broadcast checkpoint and a non-refusal error record the previous bid, so a resume re-bids exactly the held fee and is refused once"}],"hash":"9eb3eea32a26cb71da0f4799f65391131fb80b36699d7076795ffeff405d6d35","nodeId":"3f746d91-2a78-46b8-8851-313e368658ae","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds 8 findings (6 low, 2 info), all in `Submission14/tools/deploy-mainnet.mjs`, each reproduced against the code with its line and snippet verified. No critical or high finding, so no Foundry proof was needed. The contract itself did not change and is byte-identical to Submission 13, as the README states.\n\n**What I kept, after merging the ten specialist findings into eight**\n\n- **Low, line 269.** A 'nonce too low' answer about a fresh transaction a gateway already delivered removes the only hash of a mined step. The run either stops NONCE_MOVED with the step missing from the record, or burns all 8 attempts into STEP_STUCK and then waits 10 minutes on a replaced hash at the next resume. The author's finding 2 test covers only a resend of identical bytes.\n- **Low, line 244 (three specialist findings merged).** meetSeed decides \"no node holds the other step's waiting transaction\" from one getTransaction answer per hash. A single null from a lagging or load-balanced backend bypasses RESUME_OUT_OF_ORDER and sends a step over a live transaction, which then reverts on chain for addPaymentToken. One RPC error goes the other way and counts as held. The TIP_CEILING held check at line 272 has the same single read.\n- **Low, line 353.** A 1-block reorg that moves the deploy or addPaymentToken to the next block leaves a stale blockNumber. The read-back then mismatches startBlock or usableAt on every resume, since only the top entry is re-read, and safe-batch accept refuses the record with RECORD_HAS_MISMATCH while the deployer key keeps owner power.\n- **Low, line 274.** An 'already known' answer means the node holds the transaction, but `sent` stays false, so the step waits 6 seconds instead of 5 minutes and replaces its own transaction at 125% of the tip.\n- **Low, line 26.** Audit finding 3 is not fixed for Nethermind releases before 1.39.0. I read the sources: 1.38.1 answers bare `ReplacementNotAllowed`, `FeeTooLow`, `AlreadyKnown` and `InsufficientFunds`, which match none of the predicates, so a refused bid never climbs and the resume loops. Nethermind switched to geth phrases in a commit from 2026-06-30, shipped in 1.39.0 on 2026-07-08, and those are matched. The specialist's `OldNonce` claim is wrong: 1.38.1 already says 'nonce too low'. I confirmed the ethers 6.17.0 classification of every string by running the three predicates in a scratch directory outside the repository.\n- **Low, line 284.** setRevealCommit's revert retries never re-read the gate. The contract refuses a repeated commit before the signature check, so a stale gate costs three reverted transactions, and the retry's meetSeed now always outbids a held later-step transaction with one certain to revert.\n- **Info, line 193.** After a 2-block reorg only the top entry is re-checked, so plan+steps+failed is one too high and the next step is signed at a gap nonce for one full STEP_STUCK cycle. The specialist claimed every resume repeats this. I traced the second resume and it recovers, so I lowered the impact.\n- **Info, line 238.** pending.fee records the previous bid after a non-refusal error, so a resume re-bids exactly the held fee and loses one attempt.\n\n**Dropped or corrected.** Nothing was dropped outright. The Nethermind finding was narrowed to pre-1.39.0 releases and its OldNonce part removed. The deep-reorg finding's \"cannot progress without hand-editing\" claim was corrected.\n\n**Limits.** The tool's own test suite cannot run in this package: `runtime.mjs`, the artifacts and the signer modules it imports are not included, so every reproduction is a trace through the code plus the ethers and client-source checks above. README questions 6 and 7 (fees at the ceiling, chainState's code check) I traced and found no defect. Besu's 'Upfront gas cost exceeds account balance' and 'Gas price below current base fee' are also unmatched, noted inside the Nethermind finding since the preflight funding check makes them hard to reach.","treeHash":null,"usage":{"cachedInputTokens":1453408,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":44364,"runtime":"claude","turns":32,"wallClockMs":599756}}],"verification":[]}