{"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":"8b5de2de-ea16-44ad-92c4-2a6437b27c5c","kind":"audit","nodes":[{"acceptedSubmissionHash":"21b8aea0085035d0786a3a69e9b929e006b22a7d9866bdffc183de44f1e29dc8","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":"df516ab648bff6273115c6ce64835bf248a792a6dfde955705ba796b27416ebc","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":"47ff5a67626670a041874e585ceb1c638902a748ac4826ab4f0fcf4998aa5d6d","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":"fd8848e5bcb2c145a80ed0c8444d0cbd41d8d959e38e3374eb437707d4a9611a","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":"3f2d1fcbcc0600a08608bff8f796d9f92969e6bc1b4e5fe997d9029d61647009","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 Submission13/ only (read Submission13/AUDIT_README.md first; Submission12 to 5 AUDIT_README are the reference for everything unchanged). Not deployed. This is the remediation of audit 161d71af (1M 5L 3I, 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 Submission12, metadata included. Changed: tools/deploy-mainnet.ps1, deploy-mainnet.mjs, safe-batch.mjs; the diff is in build/tools-and-tests.Submission12-to-13.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. recordedReceipt: can an entry leave the record when the step is on the chain, in a way the gates cannot recover? Can a stale node reach the drop?\n2. failed[] and the floor: can the floor exceed the real nonce and leave a run on a nonce gap?\n3. seedLanded: can a true gate fire for the wrong step or after a reorg? Is the nonce right on both branches?\n4. EXTRA_TOKEN_COUNT_SLOT = 11 replaces the log count and the bypass flag is gone. Is anything else needed?\n5. Fees: can mayHoldBid miss an error that means a bid is held? Can the ceiling (3x today's tip, at least 2 gwei) stop a valid run or leave a transaction unmined?\n6. Can TIP_CEILING or STEP_STUCK block a valid run for good?\n7. deploy-mainnet.ps1: every prompt is now in the try. Is any exit path left?\n8. Anything in audit 161d71af that is not actually fixed.\n\nReport severity, reproduction and fix. Not in scope: artwork, the Mint web page, World 3D.","parentJobId":null,"planHash":"a7e6e7312d844da712c7ed260ca933d09c7996c3b2ae0a7a1c560347288379c8","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"8b5de2de-ea16-44ad-92c4-2a6437b27c5c","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":"52480","feedbackHash":"01a6747ef271e1cc120c535125b9d715f024e35dee7badc1aa3ab05672923e78","nodeKey":"audit_economics","submissionHash":"21b8aea0085035d0786a3a69e9b929e006b22a7d9866bdffc183de44f1e29dc8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52615","feedbackHash":"104f2672bd397b8277ba58096e0013739922b3b6ea393b8a32001b7bd6db3150","nodeKey":"audit_flow","submissionHash":"df516ab648bff6273115c6ce64835bf248a792a6dfde955705ba796b27416ebc","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51473","feedbackHash":"35c15cb3910a5624368f3f0b247ae10d76b816e30fc0fe41a005a1bea05917ca","nodeKey":"audit_judge","submissionHash":"47ff5a67626670a041874e585ceb1c638902a748ac4826ab4f0fcf4998aa5d6d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52482","feedbackHash":"b89060b3a1dc4f2c65672976bd8c5ea6e4ec996fbaa800b30c177d4f43e28e53","nodeKey":"audit_math","submissionHash":"fd8848e5bcb2c145a80ed0c8444d0cbd41d8d959e38e3374eb437707d4a9611a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51374","feedbackHash":"1e53acc59f17d5cf1260134422183f890dcb1a795f19103b08ffe848a3f0783b","nodeKey":"audit_permissions","submissionHash":"3f2d1fcbcc0600a08608bff8f796d9f92969e6bc1b4e5fe997d9029d61647009","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"552f04252c6083d841da4940a5aea9cd8abac2e731511fa6d0ccdb6ae43bb9c0","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b0b4e7bbc84f9d80","findings":[{"citation":"resolved","description":"Finding 5 of audit 161d71af is fixed by raising the bid only on errors that can mean a node already holds a bid. The list is geth's wording: ethers' REPLACEMENT_UNDERPRICED (its regex needs both 'replacement transaction' and 'underpriced'), TIMEOUT, and /already known|known transaction|underpriced/. Nethermind, Erigon and Besu use the same strings, but reth (common behind public and load-balanced RPCs) answers a refused replacement with 'insufficient gas price to replace existing transaction', a duplicate with 'already imported', and a tip under its configured minimum with 'transaction priority fee below minimum required priority fee N' (crates/transaction-pool/src/error.rs). None of these match, ethers maps them to UNKNOWN_ERROR ('could not coalesce error (error={...message...})'), so mayHoldBid is false and `last` is not raised. The paths that depend on the climb are exactly the ones Submission 13 added: after the ceiling branch sets last=null because getTransaction answered null (section 3, bullet 2: 'the bids climb again up to the ceiling'), and a resumed record whose waiting transaction is held under a hash the record does not know (a TIMEOUT send). On reth the same market bid is refused on every remaining attempt, each followed by the 6 s receipt poll, and send() ends in STEP_STUCK; every resume repeats it while the node holds the earlier bid. The run is stalled, not broken: it clears when that transaction mines or is evicted. Fix: widen the test to the client wordings, e.g. /already known|known transaction|already imported|underpriced|insufficient gas price to replace|priority fee below minimum/i, and add a CLI test that scripts eth_sendRawTransaction to answer {code:-32000,message:'insufficient gas price to replace existing transaction'} and asserts the next bid is higher.","line":26,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"State: a resume with pending {name:'addPaymentToken(USDC, 1600 GOLD)', nonce:n, hashes:[h], fee:{maxFeePerGas:'500000000000', maxPriorityFeePerGas:'100000000000'}} (as in the test 'a recorded bid above the ceiling whose transaction no node holds is not waited for'), eth_maxPriorityFeePerGas = 1 gwei, eth_getTransactionByHash(h) answers null once (stale) while the node still holds h, and the node is reth: eth_sendRawTransaction answers {\"code\":-32000,\"message\":\"insufficient gas price to replace existing transaction\"}. Expected (Submission 13 section 3): the market bid is refused, last is raised, the bids climb 1.25x per attempt up to the 3 gwei ceiling and the replacement goes out. Actual: fees(last) throws TIP_CEILING (125 gwei > 3 gwei), held=false so last=null; attempt 2 sends the 1 gwei market bid, reth refuses it, ethers yields code UNKNOWN_ERROR with the reth text in e.message, mayHoldBid returns false, last stays null; attempts 3..8 send the identical 1 gwei bid and are refused the same way (6 s each); send() throws 'STEP_STUCK addPaymentToken(USDC, 1600 GOLD)'. With geth's wording ('replacement transaction underpriced') the same script climbs 1.25, 1.56, 1.95, 2.44 gwei as the README describes. Same miss for 'already imported' and 'transaction priority fee below minimum required priority fee'.","severity":"low","snippet":"const mayHoldBid=e=>e.code==='REPLACEMENT_UNDERPRICED'||e.code==='TIMEOUT'||/already known|known transaction|underpriced/i.test(String(e.message));","title":"mayHoldBid misses reth's refusal wording, so a held bid is not climbed on a reth-backed RPC and the step ends STEP_STUCK (audit 161d71af finding 5, partial)"},{"citation":"resolved","description":"Answer to section 4 questions 1 and 2. recordedReceipt returns null only after visibleTries (90) polls with no receipt for any recorded hash and the last visibleReads (3) polls answered from a head at or past the entry's block. On a single node that is impossible for a transaction that is on the chain, and on a load-balanced RPC it needs every receipt read for 3 minutes to come from a node behind the block while the head reads come from a node past it: unlikely, but it is the one model the tool documents (a split backend). When it happens the entry leaves the record and, with the new line 182, the waiting transaction's nonce no longer bounds the resume: floor=0 and planNonce+steps.length+failed.length is one short, so nonce comes from stableNonce alone. Two equal stale nonce reads then set nonce to the dropped step's own nonce. The dropped step's gate (fresh node) says done, so the run goes on to the waiting step at that stale nonce; because seed.nonce is one higher, send() neither throws RESUME_OUT_OF_ORDER nor reuses the seed's hashes, the node answers 'nonce too low', and line 230 checkpoints state() without the pending entry before throwing NONCE_MOVED. The record now lacks both the dropped step and the waiting transaction's hashes and fee. The next resume still cannot deploy twice or skip a step (the gates decide from the chain, the step lands under the forgotten hash or is replaced), but it costs up to one stepTimeoutMs wait per attempt and the final record loses that step's hash, gasUsed and blockNumber, which safe-batch's recordBlock and the read-back's usdcUsableAt check then do without. Suggested fix: keep the seed's hashes when NONCE_MOVED is thrown (checkpoint(state(seed?{pending:seed}:{})) at line 230), and when the top is dropped keep floor at seed.nonce-1 for a seed whose nonce is the dropped entry's nonce+1 instead of 0, so the stale nonce cannot go below the step that must fill the gap.","line":182,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Scripted RPC in tests/contract/deploy-mainnet-cli.test.mjs style: stoppedRun(stopBefore:4, mined:false) gives steps [deploy, setRevealURIs, setRevealCommit (nonce n, block b)] and pending {addPaymentToken, nonce n+1, hashes:[h4], fee}. Script: eth_getTransactionReceipt(top.hash) -> null for every call; eth_blockNumber -> real (>= b); eth_getTransactionCount -> toQuantity(n) for the first 4 calls (the stable reads at line 188 and the NONCE_MOVED check), then real (n+1); eth_call revealCommit() -> real. Expected: the pending entry for addPaymentToken (h4) survives in the record whatever the stale reads say. Actual: recordedReceipt returns null after 90 polls, topLeft=true, floor=0, nonce=max(n, 0, n)=n; notDone('setRevealCommit') reads the real commit and returns false; notDone('addPaymentToken') is true, send() goes to nonce n with hashes=[], the node answers nonce too low, stableNonce() now reads n+1 > n, checkpoint(state()) is written without `pending` and NONCE_MOVED is thrown. The checkpoint passed to deployMainnet has no pending key and no setRevealCommit entry; h4 is no longer anywhere in the record.","severity":"info","snippet":"  let floor=topLeft?0:seed?.nonce??0;   // a waiting transaction above a nonce that came free cannot land before that nonce is filled","title":"A stale drop of the top entry (floor=0) plus a stale nonce read reaches NONCE_MOVED, whose checkpoint discards the live pending entry of the next step"}],"hash":"21b8aea0085035d0786a3a69e9b929e006b22a7d9866bdffc183de44f1e29dc8","nodeId":"e12b76e1-017b-4f98-850e-0785995ff523","outcome":"completed","summary":"The review is complete. Findings are in `.imd-findings.json` (one low, one info). Nothing else in the tree was changed.\n\n**What I verified**\n\n- **Slot 11 is right.** I compiled the pinned standard input in a scratch directory with solc 0.8.38 and the same settings. The storage layout puts `extraPaymentTokenCount` at slot 11, alone in its word, after ERC721 (0–5), Ownable (6), Ownable2Step (7), EIP712 fallbacks (8–9) and `paymentTokens` (10). Nothing else is needed for the read itself.\n- **recordedReceipt and the drop.** An entry leaves the record only after 90 polls with no receipt and the last three answered from a head past the entry's block. A single lagging node cannot reach it. Only a split backend lagging on receipts for three minutes can, and even then the gates decide from the chain, so no second deploy and no skipped step.\n- **failed[] and the floor.** Every entry corresponds to one spent nonce. The floor can exceed the chain only through a reorg deeper than the dropped top entry, which the README accepts.\n- **seedLanded.** It fires only for the seed's own step. The receipt branch uses the seed's nonce plus one. The no-receipt branch reads the chain. Both are right.\n- **Fees and the ceiling.** Each bump is 25 percent on both tip and max fee, which satisfies the 10 percent replacement rule. The ceiling stops only the tip, and max fee follows the market, so a base fee rise is covered by the first replacement.\n- **deploy-mainnet.ps1.** Every prompt and every secret assignment is inside the try. The finally clears the variables and PowerShell runs finally on Ctrl+C. No exit path leaks.\n- **Audit 161d71af mapping.** Findings 1 to 4 and 6 to 9 are fixed as described. Finding 5 is fixed for geth, Nethermind, Erigon and Besu wording but not for reth.\n\n**Findings**\n\n1. **Low, deploy-mainnet.mjs line 26.** `mayHoldBid` misses reth's refusal strings: \"insufficient gas price to replace existing transaction\", \"already imported\" and \"transaction priority fee below minimum required priority fee\". Ethers maps them to UNKNOWN_ERROR, so the bid is not raised. On a reth-backed RPC the path Submission 13 added (a stale null drops the bid to market, then \"the bids climb again\") does not climb. The same market bid is refused on every attempt and the step ends in STEP_STUCK until the held transaction mines or is evicted. Fix: widen the regex to those wordings and add a scripted test.\n2. **Info, deploy-mainnet.mjs line 182.** When the top entry is dropped (floor zero) and two nonce reads are stale, the run reaches NONCE_MOVED at the waiting step's predecessor nonce, and that checkpoint writes the record without the pending entry. The waiting transaction's hash, fee and gas limit are lost. Recovery still happens from the chain, at the cost of waits and a thinner final record. Fix: keep the seed in the NONCE_MOVED checkpoint and bound the floor by the seed's nonce minus one instead of zero.\n\n**Limits of this review.** The package has no `runtime.mjs` or artifacts, so the Node test suite could not be run here. Both reproductions are scripted-RPC scenarios in the style of the existing CLI tests, and the reth and Nethermind wordings were read from their current source on GitHub. No contract test was needed: the contract is unchanged and no finding is in Solidity.\n\nSources: [reth transaction-pool error.rs](https://raw.githubusercontent.com/paradigmxyz/reth/main/crates/transaction-pool/src/error.rs), [Nethermind TxPoolErrorMessages.cs](https://raw.githubusercontent.com/NethermindEth/nethermind/master/src/Nethermind/Nethermind.TxPool/TxPoolErrorMessages.cs), [Nethermind AcceptTxResult.cs](https://raw.githubusercontent.com/NethermindEth/nethermind/master/src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs), [ethers provider-jsonrpc.ts](https://raw.githubusercontent.com/ethers-io/ethers.js/main/src.ts/providers/provider-jsonrpc.ts), [reth PoolErrorKind docs](https://reth.rs/docs/reth/transaction_pool/error/enum.PoolErrorKind.html), [execution-apis issu","treeHash":null,"usage":{"cachedInputTokens":1543726,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":58650,"runtime":"claude","turns":28,"wallClockMs":846001}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"13603a08ada71730","findings":[{"citation":"resolved","description":"The check that a node is 'past the block' (eth_blockNumber) and the receipt lookup (eth_getTransactionReceipt per hash) are separate JSON-RPC requests. A load-balanced RPC sends each one to any backend. So a fresh backend can answer the head while a backend behind top.blockNumber answers the receipt. A node that has not seen a transaction returns null, not an error, so `answered` stays true and `empty` grows. If the last visibleReads (3) of the 90 iterations happen this way, recordedReceipt returns null. Line 179 then splices the entry, topLeft=true, and the checkpoint at line 277 writes the record without it. The entry is lost for good: notDone() returns false on a done gate without recording anything, so no later resume puts the step's hashes, gasUsed or blockNumber back. totalGas is undercounted, and stepBlock() no longer bounds that step. If it was addPaymentToken, the usableAt cross-check at line 330 is skipped. The drop also sets floor=0 and lowers planNonce+steps+failed by one. A stale stableNonce in the same resume then sends the next step at a spent nonce (NONCE_EXPIRED, then NONCE_MOVED), and that checkpoint also discards any pending seed (see the separate finding). The package's own test only covers a node that is behind on both reads together (eth_blockNumber scripted to top.blockNumber-1). The package's own lag model, lagger() in tests/contract/deploy-mainnet-cli.test.mjs, routes every request on its own and makes this split. Fix: drop an entry only when a reorg is proven. Store rc.blockHash in record(). On resume, read eth_getBlockByNumber(top.blockNumber) twice with the same hash, and drop only if that hash differs from the recorded blockHash and no recorded hash has a receipt whose blockHash is canonical. Otherwise throw RECEIPT_NOT_VISIBLE, as for a node behind.","line":159,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"State: a partial record with steps [deploy (block 100), setRevealURIs (105), setRevealCommit (110, hashes [h])]. The resume runs at head 400 through an RPC where half the backends lag (the test lagger with share 0.5: a lagging eth_blockNumber still answers 398, and a lagging eth_getTransactionReceipt answers null). In iterations 88, 89 and 90 of recordedReceipt, eth_getTransactionReceipt(h) goes to a lagging backend. This happens with probability 0.5^3 = 12.5% for a one-hash entry. Expected: the entry stays, or RECEIPT_NOT_VISIBLE leaves the record unwritten. Actual: recordedReceipt returns null and the log says 'setRevealCommit not on the chain any more'. The record written at line 277 has steps [deploy, setRevealURIs]. The gate for setRevealCommit is done, so the step is never re-recorded, and the final 1-<time>.json lacks setRevealCommit and its gas.","severity":"low","snippet":"const n=await provider.getBlockNumber().catch(()=>-1);let answered=n>=top.blockNumber;","title":"recordedReceipt: a stale receipt node reaches the drop, because the head and the receipt are read from separate requests"},{"citation":"resolved","description":"mayHoldBid (line 26) now counts TIMEOUT as 'a bid may be held', and the next bid is raised past it. The transaction was signed locally, though, and may have reached the pool, and its hash is only pushed into `hashes` after sendTransaction returns. When eth_sendRawTransaction times out (ethers' default request timeout is 300 s), that transaction is often mined while the tool waits. From then on every attempt at this nonce fails with NONCE_EXPIRED. Line 230 converts that to NONCE_MOVED only when `hashes` is empty. Otherwise firstReceipt keeps watching only the earlier hashes. After `attempts` (8) tries the step ends in STEP_STUCK, although it landed, and the checkpoint at line 242 writes a pending entry without the landed hash. The next resume recovers through the gate, but the step's receipt is never recorded: gas is undercounted, and for addPaymentToken the usableAt check at line 330 is skipped. Fix: sign first (`const signed=await deployer.signTransaction(await deployer.populateTransaction({...req,gasLimit:limit,nonce,...fee}))`), push ethers.keccak256(signed) into `hashes` and checkpoint, then broadcastTransaction(signed). A TIMEOUT or 'already known' then leaves the held transaction watched by firstReceipt.","line":218,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Step setRevealURIs at nonce k. Attempt 1 places h1 (hashes=[h1]), which is not mined in 5 min. Attempt 2 signs h2 at 1.25x the fee. The RPC forwards h2 to the pool but the HTTP reply times out, so ethers throws code TIMEOUT. last=fee and h2 is not in hashes. h2 is mined at nonce k. Attempts 3 to 8 get NONCE_EXPIRED, and firstReceipt([h1]) returns null. Expected: the receipt of h2 is recorded and the run moves on to setRevealCommit. Actual: Error 'STEP_STUCK setRevealURIs' and exit 1. The record holds pending {nonce:k, hashes:[h1]}, and h2 never appears in steps.","severity":"low","snippet":"const tx=await deployer.sendTransaction({...req,gasLimit:limit,nonce,...fee});","title":"A send that ends in TIMEOUT is treated as a held bid, but its hash is thrown away, so the held transaction is never watched"},{"citation":"resolved","description":"Each raise is fixed at 25% (line 134: prev*125/100+1), but a node needs only +10% on both maxPriorityFeePerGas and maxFeePerGas to accept a replacement. When the 25% raise passes the ceiling, fees() throws instead of bidding the ceiling, even when ceiling >= prev*1.1. The handler at lines 223-226 then only waits for the held transaction. If that transaction is unmineable because its maxFeePerGas is below the current base fee, the step ends in STEP_STUCK after 8 x 5 min and every resume repeats it. A replacement under the ceiling, with a fresh maxFeePerGas (2 x base + tip), would have been mined. Section 5 accepts this only for a transaction 'at the ceiling'. Here it hits one up to 25% below it. Fix: when up(tip,prev) > ceiling but prev*110n/100n+1n <= ceiling, bid maxPriorityFeePerGas = ceiling, keeping maxFeePerGas = max(market, prev*110/100+1). Throw TIP_CEILING only when even the 10% raise passes the ceiling.","line":138,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Today's eth_maxPriorityFeePerGas is 1 gwei, so the ceiling is max(3, 2) = 3 gwei. One run's bids for a step go 1, 1.25, 1.5625, 1.953, 2.441 gwei of tip, each held unmined for 5 min because the base fee rose above each maxFeePerGas. Attempt 6 computes 2.441 x 1.25 = 3.05 gwei > 3 gwei and gets TIP_CEILING. The transaction is held, so the run only waits, then ends in 'STEP_STUCK ...: TIP_CEILING'. Expected: a replacement at a 3 gwei tip (+22.9%, above the 10% rule) with maxFeePerGas = 2 x base + tip is accepted and mined. Actual: no bid is sent, and every resume with the same fees stops the same way until the base fee falls back under the old maxFeePerGas.","severity":"low","snippet":"if(bid.maxPriorityFeePerGas>ceiling)throw Object.assign(Error('TIP_CEILING the next bid passes '+ceiling+' wei of tip: check the deployer on Etherscan and resume later'),{code:'TIP_CEILING'});","title":"TIP_CEILING stops a step when a valid 10% replacement still fits under the ceiling"},{"citation":"resolved","description":"New code in section 3: after a drop (topLeft) the floor ignores seed.nonce, so nonce < seed.nonce and the seed stays in memory while an earlier step refills the freed nonce. Every checkpoint inside send() for that earlier step drops the seed's pending entry. Line 220 writes state(waiting()), which holds the current step's pending. Lines 230 and 235 write state() with no pending. Only the in-memory `seed` still knows the later step's hashes and fee. If the run stops before that later step is reached (STEP_STUCK or TIP_CEILING on the refilled step, RECEIPT_NOT_VISIBLE in settled, a kill), the next resume has no seed. It does not watch the old hashes, does not bid over the old fee, and does not record that transaction when it lands. For setRevealCommit, a late landing after more than 256 blocks reverts and spends a nonce that never reaches failed[]. Fix: in send(), write `pending` as the current step's waiting entry plus the outstanding seed, for example `pending:[...]` or an extra `queued` field. Make the state() calls at lines 230 and 235 keep `seed` while seed.nonce > nonce.","line":220,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Record: steps [deploy (nonce k0), setRevealURIs (nonce k1, block B)] and pending {name:'setRevealCommit', nonce:k1+1, hashes:[h]}. A reorg removed block B, and no hash of setRevealURIs is held. Resume: recordedReceipt returns null, topLeft=true, and nonce = k1 with the seed kept. send('setRevealURIs') places h' at k1, and line 220 checkpoints pending = {name:'setRevealURIs', nonce:k1, ...}. Kill the process now, or let h' stay unmined until STEP_STUCK. Expected: the partial record still lists h for setRevealCommit at k1+1. Actual: the partial file has no reference to h, so the next resume neither watches nor outbids it.","severity":"info","snippet":"checkpoint(state(waiting()));","title":"After a dropped entry, the refilled step's checkpoints erase the waiting transaction of the later step from the record"}],"hash":"3f2d1fcbcc0600a08608bff8f796d9f92969e6bc1b4e5fe997d9029d61647009","nodeId":"a43a3082-d09c-4e6b-aadb-d7dfd5c3775c","outcome":"completed","summary":"I found 3 Low and 1 Info finding in the deployment tools, all in `Submission13/tools/deploy-mainnet.mjs`, and nothing above Low. They are in `.imd-findings.json`, and each snippet matches the file at the line given. None carries a Foundry proof, since none is Solidity or high severity. I couldn't run the package's Node tests here (`tools/runtime.mjs`, the artifacts and ethers aren't in it), so every reproduction is traced through the code, using the package's own stale-node model (`lagger()` in the tests).\n\n**Findings**\n1. **Low, question 1 (`deploy-mainnet.mjs:159`): a stale node can make the resume drop an entry.** The \"is the node past this block\" read and the receipt read are separate requests. A load-balanced RPC can answer the first from a current node and the second from one that is behind and returns null. That counts as \"the transaction is gone\", so a step that is on chain is dropped from the record and written out at line 277. No later resume puts it back: gas is undercounted and, for `addPaymentToken`, the `usableAt` check is skipped. The package's own test only covers a node that is behind on both reads. Under `lagger()` at a 50% lag rate this happens about one time in eight for a one-hash entry. Fix: store each receipt's block hash, and drop an entry only when the block at that height reliably shows a different hash.\n2. **Low, questions 5 and 8 (`:218`): a timed-out send's hash is thrown away.** `mayHoldBid` now treats a TIMEOUT as a held bid, but the hash of that signed transaction is never kept. If it gets mined, every later attempt fails with \"nonce too low\" while the tool watches only the older hashes. The step ends in `STEP_STUCK` although it landed, and its receipt never reaches the record. Fix: sign first, record the hash, then broadcast.\n3. **Low, questions 5 and 6 (`:138`): `TIP_CEILING` refuses a replacement that would be accepted.** Each raise is fixed at 25%, but a node accepts a replacement at +10%. With today's tip at 1 gwei, the ceiling is 3 gwei. A held tip of 2.44 gwei would next go to 3.05 gwei, so the tool throws and only waits, although a 3 gwei replacement would be valid. The run ends in `STEP_STUCK` if the held transaction's max fee is below the base fee. This is not the \"at the ceiling\" case section 5 accepts. Fix: bid the ceiling when a 10% raise still fits under it.\n4. **Info, new code from section 3 (`:220`): refilling a freed nonce erases the later step's waiting transaction from the record.** After a dropped entry, the checkpoints at lines 220, 230 and 235 write the record without the later step's waiting transaction. If the run then stops, the next resume no longer knows that transaction's hashes or fee.\n\n**No defect found:**\n- **Question 2 (the floor):** no sequence I traced counts a nonce twice in `planNonce + steps + failed`.\n- **Question 3 (`seedLanded`):** it only fires for the waiting step's own name, and the nonce is right on both branches. A stale nonce read can still stop a run with `NONCE_MOVED`, but a resume recovers it.\n- **Question 4 (slot 11):** I worked through the OpenZeppelin v5 storage layout by hand rather than with a compiler run: `paymentTokens` sits in slot 10 and `extraPaymentTokenCount` in slot 11. The count can only go up, and the deployment read-back already confirmed USDC. So `extraTokens === '1'` is enough and nothing else is needed.\n- **Question 6:** apart from finding 3, `TIP_CEILING` and `STEP_STUCK` block a run only in the cases section 5 already accepts.\n- **Question 7 (`deploy-mainnet.ps1`):** every secret prompt and environment variable is inside the `try`, and the `finally` clears them (it also runs on Ctrl+C). I found no exit path left.\n- **Question 8:** findings 1, 3, 4, 6, 7 and 9 of audit 161d71af are fixed. Findings 2 and 5 are fixed, apart from the gaps in my findings 1 and 2.\n\nCoverage: I did the access-control, trust-gap and asymmetry passes over the three changed files, with the deploy logic in most depth. I did not re-review the unchanged c","treeHash":null,"usage":{"cachedInputTokens":997794,"inputTokens":29,"model":"claude-opus-5-5","outputTokens":39919,"runtime":"claude","turns":17,"wallClockMs":432157}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"dc34db8e17664ebd","findings":[{"citation":"resolved","description":"The drop condition in recordedReceipt is built from two separate JSON-RPC requests: eth_blockNumber (line 159, `answered=n>=top.blockNumber`) and eth_getTransactionReceipt per hash (line 161). On a load-balanced or method-routed RPC (the one model the tool documents) the node that reported the height is not the node that answered the receipt read, so `answered` does not show that a node holding the block lacked the receipt. A node that has not indexed the transaction returns null, not an error, so `empty` grows and after visibleTries polls with no receipt the function returns null. Note this needs every one of the visibleTries receipt reads to be null (line 162 returns on the first receipt), so random per-request lag does not reach it; persistent routing (receipt calls served by a backend whose tx index is behind or has a gap, head calls by a current head tracker) does. Line 179 then splices the entry and line 277 checkpoints the record without it. For a steps[] entry the gate recovers the step from the chain but it is never re-recorded (notDone returns false on a done gate without recording), so stepBlock() is undefined, totalGas and safe-batch's recordBlock miss it. For a failed[] entry there is no gate at all: the reverted transaction's gasUsed leaves totalGas for good, and the count floor planNonce+steps+failed (the guard audit 161d71af finding 3 added against stale nonce reads) drops by one. With the drop and two equal stale eth_getTransactionCount answers, the resume signs the next step at the spent nonce and depends on the node's 'nonce too low' to stop (NONCE_MOVED). The package's own test only covers a node behind on both reads together. Merged from audit_permissions and audit_flow. Fix: prove the absence in one request answered by one node: read eth_getBlockByNumber(top.blockNumber, true) (or by the recorded blockHash, stored in record()) and drop only when that block exists, its hash differs from the recorded one, and its transaction list holds none of top.hashes; a null block counts as 'not answered'. Otherwise throw RECEIPT_NOT_VISIBLE as for a node behind.","line":159,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Reproduced with the Submission13 deploy-mainnet.mjs behind a scripted JSON-RPC (ethers 6.17, real Wallet signing). State: a run where the first setRevealCommit was mined with status 0 (record: steps [deploy, setRevealURIs], failed [setRevealCommit at block 1003, hash R], pending setRevealCommit nonce 3 with no hashes); the chain holds receipt R with status 0x0, head 1003, nonce 3. Resume with a script that answers eth_getTransactionReceipt(R) with null on every call and everything else (eth_blockNumber 1003 >= 1003, nonce, code, calls) truthfully. Expected: the entry stays (R is mined) or the run stops with RECEIPT_NOT_VISIBLE because no node holding block 1003 was shown to lack R. Actual: recordedReceipt returns null, the first checkpoint written on the resume has failed=[] and R appears in no checkpoint; the run finishes with final failed=[] and totalGas 126000 (R's gas gone). Variant with the first two eth_getTransactionCount answers stale at planNonce+2 (=2): nonce = max(2, 0, planNonce+2+0=2) = 2, setRevealCommit is signed and sent at the spent nonce 2 ('nonce too low'), and the run ends with 'NONCE_MOVED setRevealCommit: the chain is past nonce 2'. Without the drop the floor planNonce+steps+failed = 3 would have sent at 3.","severity":"low","snippet":"   const n=await provider.getBlockNumber().catch(()=>-1);let answered=n>=top.blockNumber;\n   for(const h of hashes){\n    const r=await provider.getTransactionReceipt(h).catch(()=>{answered=false;return null;});\n    if(r)return r;\n   }\n   empty=answered?empty+1:0;\n   await sleep(pollMs);\n  }\n  if(empty>=visibleReads)return null;","title":"recordedReceipt drops a mined entry on the word of two different backends: a receipt-serving node that lacks the receipt while the head read is past the block takes a steps[] or failed[] entry out of "},{"citation":"resolved","description":"`deployer` is an ethers Wallet: the transaction is signed locally, so its hash is known before eth_sendRawTransaction goes out, but line 219 pushes `tx.hash` into `hashes` only when `deployer.sendTransaction` returns. If the RPC call fails after the node accepted the transaction, the hash is lost. mayHoldBid (line 26) counts TIMEOUT as a held bid, so attempt a+1 sends a 25% higher replacement at the same nonce; when the first transaction is mined before that, the replacement gets NONCE_EXPIRED with `hashes` empty, stableNonce() > nonce, and line 230 throws NONCE_MOVED after a step that in fact succeeded. The checkpoint at line 230 holds no hash for the step. On the resume the gate says done and the step is skipped, but it never enters steps: stepBlock(name) stays undefined, totalGas and the record miss it, and for addPaymentToken(USDC, 1600 GOLD) the usableAt read-back at line 330 is silently skipped. This is the 'lost hash' class the README accepts in section 5, but here the tool's own send path creates it. Merged from audit_permissions and audit_math. Fix: populate and sign first (`const signed=await deployer.signTransaction(await deployer.populateTransaction({...req,gasLimit:limit,nonce,...fee}))`), push `ethers.Transaction.from(signed).hash` into `hashes`, checkpoint(state(waiting())), then `provider.broadcastTransaction(signed)`; a broadcast error then leaves a hash the receipt logic can wait for.","line":218,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Reproduced with the Submission13 deploy-mainnet.mjs behind a scripted JSON-RPC. Fresh run; deployer.sendTransaction is wrapped so that for the second step (setRevealURIs, nonce 1) the real send runs (the node takes and mines H at block 1002) and then {code:'TIMEOUT'} is thrown, as a lost HTTP reply does. Expected: the tool knows H (it signed it), waits for its receipt, records the step and goes on. Actual: sent nonces [0, 1, 1] with tips [1.0, 1.0, 1.25 gwei]: the retry bids 25% higher at nonce 1, the node answers 'nonce too low', and the run throws 'NONCE_MOVED setRevealURIs (not locked): the chain is past nonce 1'. H appears in no checkpoint. Resuming from the last checkpoint finishes with readBackMismatch [] but d.steps is [deploy, setRevealCommit, addPaymentToken, approvePaidPrice, transferOwnership]: setRevealURIs is never recorded and its gas is missing from totalGas.","severity":"low","snippet":"    const tx=await deployer.sendTransaction({...req,gasLimit:limit,nonce,...fee});\n    hashes.push(tx.hash);knownLimit=limit;last=placed=fee;sent=true;log(name,tx.hash);","title":"A send whose reply is lost after the node took the transaction (TIMEOUT, dropped connection, 502 on the response) throws the hash away: the landed step is never recorded, the run stops with NONCE_MOVE"},{"citation":"resolved","description":"The list is geth's wording: ethers' REPLACEMENT_UNDERPRICED (its regex in provider-jsonrpc.js needs both 'replacement transaction' and 'underpriced'), TIMEOUT, and /already known|known transaction|underpriced/. reth (crates/transaction-pool/src/error.rs at main, fetched 2026-11-03) answers a refused replacement with 'insufficient gas price to replace existing transaction', a duplicate with 'already imported', and a tip under its minimum with 'transaction priority fee below minimum required priority fee N'. None of these match (only reth's generic 'transaction underpriced' does), ethers maps them to UNKNOWN_ERROR, mayHoldBid is false and `last` is not raised. The paths that depend on the climb are the ones Submission 13 added: the ceiling branch that sets last=null when getTransaction answered null (README section 3, 'the bids climb again up to the ceiling'), and a resume whose waiting transaction is held under a hash the record does not know. On reth the identical market bid is refused on every remaining attempt, each followed by the receipt wait, and send() ends in STEP_STUCK; every resume repeats it while the node holds the earlier bid. Fix: widen the test, e.g. /already known|known transaction|already imported|underpriced|insufficient gas price to replace|priority fee below minimum/i, and add a CLI test that scripts eth_sendRawTransaction to answer {code:-32000,message:'insufficient gas price to replace existing transaction'} and asserts the next bid is higher.","line":26,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Reproduced with the Submission13 deploy-mainnet.mjs behind a scripted JSON-RPC, eth_maxPriorityFeePerGas = 1 gwei. The second step's eth_sendRawTransaction is answered with a -32000 error three times, then taken. With geth's message 'replacement transaction underpriced' the tips at nonce 1 are 1, 1.25, 1.5625, 1.953 gwei (the climb the README describes). With reth's 'insufficient gas price to replace existing transaction', 'already imported' or 'transaction priority fee below minimum required priority fee 1000000000' the tips at nonce 1 are 1, 1, 1, 1 gwei: the same bid is resent unchanged. Expected: a refusal that can only mean a bid is held raises the next bid on every client wording the tool may meet. Actual: on reth the bid never rises, so a held transaction above the market bid is never outbid and with 8 attempts the step ends STEP_STUCK.","severity":"low","snippet":"const mayHoldBid=e=>e.code==='REPLACEMENT_UNDERPRICED'||e.code==='TIMEOUT'||/already known|known transaction|underpriced/i.test(String(e.message));","title":"mayHoldBid misses reth's refusal wording, so on a reth-backed RPC a held bid is never climbed and every attempt resends the same refused bid (audit 161d71af finding 5, partially fixed)"},{"citation":"resolved","description":"New code in section 3: when the top entry is dropped (topLeft) the floor ignores seed.nonce (line 182), so nonce < seed.nonce, the seed stays in memory (line 189) and an earlier step refills the freed nonce. Every checkpoint inside send() for that earlier step drops the seed's pending entry: line 220 writes state(waiting()), whose `pending` is the current step, and lines 230 and 235 write state() with no pending at all. Only the in-memory `seed` still knows the later step's hashes, fee and gasLimit. If the run stops before that later step is reached (STEP_STUCK or TIP_CEILING on the refilled step, RECEIPT_NOT_VISIBLE in settled, a kill, or NONCE_MOVED at line 230 after a stale nonce read sent the later step at the dropped step's nonce), the next resume has no seed: it does not watch the old hashes, does not bid over the old fee, and does not record that transaction when it lands; a market bid at that nonce is refused as underpriced while the old one is held, and for setRevealCommit a late landing after more than 256 blocks reverts and spends a nonce that never reaches failed[]. Merged from audit_permissions and audit_economics. Fix: in send(), write the current step's waiting entry plus the outstanding seed (for example a `queued` field or `pending` as a list) whenever seed.nonce > nonce, and make the state() checkpoints at lines 230 and 235 keep the seed the same way.","line":220,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Reproduced with the Submission13 deploy-mainnet.mjs behind a scripted JSON-RPC. Record from a run stopped before its 4th send: steps [deploy, setRevealURIs, setRevealCommit (nonce 2)], pending {addPaymentToken(USDC, 1600 GOLD), nonce 3, hashes [h4]}, h4 held in the pool. The chain is then set as a reorg left it: no receipt for setRevealCommit's hash, revealCommit() zero, nonce 2 free again; the refill of setRevealCommit is taken by the node but not mined before the run gives up (attempts 2). Expected: every checkpoint still names h4 at nonce 3 so the next resume watches and outbids it. Actual: checkpoint 0 (line 277) has pending addPaymentToken nonce 3 and mentions h4; checkpoints 1 to 3 (line 220 and the final line 242 before 'STEP_STUCK setRevealCommit') have pending setRevealCommit nonce 2 and none of them mentions h4. The partial record on disk no longer references the live transaction at nonce 3.","severity":"low","snippet":"    checkpoint(state(waiting()));","title":"After a dropped entry, the refilled step's checkpoints (and the NONCE_MOVED checkpoint) erase the waiting transaction of the later step from the record, so a resume neither watches nor outbids it"},{"citation":"resolved","description":"Each raise is fixed at prev*125/100+1 (line 134), but a node needs only +10% on both maxPriorityFeePerGas and maxFeePerGas to accept a replacement. When the 25% raise passes the ceiling, fees() throws (line 138) instead of bidding the ceiling, even when the ceiling is at least prev*1.1. The handler at lines 223 to 226 then only waits for the held transaction; if that one is unmineable because the base fee rose above its maxFeePerGas, the step ends STEP_STUCK after attempts x stepTimeoutMs and every resume with the same market fees repeats it. README section 5 accepts this only for a transaction 'at the ceiling'; here the last bid is 18.6% below it (2.44 of 3 gwei), and a replacement at 3 gwei with a fresh maxFeePerGas would have been accepted and mined. Fix: when up(tip,prev) > ceiling but prev*110n/100n+1n <= ceiling, bid maxPriorityFeePerGas = ceiling and maxFeePerGas = max(market, prev.maxFeePerGas*110/100+1); throw TIP_CEILING only when even the 10% raise passes the ceiling.","line":134,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Reproduced with the Submission13 deploy-mainnet.mjs behind a scripted JSON-RPC: eth_maxPriorityFeePerGas = 1 gwei (ceiling max(3 gwei, 2 gwei) = 3 gwei), every eth_sendRawTransaction is taken by the node and held unmined (base fee above each maxFeePerGas). Tips bid for the deploy step: 1, 1.25, 1.5625, 1.953, 2.441 gwei; attempt 6 computes 2.441 x 1.25 = 3.05 gwei > 3 gwei and fees() throws TIP_CEILING; no further bid is sent and the run ends 'STEP_STUCK deploy EmberEvoGenesis: TIP_CEILING the next bid passes 3000000000 wei of tip'. Expected: a bid at the 3 gwei ceiling, which is +22.9% over 2.441 gwei (above the 10% rule), goes out with a fresh maxFeePerGas. Actual: the highest bid ever sent is 2.441 gwei, and the package's own test asserts exactly this (tips.at(-1)*125n/100n+1n > 3 gwei) as the intended stop.","severity":"low","snippet":"  const up=(v,prev)=>{const bumped=prev==null?0n:prev*125n/100n+1n;return v>bumped?v:bumped;};\n  const tip=f.maxPriorityFeePerGas??0n,bid={maxFeePerGas:up(f.maxFeePerGas,last?.maxFeePerGas),maxPriorityFeePerGas:up(tip,last?.maxPriorityFeePerGas)};\n  // the tip is paid in full: bids stop at a multiple of today's tip, also across resumes. A waiting transaction above it is only waited for\n  const ceiling=tip*TIP_CEILING_TIMES>TIP_CEILING_FLOOR?tip*TIP_CEILING_TIMES:TIP_CEILING_FLOOR;\n  if(bid.maxPriorityFeePerGas>ceiling)throw Object.assign(Error('TIP_CEILING the next bid passes '+ceiling+' wei of tip: check the deployer on Etherscan and resume later'),{code:'TIP_CEILING'});","title":"TIP_CEILING fires on a 25% raise when a replacement 10% to 25% above the last bid would still fit under the ceiling: the step waits on an unmineable transaction and ends STEP_STUCK instead of bidding "},{"citation":"resolved","description":"seedLanded() runs only after a gate said the waiting step is done. When no receipt for seed.hashes arrives within stepTimeoutMs, line 195 sets nonce = max(stableNonce(), planNonce+steps.length+failed.length) and discards both bounds the run already had: the current `nonce` and seed.nonce+1. stableNonce() accepts two equal 'latest' counts, and a lagging node (the tool's own model) returns two equal old counts. The record cannot cover the gap: a step that landed under a hash the run never saw is in neither steps[] nor failed[], so the count is at most seed.nonce. The result is a nonce at or below seed.nonce, which the gate's own answer shows is spent. Submission 12 used seed.nonce+1, which is right in exactly this case; the README's reason for the change (a step that landed at some other nonce) does not need the existing bound to go away. The send that follows cannot do harm (NONCE_EXPIRED, then NONCE_MOVED if the next read is fresh, or NONCE_EXPIRED after all attempts) but a valid resume stops and a person has to run it again although the chain had already settled the question. Fix: nonce = Math.max(nonce, seed.nonce+1, <read>, planNonce+steps.length+failed.length), reading the count until a value above seed.nonce is seen (a lagging node can only report too low), and a named error if none is seen within visibleTries.","line":195,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Reproduced with the Submission13 deploy-mainnet.mjs behind a scripted JSON-RPC. Record: planNonce 0, steps [deploy@0, setRevealURIs@1], pending {setRevealCommit, nonce 2, hashes [H]}. On the chain setRevealCommit was mined at nonce 2 under another hash H2 (a resend the run never heard back about): getTransactionCount is 3 on fresh nodes, revealCommit() is set, H has no receipt. The first 8 eth_getTransactionCount answers come from a lagging node (2), later ones are fresh (3). Expected: after the gate says done the next step goes out at nonce 3 = seed.nonce+1. Actual: line 185 (2 > 2 false) keeps the seed, nonce = max(2, 2, 2) = 2; the setRevealCommit gate is true, seedLanded finds no receipt for H and sets nonce = max(2, 2) = 2; addPaymentToken(USDC, 1600 GOLD) is signed and sent at nonce 2 twice ('nonce too low'), and the run ends 'NONCE_MOVED addPaymentToken(USDC, 1600 GOLD): the chain is past nonce 2. Run the resume again'. Nonces sent on the resume: [2, 2].","severity":"low","snippet":"  else nonce=Math.max(await stableNonce(),planNonce+steps.length+failed.length);   // it landed under another hash, at a nonce only the chain knows","title":"seedLanded's no-receipt branch drops every lower bound the run already holds on the nonce: one stale eth_getTransactionCount pair sends the next step at the spent seed nonce and stops a valid resume"},{"citation":"resolved","description":"When recordedReceipt returns null for the top entry, when seedLanded takes its no-receipt branch, or when a step landed under a lost hash, the step is never in steps: the gate skips it from the chain, which is right, but stepBlock(name) stays undefined for the rest of the run. For addPaymentToken(USDC, 1600 GOLD) that makes addBlock undefined, usdcUsableAt null, and line 330 (`if(usdcUsableAt!==null&&...)`) skips the only check that paymentTokens(USDC).usableAt is the add block's timestamp plus two days; totalGas and safe-batch's recordBlock also miss the entry. The chain still holds the step and nothing is resent, so this is a loss of verification, not of state, bounded to one entry per resume. Fix: when a gate says done for a step not in steps, record a hash-less entry {name, blockNumber: the block of the gate read} so stepBlock works, or derive usdcUsableAt from the chain (Number(p>>216n) must equal the timestamp of some block in the run's window plus 172,800) instead of skipping the check when addBlock is unknown.","line":307,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Reproduced with the Submission13 deploy-mainnet.mjs behind a scripted JSON-RPC, as the consequence of the two findings above. In the lost-reply scenario the resume finishes with readBackMismatch [] and d.steps without setRevealURIs; in the stale-drop scenario the resume finishes with failed [] and totalGas 126000 while the chain holds a reverted transaction that burned gas. For the USDC case: the partial record from a run stopped after addPaymentToken(USDC, 1600 GOLD) with that entry's hashes replaced by one hash the chain never saw (the package's own test 'a recorded hash the chain no longer shows leaves the record' sets this up). Expected: paymentTokens(USDC).usableAt is still verified against the chain. Actual: addBlock is undefined, usdcUsableAt is null, line 330 never runs, and the run finishes with readBackMismatch [] whatever usableAt holds.","severity":"info","snippet":" const usdcUsableAt=addBlock!==undefined?(await retry(async()=>{const b=await provider.getBlock(addBlock);if(!b)throw Error('BLOCK_NOT_VISIBLE '+addBlock);return b;})).timestamp+PAYMENT_TOKEN_DELAY:null;","title":"A step that the chain shows but the record lost (dropped entry, no-receipt seedLanded, lost hash) silently disables the USDC usableAt read-back check and the step's gas accounting"},{"citation":"resolved","description":"EXTRA_TOKEN_COUNT_SLOT=11 is correct for the reviewed build: compiling build/standard-input.json's sources with solc 0.8.38 (via-IR, 200 runs, cancun) gives _owner 6, _pendingOwner 7, _nameFallback 8, _versionFallback 9, paymentTokens 10, extraPaymentTokenCount 11 (forge inspect storageLayout, verified during this review). The constant is only meaningful for that layout, but chainState/checkRecord never confirm that the code at d.contract is that build: the record carries codeHash and reviewedBuild.runtimeSha256, and deploy-mainnet.mjs checks runtimeMatches on every resume, while safe-batch.mjs trusts d.contract as given. A build that inserts one state variable before extraPaymentTokenCount moves the count to slot 12 and makes slot 11 the mapping base of paymentTokens (always 0): accept/open then stop with 'CHAIN_DIFFERS_FROM_RECORD extraTokens: the deployer key changed the contract' on a correct contract, blaming the deployer key. Fail-safe today, and an answer to README question 4: the one thing still needed is to tie the slot read to the code. Fix: in chainState read provider.getCode(d.contract,head), compare ethers.keccak256(code) with d.codeHash (and runtimeMatches against the reviewed runtime when the build is available) and throw CODE_NOT_RECORDED_BUILD otherwise; keep the slot constant next to the pinned build hash so a re-compile has to change both.","line":108,"path":"Submission13/tools/safe-batch.mjs","reproduction":"State: a deployment record whose contract field points at an address holding any other contract where slot 11 is not 1 (for example the Sepolia TestUSDC, or a future candidate build with one more variable before the count), with safe and readBackMismatch [] as usual. Run `node contract/tools/safe-batch.mjs accept <record>`. Expected: 'the code at <contract> is not the reviewed build' before any slot is interpreted. Actual: chainState reads slot 11 (line 108) with no code check, checkRecord (line 120) compares it with '1' and stops with 'CHAIN_DIFFERS_FROM_RECORD extraTokens: the deployer key changed the contract, stop and check'. Verified by reading safe-batch.mjs: no getCode or codeHash comparison exists in chainState, checkRecord, checkAccept or checkOpen.","severity":"info","snippet":" const extraTokens=BigInt(await steady(()=>provider.getStorage(d.contract,EXTRA_TOKEN_COUNT_SLOT,head))).toString();","title":"safe-batch reads storage slot 11 of whatever address the record names without first pinning that code to the reviewed build the slot belongs to"}],"hash":"47ff5a67626670a041874e585ceb1c638902a748ac4826ab4f0fcf4998aa5d6d","nodeId":"9f85ea65-48bd-4153-b84d-67a399bd2019","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds 8 findings (6 low, 2 info), every snippet verified against its line, and no repository file was changed.\n\n**How I checked.** The package's own suite cannot run here because `runtime.mjs`, the artifacts and the Hardhat chain are not in the tree. I built a scripted JSON-RPC stand-in outside the repository and ran the unmodified `Submission13/tools/deploy-mainnet.mjs` through it with a real ethers 6.17 Wallet, so every kept finding was exercised on the actual code. I also compiled the standard input with solc 0.8.38 to confirm the storage layout, and fetched reth's error wording from its source.\n\n**Findings kept, after merging the specialists' duplicates:**\n\n- **Stale drop in recordedReceipt (low).** The head read and the receipt reads are separate requests, so a receipt-serving backend that lacks the receipt while the head is past the block drops a mined entry. A failed[] entry never comes back, and with two stale nonce reads the resume sends at the spent nonce. Merged from audit_permissions and audit_flow. The specialist's \"12.5% probability\" claim was wrong: all polls must be null, so only persistent routing reaches it.\n- **Lost reply after the node took the transaction (low).** The hash is pushed only after sendTransaction returns. A TIMEOUT after the node mined it leads to NONCE_MOVED, and the resume skips the step without ever recording it. Merged from audit_permissions and audit_math.\n- **mayHoldBid misses reth wording (low).** Verified against reth's error.rs: three refusal strings do not match, so the bid never climbs and the step ends STEP_STUCK. Partial fix of audit 161d71af finding 5.\n- **Seed erased from checkpoints after a drop (low).** The refilled step's checkpoints and the NONCE_MOVED checkpoint drop the later step's waiting transaction. Merged from audit_permissions and audit_economics.\n- **TIP_CEILING on a 25% raise when 10% fits (low).** The last bid stops at 2.44 of 3 gwei.\n- **seedLanded no-receipt branch loses the nonce lower bound (low).** Two stale count reads send at the spent seed nonce.\n- **Lost entry silently disables the usableAt check and gas accounting (info).**\n- **safe-batch reads slot 11 without pinning the code (info).** Slot 11 itself is correct for the reviewed build.\n\n**Dropped.** The tip start floor finding from audit_math: a bid at the node's suggested tip is a percentile of included tips, and no evidence showed it goes unmined.\n\n**README questions with no finding.** The PowerShell script's prompts all sit inside the try and the finally clears every variable on each exit path I traced, including Ctrl+C. The floor exceeding the real nonce only happens through the deeper-reorg case section 5 already accepts.","treeHash":null,"usage":{"cachedInputTokens":2067597,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":45649,"runtime":"claude","turns":29,"wallClockMs":583421}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b8fd8054ea57c5d8","findings":[{"citation":"resolved","description":"seedLanded() runs only after a gate read said the waiting step is done (notDone, line 261) or the code check found the contract (line 282). If no receipt for seed.hashes arrives within stepTimeoutMs, the new branch sets nonce = max(stableNonce(), planNonce+steps+failed) and discards both bounds the run already had: the current `nonce` and seed.nonce+1. stableNonce() accepts two equal 'latest' counts, and a lagging node in the tools' own model (tests/contract/deploy-mainnet-cli.test.mjs `lagger`, and the finding-3 tests that script stale eth_getTransactionCount) returns two equal old counts. The record cannot cover the gap either: a step that landed under a hash the run never saw (the only reason this branch exists) is in neither steps[] nor failed[], so planNonce+steps+failed is at most seed.nonce. The result is a nonce at or below seed.nonce, which the gate's own answer shows is already spent. Submission 12 used seed.nonce+1, which is right in exactly this case. The README (section 3) gives the reason for the change: a step that landed at some other nonce. That reason does not need the bound to go away. A nonce that only lagging nodes report too low should be read again until it agrees, and the value kept should not drop below what the run already holds. The send that follows cannot do harm, because the nonce is spent: it gets NONCE_EXPIRED, and send() throws NONCE_MOVED if the second stableNonce() read is fresh. If that read is stale too, all `attempts` fail on NONCE_EXPIRED and the run throws. Either way a valid resume stops and a person has to run it again, even though the chain had already settled the question. Partial answer to section 4 question 3: on the no-receipt branch the nonce is not right when one read comes from a lagging node.","line":195,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Record: planNonce=0, steps=[deploy@0 (block B0), setRevealURIs@1 (block B1)], failed=[], pending={name:'setRevealCommit',nonce:2,hashes:[H],fee:{...}}. On the chain, setRevealCommit was mined at nonce 2 under a different hash H2 that was broadcast but never returned to the run (for example a send that ended in TIMEOUT or a 502 after the node took it), so getTransactionCount('latest') is 3 on up-to-date nodes and H has no receipt. Resume while H2 is still pending: the first firstReceipt(H) misses, stableNonce() = 2 is not > 2, nonce = max(2, floor 2, 0+2+0) = 2, and the seed is kept. H2 is then mined. The setRevealCommit gate is read on an up-to-date node and returns true, so seedLanded() runs: firstReceipt([H], 5 min) returns null. stableNonce() then gets two eth_getTransactionCount answers from a lagging node, 2 and 2 (both one block old). nonce = max(2, 2) = 2. Expected: the next step goes out at nonce 3 = seed.nonce+1. A gate that says done means nonce 2 is spent, and 2 is also the current nonce. Actual: addPaymentToken(USDC) is signed at nonce 2. The node answers 'nonce too low' (NONCE_EXPIRED), hashes is empty, and the run ends with NONCE_MOVED, or with NONCE_EXPIRED after all attempts if the second read is stale too. Fix: in the no-receipt branch, read the nonce again until a value above seed.nonce is seen, or take the highest of several reads over visibleReads*pollMs (a lagging node can only report too low). Never let nonce fall below its value on entry: nonce = Math.max(nonce, <that read>, planNonce+steps.length+failed.length). If no read gets past seed.nonce within visibleTries, stop with a named error instead of sending.","severity":"low","snippet":"else nonce=Math.max(await stableNonce(),planNonce+steps.length+failed.length);   // it landed under another hash, at a nonce only the chain knows","title":"seedLanded without a receipt drops every known lower bound on the nonce: one stale nonce read sends the next step at the spent seed nonce and stops the run"},{"citation":"resolved","description":"The drop condition is that, in the last visibleReads polls, eth_blockNumber was >= top.blockNumber and every eth_getTransactionReceipt returned null without an error. These are separate HTTP requests. On a load-balanced or method-routed RPC, the node that reported the height is not the node that answered the receipt reads. So 'answered' does not show that a node holding the block lacked the receipt. The run only needs no receipt for the whole visibleTries*pollMs window (90 polls, about 3 minutes). That happens when receipt calls are served by a backend whose receipt index is behind, or has a gap for that block, while eth_blockNumber comes from a head tracker that is current. Several providers route methods this way, and a node still building its tx index can return null rather than an error. Random per-request lag does not reach the drop, but routing that persists does. What is lost: a steps[] entry is recovered by its gate, because the chain still shows the state. A failed[] entry has no gate, so once dropped it never comes back. totalGas no longer includes the gas that reverted transaction burned, and the count part of the nonce floor (planNonce+steps+failed) falls by one. That count is the guard the finding-3 fix added against stale nonce reads, for example PLANNED_NONCE_USED after a reverted deploy. The record is checkpointed without the entry at line 277, so every later resume has lost it too. This answers section 4 question 1: yes, an entry can leave while still on the chain, and for failed[] the gates cannot recover it.","line":159,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"Record after a reverted deploy (the finding-3 test 'a reverted deploy is in the record'): steps=[], failed=[{name:'deploy EmberEvoGenesis',hash:R,hashes:[R],blockNumber:B}], with R mined at block B and status 0. Resume through an RPC that answers eth_blockNumber with >= B but answers eth_getTransactionReceipt(R) with null for 90 polls: the script in the test 'a recorded hash the chain no longer shows leaves the record', applied to a failed entry. Expected: R is mined, so the entry stays, or the run stops with RECEIPT_NOT_VISIBLE because the absence was not shown on a node that holds block B. Actual: recordedReceipt returns null, the entry is spliced out (topLeft), and checkpoint writes failed:[]. The count floor goes from planNonce+1 to planNonce. With two stale eth_getTransactionCount answers of planNonce, the resume no longer stops at once with PLANNED_NONCE_USED. It signs a second deploy at the spent nonce and depends on NONCE_EXPIRED to stop. Every later record and the final totalGas leave out R's gasUsed. Fix: show the absence in one request answered by one node. Read eth_getBlockByNumber(top.blockNumber) and drop only when that block exists and its transaction list contains none of top.hashes. A node that does not have the block returns null, which counts as 'not answered'. Optionally also require getTransaction(h) to be null for every hash.","severity":"low","snippet":"const n=await provider.getBlockNumber().catch(()=>-1);let answered=n>=top.blockNumber;","title":"recordedReceipt decides 'a node past the block has none of them' from two different RPC requests: a receipt-serving node that persistently lacks the receipt drops a mined entry, and a dropped failed[]"}],"hash":"df516ab648bff6273115c6ce64835bf248a792a6dfde955705ba796b27416ebc","nodeId":"51ddffa4-342f-4306-aaa5-0a322f456428","outcome":"completed","summary":"I found two Low findings and nothing Medium or above. Both are written to `.imd-findings.json`, and their file, line and quoted snippet match the tree. Neither was run: the package doesn't include `tools/runtime.mjs` or the private test artifacts its tests need, so both reproductions come from reading the code against the tools' own lagging-node model. No proof files were needed, since neither finding is critical or high. I changed no repository files.\n\n## Findings\n\n**1. Low: on its no-receipt branch, `seedLanded` loses what the run already knew about the nonce** (`deploy-mainnet.mjs:195`)\n- **Cause:** when a gate says the waiting step is done but no receipt arrives, the nonce becomes `max(stableNonce(), planNonce+steps+failed)`. That drops both the current nonce and `seed.nonce+1`. The step count can't make up for it, because a step that landed under a hash the run never saw is in neither list. So two equal stale nonce reads from a lagging node set the nonce to `seed.nonce`, which the gate has just shown is spent.\n- **Failing state:**\n  - Record: steps at nonces 0 and 1, `setRevealCommit` waiting at nonce 2 under hash H.\n  - Chain: that step was mined under a different hash H2.\n  - The run: `addPaymentToken` is signed at nonce 2, gets \"nonce too low\", and the run stops with `NONCE_MOVED`, or with `NONCE_EXPIRED` once every attempt has failed.\n- **Effect:** a resume that should succeed stops and has to be run again. Submission 12's `seed.nonce+1` was right in this case.\n- **Fix:** read the nonce again until a value above `seed.nonce` appears, or take the highest of several reads. Never go below the nonce held on entry. If no read gets past `seed.nonce`, stop with a named error.\n\n**2. Low: `recordedReceipt` concludes \"this node has the block but no receipt\" from two separate requests** (`deploy-mainnet.mjs:159`)\n- **Cause:** the block height and the receipt come from separate requests. On an RPC that routes methods to different backends, a current head plus a receipt backend that persistently returns null drops an entry that is still on the chain. Random per-request lag won't do it over 90 polls; routing that persists will.\n- **What can't be recovered:** a `steps[]` entry comes back through its gate. A `failed[]` entry has no gate, so it is lost for good. `totalGas` then leaves out that transaction's gas, and the count part of the nonce floor (the guard from the Finding 3 fix) drops by one. In the reverted-deploy case, a stale nonce read no longer stops at `PLANNED_NONCE_USED`; it signs a second deploy at the spent nonce, which only \"nonce too low\" then stops.\n- **Fix:** drop the entry only when `eth_getBlockByNumber(top.blockNumber)` returns the block and none of the recorded hashes is in it. A node without the block returns null, which counts as \"not answered\".\n\n## Answers to the eight questions\n\n1. **`recordedReceipt`:** yes, an entry can leave the record while still on the chain (Finding 2). Only `failed[]` entries are lost for good. A stale node reaches the drop only through routing that persists, not random lag.\n2. **`failed[]` and the floor:** I found no new way for the floor to exceed the real nonce beyond the deeper-reorg case you already accept in section 5. Every entry is counted once per nonce, including the re-recording on resume and after a revert retry.\n3. **`seedLanded`:** the branch with a receipt is correct. The branch without one is Finding 1. A true gate only fires for a step of the same name. A false \"done\" after a reorg is possible in principle, but I couldn't show a concrete case.\n4. **Slot 11:** correct. The storage layout puts ERC721 at slots 0–5, Ownable at 6, Ownable2Step at 7, EIP712 at 8–9, `paymentTokens` at 10, and `extraPaymentTokenCount` (a full `uint256`, not packed with anything) at 11. Together with the USDC read-back and the fact that tokens can't be removed, the count of 1 is enough. Nothing else is needed.\n5. **Fees:** every bid rises at least 25% on both the tip and the max fee, so it alw","treeHash":null,"usage":{"cachedInputTokens":1444366,"inputTokens":31,"model":"claude-opus-5-5","outputTokens":66112,"runtime":"claude","turns":19,"wallClockMs":690068}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e8e860c7f2306473","findings":[{"citation":"resolved","description":"The hash of a step's transaction enters `hashes` (and so the checkpointed `pending.hashes`, `recordedReceipt`, `firstReceipt`, `seedLanded` and the `held` check) only when `deployer.sendTransaction` returns. `deployer` is an ethers `Wallet`: it signs locally, so the hash is known before `eth_sendRawTransaction` goes out, but it is thrown away when the RPC call fails after the node accepted the transaction (a `TIMEOUT`, a 502 on the response, a dropped connection). That is exactly the 'lost hash' class the README accepts in section 5, but here it is created by the tool's own send path rather than by the chain. Consequences on this code: `mayHoldBid` raises `last` for a `TIMEOUT`, so attempt a+1 sends a 25% higher replacement at the same nonce; if the first transaction is mined before that, the replacement gets `NONCE_EXPIRED` with `hashes` empty and `stableNonce()>nonce`, and the run stops with `NONCE_MOVED` after a step that in fact succeeded. On the resume the gate says done and the step is skipped, but it never enters `steps`: `stepBlock(name)` stays undefined, `totalGas` and `recordBlock` miss it, and for `addPaymentToken(USDC, 1600 GOLD)` the read-back check of `paymentTokens(USDC).usableAt` is silently skipped (`addBlock` undefined gives `usdcUsableAt=null`). For a 502 (not `mayHoldBid`) the next attempt re-sends the same payload at the same fee: geth/Nethermind answer 'already known', which raises the bid and gets a replacement out, so the first transaction is replaced and paid for twice in bidding rather than waited for.","line":218,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"State: fresh run, step `setRevealURIs (not locked)` at nonce 1. Make `deployer.sendTransaction` forward the raw transaction to the node (so the pool holds hash H) and then throw `{code:'TIMEOUT'}` (the harness in tests/contract/deploy-mainnet-cli.test.mjs can do this with a wrapper around `sendTransaction` that calls the real one and then throws). Mine H. Expected: the tool knows H (it signed the transaction), waits for its receipt, records the step and goes on. Actual: `hashes` is empty, `last=fee` is raised, attempt 2 sends a replacement at nonce 1 and receives `NONCE_EXPIRED`; `stableNonce()` is 2 > 1, so `deployMainnet` throws `NONCE_MOVED setRevealURIs (not locked)` and the operator must resume by hand; after the resume `d.steps` has no `setRevealURIs` entry. Fix: populate and sign first (`const signed=await deployer.signTransaction(await deployer.populateTransaction({...req,gasLimit:limit,nonce,...fee}))`), push `ethers.Transaction.from(signed).hash` into `hashes` and checkpoint `waiting()` before `provider.broadcastTransaction(signed)`; a broadcast error then leaves a hash the existing receipt logic can wait for, and the lost-hash class shrinks to node-side eviction.","severity":"low","snippet":"    const tx=await deployer.sendTransaction({...req,gasLimit:limit,nonce,...fee});\n    hashes.push(tx.hash);knownLimit=limit;last=placed=fee;sent=true;log(name,tx.hash);","title":"A send that errors after the node took the transaction loses its hash: the step cannot finish in-run and the record never learns it"},{"citation":"resolved","description":"The first bid is exactly `eth_maxPriorityFeePerGas` from the deploy RPC and every later bid is +25% over the last placed or refused one, only after a 5-minute `stepTimeoutMs` or a refusal. The ceiling (max(3x suggestion, 2 gwei)) is fine, but there is no floor on the starting tip, so the number of attempts needed to reach a tip builders include is governed by the RPC's estimate. Geth's gas-price oracle suggests the 60th percentile of the lowest tip in each of the last 20 blocks, which on mainnet is often around 0.01 gwei; ethers only substitutes 1 gwei when the RPC has no `eth_maxPriorityFeePerGas` at all. Numbers: suggestion 1e7 wei (0.01 gwei): bids 1e7, 1.25e7, 1.56e7, 1.95e7, 2.44e7, 3.05e7, 3.81e7, 4.77e7 over `attempts=8` (up to 40 minutes), then `STEP_STUCK`; the next resume starts from `pending.fee` and adds 8 more; reaching 1 gwei takes 21 bumps, i.e. three runs and about two hours, while the ceiling (2 gwei) is never the limit. The run is not blocked for good, but the README's question 'is the ceiling too low to get a transaction mined' has the answer at the other end of the range: the start, not the ceiling.","line":134,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"State: deploy RPC answers `eth_maxPriorityFeePerGas` = 0x989680 (1e7 wei) and nothing with a tip under 1e8 wei is mined (script the stand-in RPC to leave such transactions pending, like the `evm_setAutomine false` tests). Expected: a deployment tool with a 2 gwei ceiling reaches a mined tip within one run. Actual: after 8 attempts the highest tip sent is 47,683,715 wei and `deployMainnet` throws `STEP_STUCK deploy EmberEvoGenesis`; each resume adds 8 attempts. Fix: start from `max(tip, TIP_START_FLOOR)` with a floor such as 1 gwei (6.3M gas x 1 gwei = 0.0063 ETH over the whole run), keep the 25% bump, and keep the ceiling as it is; or make the bump a larger multiple when the current bid is under the floor.","severity":"info","snippet":"  const up=(v,prev)=>{const bumped=prev==null?0n:prev*125n/100n+1n;return v>bumped?v:bumped;};","title":"The bid climbs multiplicatively from whatever tip the RPC suggests: a small suggestion needs several resumes to reach a tip that gets mined"},{"citation":"resolved","description":"`EXTRA_TOKEN_COUNT_SLOT=11` is correct for the reviewed build (ERC721 slots 0-5, Ownable `_owner` 6, Ownable2Step `_pendingOwner` 7, EIP712 `_nameFallback`/`_versionFallback` 8-9, `paymentTokens` 10, `extraPaymentTokenCount` 11; linearisation ERC721, Ownable, Ownable2Step, EIP712, then the contract). The constant is only meaningful for that layout, but `chainState`/`checkRecord` never confirm that the code at `d.contract` is that build: the record carries `codeHash` and `reviewedBuild.runtimeSha256` and `deploy-mainnet.mjs` checks `runtimeMatches` on every resume, while `safe-batch.mjs` trusts `d.contract` as given. A later build that inserts one state variable before `extraPaymentTokenCount` moves the count to slot 12 and makes slot 11 the mapping base of `paymentTokens` (always 0): `accept`/`open` then stop with `CHAIN_DIFFERS_FROM_RECORD extraTokens` on a correct contract, and a build that removes a variable reads `paidOrderTokenId`'s base, also 0. Fail-safe today, but the stop message blames the deployer key. The check is one `keccak256(getCode)` compare against `d.codeHash` (or `runtimeMatches` against the reviewed runtime hash) before the slot read.","line":108,"path":"Submission13/tools/safe-batch.mjs","reproduction":"State: a deployment record whose `contract` points at an address holding any other verified contract where slot 11 is not 1 (for example the Sepolia TestUSDC or a future candidate build with one more variable before the count), `safe` and `readBackMismatch:[]` as usual. Run `safe-batch.mjs accept <record>`. Expected: 'the code at <contract> is not the reviewed build' before any slot is interpreted. Actual: `CHAIN_DIFFERS_FROM_RECORD extraTokens: the deployer key changed the contract, stop and check`. Fix: in `chainState`, read `provider.getCode(d.contract,{blockTag:head})`, compare `ethers.keccak256(code)` with `d.codeHash` (and, when present, the runtime bytes with `d.reviewedBuild`), and throw `CODE_NOT_RECORDED_BUILD` otherwise; keep the slot constant next to the pinned build hash so a re-compile has to change both.","severity":"info","snippet":" const extraTokens=BigInt(await steady(()=>provider.getStorage(d.contract,EXTRA_TOKEN_COUNT_SLOT,head))).toString();","title":"safe-batch reads storage slot 11 of whatever address the record names without pinning that code to the reviewed build the slot belongs to"},{"citation":"resolved","description":"Answer to README question 1, 'what does a later resume lose'. When `recordedReceipt` returns null for the top entry (a node past its block with none of its hashes), when `seedLanded` takes its no-receipt branch, or when a step landed under a lost hash, the step is never in `steps`: the gate skips it from the chain, which is right, but `stepBlock(name)` stays undefined for the rest of the run. For `addPaymentToken(USDC, 1600 GOLD)` that makes `addBlock` undefined, `usdcUsableAt` null, and line 330 (`if(usdcUsableAt!==null&&...)`) skips the only check that `paymentTokens(USDC).usableAt` is the add block's timestamp plus two days; `totalGas` and `safe-batch`'s `recordBlock` also miss the entry. The chain still holds the step and nothing is resent, so this is a loss of verification, not of state. Only one entry can be dropped per resume, as the README says, so the loss is bounded to one step per resume.","line":307,"path":"Submission13/tools/deploy-mainnet.mjs","reproduction":"State: the partial record from a run stopped after `addPaymentToken(USDC, 1600 GOLD)`, with that entry's `hashes` replaced by one hash the chain never saw (the 'a recorded hash the chain no longer shows' test in deploy-mainnet-cli.test.mjs sets this up with a scripted null receipt). Resume. Expected: the step is recognised from the chain and `paymentTokens(USDC).usableAt` is still verified against the chain (the add block can be found from `usableAt - 2 days` or from the gate read). Actual: the resume finishes with `readBackMismatch:[]` whatever `usableAt` holds, `d.steps` has four entries and `totalGas` omits the USDC transaction. Fix: when a gate says done for a step not in `steps`, record a hash-less entry `{name, blockNumber: the block of the gate read}` so `stepBlock` works, or derive `usdcUsableAt` from the chain (`Number(p>>216n)` must equal the timestamp of some block plus 172,800 and be within the run's time window) instead of skipping the check when `addBlock` is unknown.","severity":"info","snippet":" const usdcUsableAt=addBlock!==undefined?(await retry(async()=>{const b=await provider.getBlock(addBlock);if(!b)throw Error('BLOCK_NOT_VISIBLE '+addBlock);return b;})).timestamp+PAYMENT_TOKEN_DELAY:null;","title":"A dropped or lost step entry silently disables the USDC usableAt read-back check and the step's gas accounting"}],"hash":"fd8848e5bcb2c145a80ed0c8444d0cbd41d8d959e38e3374eb437707d4a9611a","nodeId":"2c563d05-8f18-46cb-ad96-b6f269c7470f","outcome":"completed","summary":"The review is complete. Findings are in `.imd-findings.json` at the repository root, with every path, line and snippet verified against the tree.\n\n**Outcome: no critical, high or medium defects in Submission13's changed tools.** One Low and three Info findings. The contract is unchanged (the published diff reproduces Submission13 from Submission12 byte for byte, and the checksums hold). The package's own test suite could not be run here because it imports `../tools/runtime.mjs` and the private artifacts, which the package does not ship, so reproductions are given as concrete states against the code.\n\nAnswers to the eight questions:\n\n1. **recordedReceipt.** The drop needs a node past the block that answers and holds none of the hashes, for the last three rounds, with no receipt in any of 90 rounds. Under the package's own lag model that is unreachable. A node that is behind sees `answered=false` and ends in `RECEIPT_NOT_VISIBLE`, without rewriting the record. A wrong drop would be bounded: the gate decides from the chain and a duplicate send is caught by the estimate or a revert. What a resume loses is verification, not state. That is the Info finding at line 307: a dropped or lost `addPaymentToken` entry silently skips the `usableAt` read-back check.\n2. **failed[] and the floor.** Every entry in `steps` or `failed` is a spent nonce, so `planNonce+steps+failed` cannot exceed the real nonce except after a reorg deeper than the top entry, which section 5 accepts. The revert-retry path increments the nonce before the retry, and the reverted deploy correctly ends in `PLANNED_NONCE_USED`.\n3. **seedLanded.** The gate only fires for the seed's own step name, and the gates do not overlap. Both branches keep the nonce at or below the real one, so no gap is created. A stale nonce read on the no-receipt branch ends in `NONCE_MOVED`, which is recoverable.\n4. **Slot 11.** Correct for the reviewed build: ERC721 slots 0 to 5, Ownable 6, Ownable2Step 7, EIP712 fallbacks 8 and 9, `paymentTokens` 10. What is missing is a pin of the code at the record's address to that build before the slot is interpreted. That is the Info finding at safe-batch line 108.\n5. **Fees.** `mayHoldBid` matches geth, Erigon, reth, Besu and current Nethermind strings. Nethermind's rejection is \"ReplacementNotAllowed, replacement transaction underpriced\" and \"AlreadyKnown, already known\", which the regex catches. The ceiling is not the problem. The start of the climb is: a small suggested tip needs several resumes to reach a mined tip. That is the Info finding at line 134, with the arithmetic.\n6. **TIP_CEILING and STEP_STUCK.** No permanent block beyond the accepted base fee case. The ceiling is recomputed from the current market on every attempt, and an evicted bid is dropped when the ceiling is hit.\n7. **deploy-mainnet.ps1.** No exit path left. Every prompt is inside the `try`, PowerShell runs `finally` on Ctrl+C, and the code before the `try` handles no secret.\n8. **Audit 161d71af.** All nine items are fixed as the README maps them.\n\nThe Low finding is adjacent to finding 2 of that audit: a `sendTransaction` that errors after the node accepted the transaction throws the hash away, although the `Wallet` signed it locally. Signing first and pushing the hash before broadcasting would shrink the lost-hash class that section 5 still accepts.\n\nSources consulted for the client error strings: [Nethermind AcceptTxResult.cs](https://raw.githubusercontent.com/NethermindEth/nethermind/master/src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs), [Nethermind TxPoolErrorMessages.cs](https://raw.githubusercontent.com/NethermindEth/nethermind/master/src/Nethermind/Nethermind.TxPool/TxPoolErrorMessages.cs), [Nethermind EthRpcModule.cs](https://raw.githubusercontent.com/NethermindEth/nethermind/master/src/Nethermind/Nethermind.JsonRpc/Modules/Eth/EthRpcModule.cs), [Tatum: error \"already known\"](https://docs.tatum.io/docs/error-already-known-eth_sendrawtransaction), [execution-apis issue 2","treeHash":null,"usage":{"cachedInputTokens":1276210,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":58983,"runtime":"claude","turns":23,"wallClockMs":857908}}],"verification":[]}