{"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":"161d71af-879f-487f-b3b8-c5ac1bb71d9c","kind":"audit","nodes":[{"acceptedSubmissionHash":"3e64267f9406ffb0fe50e6b3006759e39858b370d53bfd0b9d37fbe56af3eb70","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":"f1d5f69eeacb7d7a7de483c3cf96bd89c2ca8aedddd3bf471f2cd7823b53eaf2","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":"9dcc9c7cfc68ab3788e87c0a71025a779388e77f081f456f9812d4a51a693be4","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":"6163e21e08928345bd763fb0e2141e821c5212d2037941e82f04a5076e9a9a8c","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":"da258a0df603a32966ae55c413b29d435c3b780982385963625e3437047458e8","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 Submission12/ only (read Submission12/AUDIT_README.md first; Submission11 to 5 AUDIT_README are the reference for everything unchanged). Not deployed. This is the remediation of audit 358f7f40 (1M 3L 4I, all in the deployment tools and the signer) on the build intended for Ethereum mainnet. The contract did not change: runtime, creation code and ABI are identical to Submission11, metadata included. Changed: tools/deploy-mainnet.ps1, deploy-mainnet.mjs, safe-batch.mjs, offchain/signer/paid-approval.mjs; the diff is in build/tools-and-tests.Submission11-to-12.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. Resume order: can floor be wrong and leave a run waiting on a nonce gap? Can dropping seed lose a needed outbid? Can a step enter steps[] when it should not?\n2. REPLACEMENT_GAS: can the fallback send something that should have stopped? Is 1,000,000 too low for any step?\n3. New stops: can NONCE_MOVED, RESUME_OUT_OF_ORDER or HEAD_BEHIND_RECORD block a valid run for good?\n4. A recorded step is not resent: does that hide anything the read-back does not catch, for example after a reorg?\n5. Fees: a refused bid raises last, and only an accepted fee reaches the record. Can a run overpay badly or fall short?\n6. safe-batch on a real RPC: can pinned reads, the head rule or log chunks cause false refusals? Can a stale state still pass three consecutive reads?\n7. Secrets: the key now lives in a PowerShell variable between the prompts and the deploy call. Is the scoping right on every exit path?\n8. Anything in audit 358f7f40 that is not actually fixed.\n\nReport severity, reproduction and fix. Not in scope: artwork, the Mint web page, World 3D.","parentJobId":null,"planHash":"c36492bc2efaf0084eca6f3063fc65a45be27578694f55ac8037db408e0f347b","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"161d71af-879f-487f-b3b8-c5ac1bb71d9c","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":"54235","feedbackHash":"3f3faa4ff6e632d9fda01efedda7f08dbb6aba3272b960a62b587013f5283d57","nodeKey":"audit_economics","submissionHash":"3e64267f9406ffb0fe50e6b3006759e39858b370d53bfd0b9d37fbe56af3eb70","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51070","feedbackHash":"5d632bd4dc648546d0391cf47c7a87777aade7b1564b30edc4d36fc891939eff","nodeKey":"audit_flow","submissionHash":"f1d5f69eeacb7d7a7de483c3cf96bd89c2ca8aedddd3bf471f2cd7823b53eaf2","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52601","feedbackHash":"b57ae6cf1004d87acce187759fe243a32bd261a832c4feee94bac7ed4d0d11df","nodeKey":"audit_judge","submissionHash":"9dcc9c7cfc68ab3788e87c0a71025a779388e77f081f456f9812d4a51a693be4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50994","feedbackHash":"94f044b86d37485c22e1b101661482f7f875f6dd1cc36e3b0acbd0aceca5061e","nodeKey":"audit_math","submissionHash":"6163e21e08928345bd763fb0e2141e821c5212d2037941e82f04a5076e9a9a8c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51064","feedbackHash":"fd38c0b37a10055c01131cc3deaaef969ee1a5a5ef8d043789e44ace56d3dd3f","nodeKey":"audit_permissions","submissionHash":"da258a0df603a32966ae55c413b29d435c3b780982385963625e3437047458e8","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"b21286a5c3d931b55250b807a93ae9bba4bb619ce7e0787dfd08c613f66b9a07","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"bc39660800b5f882","findings":[{"citation":"resolved","description":"The fallback treats every failed estimate as 'the node simulated on top of the waiting transaction' as soon as this step already has a hash. It does not distinguish that case from a call that also reverts on the canonical latest state. For the four fixed-calldata steps this is harmless: the replacement is byte-identical to the waiting one and the shared nonce makes them exclusive. setRevealCommit is different: request() rebuilds the payload on every attempt (recentBlock(), a fresh acceptance over block.number and block.hash, previous=revealCommit(), signerEpoch). The contract checks blockhash(blockNumber) against the signed hash (EmberEvoGenesisCandidate.sol:318-321), so an acceptance built from a block that is not canonical reverts with REVEAL_ACCEPTANCE_INVALID. recentBlock() only requires two reads of the same hash, which one node on a short fork answers consistently. In that state the estimate reverts for a real reason, the fallback sends the reverting call anyway at >=1.25x the waiting transaction's fee and 1,000,000 gas, the higher bid displaces the valid waiting h1 in the mempool, is mined, reverts, and the nonce is spent. The valid h1 can never land. revertRetries then costs a further nonce and gas, and if the forked node keeps answering, TX_FAILED stops the run. Without the fallback the estimate would have refused h2 and h1 would have landed. Fix: before returning REPLACEMENT_GAS, simulate the request against the canonical head explicitly, e.g. await provider.call({...req,from:owner},'latest') (ethers sends eth_call with blockTag latest, which never includes pending transactions); use the fixed limit only when that call succeeds, otherwise rethrow the estimate error. Alternatively limit the fallback to the four fixed-calldata steps and let setRevealCommit keep its full-estimate path.","line":179,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"State: step 'setRevealCommit' at nonce N, attempt 1 sent h1 (valid acceptance for canonical block b1, waiting in the mempool, hashes=[h1]). Attempt 2: recentBlock() is answered twice by a node whose head-2 block hash is not canonical (short fork) so the acceptance signs a hash blockhash(n) will not match; eth_estimateGas reverts with REVEAL_ACCEPTANCE_INVALID. Expected: nothing is sent, h1 stays the bid, the attempt loop continues or stops. Actual: gasLimit(req).catch returns REPLACEMENT_GAS because hashes.length is 1 and req.to is set; sendTransaction goes out at nonce N with fee >=1.25x h1 and gasLimit 1,000,000; h2 replaces h1, is mined and reverts (rc.status 0), nonce++ and revertRetries resend at N+1. Observable: a reverted setRevealCommit transaction from the deployer at nonce N with gasLimit exactly 1000000, and h1 absent from the chain.","severity":"low","snippet":"    const req=await request(),limit=await gasLimit(req).catch(e=>{if(hashes.length&&req.to)return REPLACEMENT_GAS;throw e;});","title":"REPLACEMENT_GAS fallback can replace a valid waiting setRevealCommit with one that reverts on the canonical chain"},{"citation":"resolved","description":"chainState pins every eth_call to the agreed head and steady() retries each read 5 times at 1 s; a read that still fails only fails this pass, and holds() runs up to 10 passes with 3 s between refusals. The PaymentTokenAdded log range is the exception: tokenLogs(...).catch(()=>null) turns any failure after the 5 in-pass tries into extraTokens=null, checkRecord turns that into LOGS_UNAVAILABLE, and holds rethrows it immediately instead of treating it as one refused pass. On a load-balanced public RPC the eth_getLogs request for [a, head] can reach a node that has not imported `head` yet (the head was agreed by two eth_blockNumber reads that may have come from other nodes); such nodes answer with an error for a toBlock above their head, and a node a few seconds behind stays behind for all 5 tries. The same lag on an eth_call is absorbed by holds; on the log read it is a hard stop for accept and open. The documented workaround, --logs-checked-on-etherscan, removes the only check that the deployer key added no other payment token, so a transient refusal nudges the owner toward disabling that check. Fix: drop the early rethrow in holds (let LOGS_UNAVAILABLE count as a refused pass like CHAIN_DIFFERS_FROM_RECORD, so it is thrown only after the 10 tries), or give tokenLogs its own retry over the refusedMs budget; keep LOGS_UNAVAILABLE as the final error when every pass fails.","line":154,"path":"Submission12/tools/safe-batch.mjs","reproduction":"Record d with startBlock S and head H>=S. A provider whose eth_getLogs rejects the first 6 calls (for example 'invalid block range' from a node behind H) and answers normally afterwards, while every eth_call succeeds. Run: node safe-batch.mjs accept <record>. Expected: the first pass is a refusal, the next passes succeed three times, the accept file is written. Actual: steady() gives up after 5 tries, tokenLogs returns null, checkRecord throws LOGS_UNAVAILABLE, holds rethrows on its first iteration, the process exits 1 with LOGS_UNAVAILABLE after about 5 s, no file written. Unit level: holds(seq(['logs-refused','ok','ok','ok']), check) where check throws Error('LOGS_UNAVAILABLE x') on 'logs-refused' rejects immediately, whereas holds(seq(['refuse','ok','ok','ok']),check) with any other error message passes (tests/contract/deploy-mainnet.test.mjs:331 and :334 show both behaviours).","severity":"low","snippet":"   if(String(e.message).startsWith('LOGS_UNAVAILABLE'))throw e;   // already asked several times","title":"safe-batch: a transiently refused log chunk ends accept/open at once, bypassing the retry budget every other read gets"},{"citation":"resolved","description":"The provider is built with staticNetwork:true and the record's chainId, so ethers never verifies the RPC's chain. chainState checks eth_chainId, but only after stableHead(provider,high) has returned, and stableHead requires the head to be at or above recordBlock(d). If READ_RPC_URL points at a chain whose height is below the record's block, stableHead never returns: each call throws HEAD_BEHIND_RECORD '... try again' after 30 s, holds counts it as a refusal and retries up to 10 times, and the run ends after about 5 minutes with a message that tells the owner to try again rather than that the RPC is on another chain. Not a safety issue (nothing is signed) but the diagnosis is wrong and 'try again' can never succeed. Fix: read eth_chainId once in the CLI block before the first stableHead and refuse there.","line":196,"path":"Submission12/tools/safe-batch.mjs","reproduction":"Record with chainId 11155111 and startBlock 9500000 (a Sepolia deployment), READ_RPC_URL set to an RPC of a chain at height 1500000 (for example Hoodi). node safe-batch.mjs status <record>. Expected: 'the read RPC is on another chain' at once. Actual: ten HEAD_BEHIND_RECORD rounds of 30 s each, then exit 1 with 'HEAD_BEHIND_RECORD the read RPC does not show block 9500000 yet: try again'.","severity":"info","snippet":"  const state=async()=>{high=await stableHead(provider,high);return chainState(provider,d,high);};","title":"safe-batch: stableHead runs before the chain-id check, so a wrong-chain READ_RPC_URL with a lower head reports HEAD_BEHIND_RECORD for good"},{"citation":"resolved","description":"$rpcUrl (line 65) and $deployerKey (line 66) are assigned in script scope outside the try/finally that begins on line 67. Inside the try every exit path, including a throw from the key check, the pre-check, the cancel prompt and the deploy call, reaches the finally, which nulls both variables and removes the environment entries; Ctrl+C inside the try also runs the finally. The gap is the window between the two prompts and the start of the try: a stop there (Ctrl+C at the key prompt, or an exception from Read-Secret) leaves $rpcUrl, and a stop after line 66 leaves both, with nothing clearing them. When the script is run as documented (powershell -File) the process ends and the point is moot. When it is dot-sourced from an interactive session, which the header does not forbid, the plaintext URL (with its API key) stays in the session's variables. Fix: move lines 63-66 inside the try, or wrap the whole section in the same try/finally.","line":66,"path":"Submission12/tools/deploy-mainnet.ps1","reproduction":"In an interactive PowerShell session: . .\\deploy-mainnet.ps1 ; answer the address prompt, enter the RPC URL, press Ctrl+C at 'Deployer wallet private key'. Then: $rpcUrl. Expected: empty. Actual: the full RPC URL including its key is printed. The same sequence with Ctrl+C anywhere after line 67 prints nothing, because the finally ran.","severity":"info","snippet":"$deployerKey = Read-Secret 'Deployer wallet private key'","title":"deploy-mainnet.ps1: the two secrets are read before the try, so the finally that nulls them does not cover an exit between the prompts"},{"citation":"resolved","description":"last is set before sendTransaction, so any failure of the send (a 502 from the RPC, a timeout, a dropped connection) is treated as a refused bid and the next attempt bids 1.25x+1 on both maxFeePerGas and maxPriorityFeePerGas. That is the right call for a timeout after broadcast (a lost hash must be outbid), but it also means a flaky RPC alone, with nothing in the mempool, drives the priority fee to about 4.8x the market value within one step's 8 attempts, and a resume restarts the bump chain from the recorded accepted fee, so two stuck runs can reach about 23x. The README accepts the uncapped 4.8x for fee refusals; the network-error path and the compounding across resumes are not named. maxFeePerGas is only a cap, so the real overpay is the priority fee times the step's gas (about 170k gas for setRevealURIs): modest in ETH, but unbounded in principle. Fix: keep the bump on every error (it is needed for the lost-hash case) but add a configured ceiling on maxPriorityFeePerGas (for example 10x the first attempt's value) and stop with STEP_STUCK when it is reached, so a person looks before more money is bid.","line":180,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"Resume with a record whose pending.fee is {maxFeePerGas 30 gwei, maxPriorityFeePerGas 1 gwei} and an RPC that answers eth_sendRawTransaction with HTTP 502 for the first 7 calls and accepts the 8th (test shape: tests/contract/deploy-mainnet-cli.test.mjs:265 with a counter). Expected per the README: refused bids escalate. Actual, and the point: the accepted 8th transaction carries maxPriorityFeePerGas >= 1 gwei * 1.25^8 ~ 5.96 gwei although no transaction with a higher fee ever existed in the mempool; a second resume after another 7 failures lands at >= 35 gwei priority.","severity":"info","snippet":"    last=fee;   // a refused bid counts too: the next attempt must beat it, or it would be refused the same way","title":"Every send error, not only a fee refusal, raises the next bid; bids have no ceiling across attempts and resumes"},{"citation":"resolved","description":"The stop is correct and recoverable by design, but it can repeat. On a resume the seed's landing is detected only through firstReceipt(seed.hashes) over 10 s and stableNonce() (two agreeing reads). If both are served by a node that is behind and the later gate read (notDone, which only needs one 'done' answer) is served by a fresh node, the resume sees the seed step as done, never raises nonce past seed.nonce, and the next step throws RESUME_OUT_OF_ORDER before sending. The record is unchanged (the seed is kept by the checkpoint at line 233), so the owner re-runs; under the same lag pattern the run stops again at the same place. Nothing is sent and no funds are at risk, which is why this is informational, but the message 'run the resume again' gives no hint when a second and third run stop the same way. Fix: when a gate says 'done' for seed.name while seed is still set, re-run firstReceipt(seed.hashes, stepTimeoutMs) and the nonce read once more before continuing, and on RESUME_OUT_OF_ORDER include the seed's hashes in the message so the owner can check them on Etherscan.","line":171,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"Record: steps=[deploy, setRevealURIs, setRevealCommit], pending={name:'addPaymentToken(USDC, 1600 GOLD)', nonce:N, hashes:[h]}, and h has landed. RPC script: eth_getTransactionReceipt(h) answers null for the first 6 calls, eth_getTransactionCount answers N for the first 2 calls, eth_call for paymentTokens(USDC) answers the real non-zero entry. Expected: the seed is pushed into steps and approvePaidPrice goes out at N+1. Actual: floor=N, firstReceipt times out, stableNonce()=N so the 5-minute wait is skipped, nonce=N, notDone(addPaymentToken) returns false on its first fresh read, send('approvePaidPrice') throws RESUME_OUT_OF_ORDER at nonce N. Re-running with the same script repeats it.","severity":"info","snippet":"  if(seed?.hashes.length&&seed.nonce===nonce&&seed.name!==name)throw Error('RESUME_OUT_OF_ORDER '+name+' at the nonce of '+seed.name+': run the resume again');","title":"RESUME_OUT_OF_ORDER recurs under a consistent lag pattern: a landed seed whose receipt and nonce reads lag but whose gate read is fresh stops every resume"}],"hash":"3e64267f9406ffb0fe50e6b3006759e39858b370d53bfd0b9d37fbe56af3eb70","nodeId":"8e36f179-e24c-4e52-bc46-2706148a52a2","outcome":"completed","summary":"Review complete. The findings file holds six entries, all validated against the tree (path, line and snippet match).\n\n**Outcome.** The Submission12 remediation holds up. The contract is unchanged, and the eight findings of audit 358f7f40 are each fixed as the README claims. I traced every resume path (floor, seed, steps push, NONCE_MOVED, RESUME_OUT_OF_ORDER, PLANNED_NONCE_USED) and found no way to deploy a second contract, send a step twice with effect, or lose a needed outbid. Nothing reached high or critical, so no Foundry proofs apply.\n\n**Findings written to .imd-findings.json:**\n\n- **Low, deploy-mainnet.mjs:179.** The `REPLACEMENT_GAS` fallback cannot tell \"the node estimated on top of the waiting transaction\" from \"this call reverts on the canonical chain\". For the four fixed-calldata steps that is harmless. For `setRevealCommit`, whose acceptance is rebuilt each attempt and signs a block hash, a forked node answering `recentBlock()` twice yields a reverting replacement that outbids and displaces the valid waiting one, spending the nonce. Fix: simulate against an explicit `latest` before using the fixed limit.\n- **Low, safe-batch.mjs:154.** A log chunk refused for about five seconds ends `accept` and `open` immediately with `LOGS_UNAVAILABLE`, while every other lagging read gets ten passes through `holds`. The documented bypass flag then removes the only extra-token check. Fix: let it count as a refused pass.\n- **Info, safe-batch.mjs:196.** `stableHead` runs before the chain-id check, so a wrong-chain read RPC with a lower head reports `HEAD_BEHIND_RECORD` \"try again\" for good instead of \"another chain\".\n- **Info, deploy-mainnet.ps1:66.** The two secrets are read before the `try`, so the `finally` does not cover a stop between the prompts. Moot under `-File`, visible when dot-sourced.\n- **Info, deploy-mainnet.mjs:180.** Any send error, including a 502 with nothing in the mempool, raises the next bid, and bumps compound across resumes with no ceiling.\n- **Info, deploy-mainnet.mjs:171.** `RESUME_OUT_OF_ORDER` can recur under one consistent lag pattern; nothing is sent, but the message gives no way forward.\n\n**Answers to the eight questions, in brief.** Floor can only be too low, never too high, and `stableNonce` covers low. Dropping `seed` happens only after the chain reports the nonce spent, and a lost-hash duplicate then fails on the estimate or reverts without effect. A reverted seed is correctly not pushed. The 1,000,000 limit is about six times the largest post-deploy step. None of the new stops is permanent on the right chain. A reorg that re-includes a recorded step in another block leaves the permanent read-back mismatch already accepted in Submission 11 section 6; the read-back otherwise covers every step's effect. A stale but consistent head can pass three reads, as the README accepts. PowerShell scoping is right on every path inside the `try`.\n\n**Not verified.** The tools import a `runtime.mjs` stand-in and a vendored ethers that are not in the package, so I could not execute the tool tests here; all reproductions are traced against the code and the existing test shapes. Whether the default public RPC refuses `eth_getLogs` for a `toBlock` above its head, or clamps silently, was not checked against a live node.","treeHash":null,"usage":{"cachedInputTokens":913068,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":54194,"runtime":"claude","turns":23,"wallClockMs":780060}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"60ffae3cc413a0c6","findings":[{"citation":"resolved","description":"The resume prologue starts with settled(steps.at(-1)), which needs provider.getTransactionReceipt(hash) to answer for the recorded hash on visibleReads consecutive reads and otherwise throws RECEIPT_NOT_VISIBLE after visibleTries*pollMs (90*2 s = 3 minutes). The recorded hash is the one attempt whose receipt the stopped run saw. When a step had two attempts in flight at the same nonce (attempt 1 waited stepTimeoutMs without a receipt, attempt 2 went out at +25%), the lower-fee attempt can be the one included first and recorded; a one-block reorg can then drop that block and the builders, which hold both transactions, include the higher-fee replacement instead. The chain state is correct (same step, same calldata; for setRevealCommit a fresh acceptance with the same commit) and the nonce is spent, but the recorded hash now has no receipt anywhere. Every subsequent --resume stops at line 150 with RECEIPT_NOT_VISIBLE before any gate, send or read-back runs. AUDIT_README section 5 accepts a reorg-removed recorded step on the grounds that 'the read-back reports a mismatch and a person must check', but on the resume path the read-back is never reached, and the error text does not say that the record must be edited by hand. Nothing in the tool can get past this state. The same trap exists for a step entry the prologue itself pushes from seed (line 158), since the next resume treats it as steps.at(-1).","line":150,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"State: partial-1-<t>.json with steps=[deploy@N0, setRevealURIs@N0+1, setRevealCommit@N0+2] and steps[2].hash=h1 (status 1 when recorded), pending absent; the chain's canonical block for nonce N0+2 now holds h2 (the tool's own +25% replacement of the same step), so eth_getTransactionReceipt(h1) returns null on every node and eth_getTransactionCount(deployer,'latest')=N0+3. Run: node tools/deploy-mainnet.mjs --resume. Expected: the resume notices the nonce N0+2 is spent and the gate revealCommit()==REVEAL_COMMIT says done, then sends addPaymentToken at N0+3. Actual: deployMainnet throws 'RECEIPT_NOT_VISIBLE <h1>' after ~3 minutes, every time it is run, with the partial record unchanged; the deployment can only continue after a person edits steps[2].hash in the record. Scripted check with the test harness in tests/contract/deploy-mainnet-cli.test.mjs: take saved from stoppedRun(rpc,srv,{stopBefore:4,mined:true}), set srv.state.script=m=>m.method==='eth_getTransactionReceipt'&&m.params[0]===saved.steps.at(-1).hash?null:undefined, and await deployMainnet({...input,resume:saved}): it rejects with /RECEIPT_NOT_VISIBLE/ instead of finishing with 6 steps. Fix: when the recorded hash yields no receipt within the window, do not stop; check that getTransactionCount('latest') > the step's nonce (planNonce+index) and let the existing gate and read-back decide, or wait on the recorded blockNumber being visible rather than on the hash; at minimum name the manual recovery in the error.","severity":"low","snippet":"  if(steps.length)await settled(steps.at(-1));","title":"Resume is blocked for good when the last recorded step's hash is no longer canonical: settled() waits for a receipt that will never come, and the read-back that section 5 relies on is never reached"},{"citation":"resolved","description":"AUDIT_README section 4.1 states 'A reverted waiting transaction is not pushed, and its nonce is counted.' That holds only when the reverted transaction is found through pending (floor=Math.max(floor,seed.nonce+1), line 157). On the in-run path a reverted receipt does nonce++ (line 192) and then, when no revertRetries remain, writes checkpoint(state()) with no pending and no step entry, so the record carries nothing about the spent nonce. On the next resume floor=planNonce+steps.length (line 151) is one below the chain's nonce, and nonce=Math.max(await stableNonce(),floor) (line 162) relies entirely on stableNonce, which is two consecutive reads that may both come from a node one block behind (the lag model this package tests against, and sticky keep-alive connections make the two reads likely to hit the same backend). The resume then sends the step at the spent nonce. A fresh node answers NONCE_EXPIRED, which with hashes empty reaches NONCE_MOVED only if the following stableNonce() is fresh; a node that is still behind accepts the transaction into its pool and returns a hash, after which firstReceipt waits stepTimeoutMs per attempt (8 x 5 min) for a receipt that cannot exist, then STEP_STUCK, and only the resume after that reaches PLANNED_NONCE_USED or the right nonce. No funds are lost and no second contract can be deployed (the nonce stays at planNonce for the deploy), but a reverted deploy or a reverted addPaymentToken costs an extra resume and up to ~45 minutes, and the record contradicts the documented invariant.","line":195,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"State: the deploy at planNonce=N0 lands with status 0 (out of gas under a changed gas schedule, or a constructor revert); the run writes partial-1-<t>.json with steps=[] and no pending, and throws 'TX_FAILED deploy EmberEvoGenesis'. Chain: getTransactionCount(deployer,'latest')=N0+1, no code at contract. Run --resume through an RPC whose first two eth_getTransactionCount answers are N0 (the stale pair the suite's finding-1 tests script with srv.state.script, stale=2). Expected: floor=N0+1, codeAbsent() is true and nonce!==planNonce, so the run stops at once with PLANNED_NONCE_USED and tells the owner to check Etherscan. Actual: floor=N0+0, nonce=Math.max(N0,N0)=N0, the PLANNED_NONCE_USED check passes, and send('deploy EmberEvoGenesis') broadcasts at nonce N0: against a fresh node it is refused (NONCE_EXPIRED) and the run stops with NONCE_MOVED only if the next stableNonce() happens to be fresh; against the stale node it is accepted, hashes=[h], and the loop waits 8 x stepTimeoutMs (40 minutes) before STEP_STUCK; the following resume then waits a further stepTimeoutMs in the prologue (line 155) before reaching PLANNED_NONCE_USED. Same shape for 'addPaymentToken(USDC, 1600 GOLD)' reverting at N0+3: the resume sends it again at N0+3. Fix: in the TX_FAILED branch write the spent nonce into the record, e.g. checkpoint(state({pending:{name,nonce:nonce-1,hashes:[rc.hash],fee:feeText(placed)}})) or a failed:[{name,nonce,hash}] list that line 151 counts, so floor is planNonce+steps.length+failed.length and matches the section 4.1 invariant on every path.","severity":"low","snippet":"    checkpoint(state());throw Error('TX_FAILED '+name);","title":"TX_FAILED drops the reverted step's nonce from the record, so the resume floor is one below the chain: a stale nonce read resends at a spent nonce (for the deploy, up to ~45 minutes before PLANNED_NON"},{"citation":"resolved","description":"The resume prologue gives a waiting transaction only firstReceipt(seed.hashes,pollMs*5) (10 s) to show a receipt; if the nonce is not yet spent it keeps seed and sets nonce=seed.nonce (lines 154-163). The gates then run for the seed's own step with visibleReads reads 2 s apart (line 218), after codeAbsent/codeVisible and the RESUME_CODE/RESUME_NOT_OWNER reads, i.e. typically 10-40 s later. A transaction that is simply still pending at the prologue and gets mined in that window makes notDone(seed.name) return false (done), but nothing advances nonce or clears seed: nonce++ happens only inside send (line 192). The next step's send then runs the line-171 check with seed.nonce===nonce and a different name and throws RESUME_OUT_OF_ORDER, whose text ('at the nonce of addPaymentToken') tells the owner the record is out of order when the real event is that the waiting transaction landed. When seed had no hashes (the stopped run was refused on every attempt and the nonce was later spent by a lost-hash broadcast), the check does not fire and the next step is sent at the spent nonce: NONCE_EXPIRED then NONCE_MOVED on a fresh node, or a pool-accepted transaction and 8 x stepTimeoutMs on a stale one, as in the TX_FAILED finding. Each variant needs another --resume; the second resume finds the receipt in the prologue and proceeds, so this is time and a wrong message, not a loss, but it is a direct answer to section 4.3: the stop is reached from a valid state and the message points at the wrong cause.","line":171,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"State: partial record with steps=[deploy, setRevealURIs, setRevealCommit] and pending={name:'addPaymentToken(USDC, 1600 GOLD)',nonce:N,hashes:[h1],fee:{...}}; h1 is in the mempool and not mined; getTransactionCount('latest')=N. Run --resume and have h1 mined about 15 s after the start (with the suite: stoppedRun(rpc,srv,{stopBefore:4,mined:false}), start deployMainnet({...input,resume:saved}), and call evm_mine from srv.state.script on the first eth_call whose data starts with the paymentTokens(address) selector GATE, i.e. during the gate reads). Expected: the tool records h1 (or at least treats nonce N as spent), sets nonce=N+1 and sends approvePaidPrice at N+1, finishing with 6 steps. Actual: notDone('addPaymentToken(USDC, 1600 GOLD)') returns false after h1 lands, nonce stays N, seed stays set, and send('approvePaidPrice') throws 'RESUME_OUT_OF_ORDER approvePaidPrice at the nonce of addPaymentToken(USDC, 1600 GOLD): run the resume again'; rawSends is unchanged and a second --resume is required. Fix: in notDone, when the gate says done for the step named by seed (or for any step with stepBlock undefined), fetch the receipt of seed.hashes once more, push it into steps if status 1, set nonce=Math.max(nonce,seed.nonce+1) and seed=null; or make the gate for the seed's step wait for firstReceipt(seed.hashes) before reading, as the prologue does, so a landed waiting transaction is always recorded rather than inferred.","severity":"low","snippet":"  if(seed?.hashes.length&&seed.nonce===nonce&&seed.name!==name)throw Error('RESUME_OUT_OF_ORDER '+name+' at the nonce of '+seed.name+': run the resume again');","title":"A waiting transaction that lands during the gate reads leaves nonce at its spent value: the next step stops with a misleading RESUME_OUT_OF_ORDER (or is sent at the spent nonce when the record had no "}],"hash":"6163e21e08928345bd763fb0e2141e821c5212d2037941e82f04a5076e9a9a8c","nodeId":"cc84ada6-8b7f-4c9b-90ec-28aec9a13f5e","outcome":"completed","summary":"The review is complete and the findings file is written and validated. Here is the recap.\n\n**Outcome.** Three low-severity findings, all in the resume path of `Submission12/tools/deploy-mainnet.mjs`, are in `.imd-findings.json` with concrete states, expected versus actual, and fixes. Nothing critical or high. The safe-batch, signer and PowerShell changes held up against the questions asked. The test suite could not be run here because `tools/runtime.mjs` and the private artifacts are not in the package, so every finding is from tracing the code and the shipped test harness, not from execution.\n\n**Findings**\n\n- **Resume blocked for good after a reorg swaps the recorded hash.** The prologue waits on the receipt of the last recorded step's hash. If a one-block reorg drops that block and builders include the tool's own higher-fee replacement instead, the state is correct but the hash has no receipt anywhere. Every resume stops with `RECEIPT_NOT_VISIBLE` after three minutes and never reaches the read-back that section 5 relies on. Fix: fall back to the nonce being spent plus the gate, and name the manual recovery.\n- **TX_FAILED drops the reverted nonce from the record.** Section 4.1 says a reverted transaction's nonce is counted, but that only holds on the pending path. On the in-run path the checkpoint has no pending and no step, so the resume floor is one short. With a stale nonce pair the resume resends at the spent nonce. For a reverted deploy that means up to 40 minutes of `STEP_STUCK` on a lagging node before `PLANNED_NONCE_USED`. Fix: record the spent nonce in that checkpoint and count it in the floor.\n- **A waiting transaction that lands during the gate reads.** The prologue gives it 10 seconds. If it mines during the later gate reads, the gate says done but the nonce never advances, so the next step hits `RESUME_OUT_OF_ORDER` with a message that names the wrong cause. One extra resume fixes it. Fix: when the seed's gate says done, fetch its receipt, record it and advance the nonce.\n\n**Answers to the eight questions where no defect was found**\n\n- The floor is a lower bound on every path except the TX_FAILED one above. Dropping the seed only happens when the chain says its nonce is spent, so no needed outbid is lost. A step enters `steps[]` only from its own hashes with status 1.\n- `REPLACEMENT_GAS` cannot reach the deploy and a replacement shares the pending nonce, so at worst it buys a cheap on-chain revert. Under today's rules the largest step is 169k gas. Under EIP-8037's new-slot surcharge, setRevealURIs with its six new slots lands near 760k, still under the cap but with only about 1.3x margin. That is a lead to recheck against the final fork values, not a finding.\n- `NONCE_MOVED` and `HEAD_BEHIND_RECORD` converge on the next resume or next block. Only the reorg case above is permanent.\n- The read-back compares every value the six steps set, so an unsent recorded step hides nothing except the block-number mismatch already accepted.\n- Fees rise 25 percent per attempt including refused ones, and only placed fees reach the record. Worst case is about 4.8x the market tip on one step, which is not a bad overpay. The caps never fall below the tip.\n- `holds` with pinned reads behaves as documented. A stale pass is still possible when the agreed head is behind, which section 5 already accepts. Log chunking has no gap or overlap.\n- The PowerShell key lives in two environment windows only, both cleared on every throw, Ctrl+C and normal exit by the finally block. Nothing is set before the address check. The in-memory string residue is already accepted.\n- Each of the eight 358f7f40 fixes is present where the table says.\n\n**Sources** consulted for the gas-schedule note: [glamsterdam repricing testing](https://blog.ethereum.org/2026/08/24/glamsterdam-repricing-testing), [EIP-8007](https://eips.ethereum.org/EIPS/eip-8007), [EIP-8038](https://eips.ethereum.org/EIPS/eip-8038), [EIP-8037 overview](https://tryethernal.com/blog/eip-8037-m","treeHash":null,"usage":{"cachedInputTokens":1293720,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":55988,"runtime":"claude","turns":28,"wallClockMs":825776}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"cc86b49d3c401897","findings":[{"citation":"resolved","description":"tokenLogs always starts its first chunk at the record's startBlock (line 97 / 107). The default RPC (PUBLIC_RPC, line 37) does not cap the width of a log range: it refuses any eth_getLogs whose fromBlock is more than about 10,000 blocks behind its head, with -32602 'Archive requests require a personal token'. Chunking cannot help, because the very first chunk is the old one. steady() retries it 5 times, the .catch turns it into extraTokens=null, checkRecord throws LOGS_UNAVAILABLE, and holds() rethrows it at once (line 154). The owner's plan runs `open` days or weeks after the deploy: the metadata host, Worker and final list are not built yet, and USDC only becomes usable after two days. So on the default RPC `open` cannot pass without --logs-checked-on-etherscan, and that flag skips the extraTokens check entirely. The check that the deployer key, which holds every owner power until the Safe accepts, added no second payment token is therefore skipped in the normal case. AUDIT_README section 5 says only an RPC 'whose log limit is under 10,000 blocks' hits this and that the default RPC 'was not tried'. That is the wrong mechanism: the limit is on age, not range. `status` (line 199, holds(state,x=>x)) also prints extraTokens:null with no warning. The same RPC refuses eth_call pinned 127 or more blocks back. The current code re-reads the head before each pass, so it is not hit by that, but it rules out pinning reads to the record's block on this RPC. Fix: Read the count without history. For example, read extraPaymentTokenCount with eth_getStorageAt at the pinned head, using its slot from the build's storage layout, and compare it to 1 in checkRecord, keeping the log count as an optional cross-check. Alternatively require a READ_RPC_URL that serves logs from the deploy block for accept/open. Also have `status` warn when extraTokens is null, and correct AUDIT_README section 5.","line":107,"path":"Submission12/tools/safe-batch.mjs","reproduction":"Measured 2026-11-03 against https://ethereum-rpc.publicnode.com at head 0x191cd68 (26,332,520). eth_getLogs {address:Safe, fromBlock: head-9999, toBlock: +10} returned {\"result\":[]}. The same request with fromBlock head-10500, head-12000 or head-50000 returned {\"error\":{\"code\":-32602,\"message\":\"Archive requests require a personal token. Get one at: https://www.allnodes.com/publicnode\"}}. eth_call {to:Safe,data:0xe75235b8} at head-100 returned ...02, and at head-127 the same -32602 error. Tool trace: record with startBlock S, run `node safe-batch.mjs open <record> <wallets> <taken> <start> --sample-look=...` (or `accept`) with no READ_RPC_URL when head > S+10,000. Expected: extraTokens='1', checkOpen passes. Actual: queryFilter('PaymentTokenAdded',S,S+9999) is refused 5 times, extraTokens=null, LOGS_UNAVAILABLE is rethrown by holds on the first pass, and no batch is written. Re-running with --logs-checked-on-etherscan writes the batch with the token count never checked.","severity":"medium","snippet":" const from=d.startBlock??d.deployBlock,extraTokens=from==null?null:await tokenLogs(g,Number(from),head).catch(()=>null);","title":"safe-batch: on the default READ RPC (publicnode) every `open`, and any `accept` run more than ~10,000 blocks (~33 h) after the deploy, ends in LOGS_UNAVAILABLE, which makes the --logs-checked-on-ether"},{"citation":"resolved","description":"A recorded step holds one hash, the one whose receipt firstReceipt saw first (line 197). The tool itself keeps up to 8 competing transactions of one step at one nonce (25% higher bids after stepTimeoutMs). Suppose h1 is mined and settled (3 reads about 6 s apart, inside one slot) and pushed into steps[], then a 1-block reorg drops its block and the next builder includes the step's replacement h2. The chain state is right, but eth_getTransactionReceipt(h1) is null for good. Every later --resume runs settled(steps.at(-1)) first: it polls visibleTries (90 x 2 s) and throws RECEIPT_NOT_VISIBLE, before codeAbsent, the gates, NONCE_MOVED or the read-back. AUDIT_README section 5 says a reorg-removed recorded step leads to a read-back mismatch that a person checks. On this path the read-back is never reached, and nothing says what to edit. deploy-mainnet.ps1 line 112 tells the owner to run RESUME again, which loops on the same stop. A step pushed from seed (line 158) becomes steps.at(-1) on the next resume and has the same exposure. This is a liveness fault only: nothing is sent. (Merges the audit_math and audit_permissions findings.) Fix: Record every hash of a step (hashes is known at line 197) and accept a receipt for any of them in settled. If none appears, drop that last entry from steps[] (or skip the wait) and let codeAbsent/notDone and the read-back decide from the chain. At minimum, name the manual edit in the error and in the README.","line":150,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"Reproduced by importing an unmodified copy of Submission12/tools/deploy-mainnet.mjs with ethers 6.15.0 into a stub harness: a JS chain model behind provider and deployer objects, a stub runtime.mjs, artifacts and signer imports, cfg.reviewed matched to the stub build, timing {pollMs:1,stepTimeoutMs:50,visibleReads:3,visibleTries:20}. sendTransaction below the chain nonce throws code NONCE_EXPIRED, as ethers maps it. First run until steps=[deploy@0,setRevealURIs@1,setRevealCommit@2] is checkpointed with no pending, then killed. Then delete the receipt of steps[2].hash from the chain model, which leaves the state and nonce 3 unchanged: the effect of a reorg that re-included the step's replacement. deployMainnet({...,resume:record}). Expected: the gates see revealCommit set, and addPaymentToken goes out at nonce 3. Actual: 'ERR RECEIPT_NOT_VISIBLE 0x183a7d36...', 0 new sends, identical on every rerun.","severity":"low","snippet":"  if(steps.length)await settled(steps.at(-1));","title":"Resume is blocked for good when the last recorded step's hash is no longer canonical: settled() throws RECEIPT_NOT_VISIBLE before any gate or read-back runs"},{"citation":"resolved","description":"AUDIT_README 4.1 says 'A reverted waiting transaction is not pushed, and its nonce is counted'. That holds only on the seed path (line 157). In send(), a reverted receipt does nonce++ (line 192) and then either retries (revertRetries, line 194) or writes checkpoint(state()) with no pending and throws TX_FAILED (line 195). Neither path leaves anything in the record about the spent nonce. On resume, floor=planNonce+steps.length is one below the chain, so the bound that answers audit 358f7f40 finding 1 is missing, and nonce comes only from stableNonce(): two reads that a lagging node can answer stale. Case 1, a reverted deploy: the owner should get PLANNED_NONCE_USED at once. Instead the tool broadcasts a second deploy at the spent planNonce. A fresh node refuses it, so no second contract is created. The run then stops with NONCE_MOVED. If the node is still behind, it can accept the transaction into its pool and the loop waits 8 x stepTimeoutMs. Case 2, a reverted setRevealCommit attempt followed by a retry that lands, then a kill before the next send: the resume sends addPaymentToken at the spent nonce and stops with NONCE_MOVED. The result is extra stops and time, with no loss. (Merges the audit_math TX_FAILED finding and the audit_flow floor finding.) Fix: Carry the run's next nonce in every checkpoint (state=>({...,nextNonce:nonce})) and set floor=Math.max(planNonce+steps.length, resume.nextNonce??0). Or record reverted attempts (status 0) in a failed[] list that the floor counts, which also makes totalGas include the burned gas.","line":151,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"Reproduced by importing an unmodified copy of Submission12/tools/deploy-mainnet.mjs with ethers 6.15.0 into a stub harness: a JS chain model behind provider and deployer objects, a stub runtime.mjs, artifacts and signer imports, cfg.reviewed matched to the stub build, timing {pollMs:1,stepTimeoutMs:50,visibleReads:3,visibleTries:20}. sendTransaction below the chain nonce throws code NONCE_EXPIRED, as ethers maps it. (1) The deploy at nonce 0 is mined with status 0. The first run ends with TX_FAILED, record steps=[], no pending, chain nonce 1. Resume with the first two eth_getTransactionCount answers stale (0). Expected: floor=1 and PLANNED_NONCE_USED. Actual: the tool resends DEPLOY@0, then 'NONCE_MOVED deploy EmberEvoGenesis: the chain is past nonce 0'. (2) setRevealCommit attempt 1 at nonce 2 reverts and the retry lands at 3. The run is killed after the checkpoint that has steps.length=3 and no pending, with chain nonce 4. Resume with the first two count reads at 3. Expected: addPaymentToken at 4. Actual: it is sent at nonce 3, then 'NONCE_MOVED addPaymentToken(USDC, 1600 GOLD): the chain is past nonce 3'.","severity":"low","snippet":"  let floor=planNonce+steps.length;","title":"A reverted transaction's nonce is not in the record (TX_FAILED, and setRevealCommit revertRetries), so the resume floor is one below the chain and a stale nonce pair resends at a spent nonce, the depl"},{"citation":"resolved","description":"The prologue gives the waiting transaction firstReceipt(seed.hashes,pollMs*5), about 10 s. If its nonce is not yet spent, seed stays and nonce=seed.nonce. The seed step's gate runs later, after codeAbsent, codeVisible and the RESUME_CODE/RESUME_NOT_OWNER reads. If the transaction is mined in that window, notDone(seed.name) says done. Nothing then advances nonce, clears seed or records the receipt: nonce++ only happens inside send. The next step's send hits line 171 and throws RESUME_OUT_OF_ORDER, whose text points at an out-of-order record when the real event is that the waiting transaction landed. A second resume finds the receipt and continues. The same mechanism makes the stop repeat under a lag pattern where receipt and nonce reads come from a node that is behind and the gate read from a fresh one. The cost is an extra run and a wrong diagnosis, with no loss. (Merges the audit_math finding and the audit_economics info finding.) Fix: In notDone, when the gate says done for seed.name while seed is set, wait for firstReceipt(seed.hashes), push it to steps if status 1, then set nonce=Math.max(nonce,seed.nonce+1) and seed=null. Include the seed hashes in the RESUME_OUT_OF_ORDER text.","line":171,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"Reproduced by importing an unmodified copy of Submission12/tools/deploy-mainnet.mjs with ethers 6.15.0 into a stub harness: a JS chain model behind provider and deployer objects, a stub runtime.mjs, artifacts and signer imports, cfg.reviewed matched to the stub build, timing {pollMs:1,stepTimeoutMs:50,visibleReads:3,visibleTries:20}. sendTransaction below the chain nonce throws code NONCE_EXPIRED, as ethers maps it. The first run sends addPaymentToken at nonce 3 into the pool, never mined. The record ends with steps=[deploy,setRevealURIs,setRevealCommit] and pending {name:'addPaymentToken(USDC, 1600 GOLD)',nonce:3,hashes:[...]}. On resume, the pooled transaction is mined on the first eth_call to paymentTokens(address), which is the gate read. Expected: the step is recorded and approvePaidPrice goes out at 4. Actual: 'RESUME_OUT_OF_ORDER approvePaidPrice at the nonce of addPaymentToken(USDC, 1600 GOLD): run the resume again', 0 sends. A second resume gets past it.","severity":"low","snippet":"  if(seed?.hashes.length&&seed.nonce===nonce&&seed.name!==name)throw Error('RESUME_OUT_OF_ORDER '+name+' at the nonce of '+seed.name+': run the resume again');","title":"A waiting transaction that lands during the resume's gate reads is never recorded, so the next step stops with a misleading RESUME_OUT_OF_ORDER"},{"citation":"resolved","description":"last is set before sendTransaction, so any error raises the next fee: a gateway 502, SERVER_ERROR, a rate limit or INSUFFICIENT_FUNDS. When nothing is sent, the loop waits only pollMs*3 (line 190), so 8 attempts fit in well under a minute. There is no ceiling, and a resume restarts the chain of bumps from the recorded fee, so two stuck runs compound. On mainnet the tip is paid in full, so an RPC fault alone, with no competing bid anywhere, overpays the 5.9M-gas deploy by about 7.5 gwei/gas, or ~0.044 ETH at a 2 gwei market tip. A timeout after broadcast does need an outbid. The defect is that the bump is not limited to errors that can mean a bid exists, and is not capped. (Merges the audit_flow low and the audit_economics info findings.) Fix: Bump only after a successful send, or on errors that can mean a bid is held: REPLACEMENT_UNDERPRICED, 'already known', or a timeout after broadcast. Add a ceiling on maxPriorityFeePerGas, for example 3x the first attempt, after which the run stops with STEP_STUCK.","line":180,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"Reproduced by importing an unmodified copy of Submission12/tools/deploy-mainnet.mjs with ethers 6.15.0 into a stub harness: a JS chain model behind provider and deployer objects, a stub runtime.mjs, artifacts and signer imports, cfg.reviewed matched to the stub build, timing {pollMs:1,stepTimeoutMs:50,visibleReads:3,visibleTries:20}. sendTransaction below the chain nonce throws code NONCE_EXPIRED, as ethers maps it. getFeeData returns tip 2 gwei and maxFee 30 gwei. The first 7 deploy sendTransaction calls throw code SERVER_ERROR ('502 Bad Gateway') before broadcast, and the 8th is accepted. The 8 attempts bid tips of 2, 2.5, 3.125, 3.906, 4.883, 6.104, 7.629 and 9.537 gwei, with maxFee rising from 30 to 143.05 gwei. Expected: about 2 gwei and 30 gwei, since no earlier bid existed. Actual: the accepted deploy carries a 9.537 gwei tip and 143.05 gwei maxFee.","severity":"low","snippet":"    last=fee;   // a refused bid counts too: the next attempt must beat it, or it would be refused the same way","title":"Every send error, including a transport error with nothing broadcast, raises the next bid 25%: seven 502s make the deploy bid 4.77x the node's suggested tip and maxFee"},{"citation":"resolved","description":"chainState maps every tokenLogs error to extraTokens=null (line 107). That includes transient ones: a timeout, a 429 after the 20 parallel eth_call reads, or a node behind the pinned head refusing toBlock. One chunk failing for about 5 s (steady: 5 tries x 1 s) is enough. checkRecord then throws LOGS_UNAVAILABLE, and holds rethrows it at once. A CHAIN_DIFFERS_FROM_RECORD or HEAD_BEHIND_RECORD from the same lag is retried for up to 10 passes. The resulting message recommends --logs-checked-on-etherscan, which turns the check off. This is separate from the structural refusal by the default RPC, which is reported as the medium finding. (Merges the audit_flow and audit_economics findings.) Fix: Treat LOGS_UNAVAILABLE as an ordinary refused pass, and throw it only if it is still present when tries run out. In tokenLogs, map only the RPC's explicit range/limit errors to null and let other errors propagate.","line":154,"path":"Submission12/tools/safe-batch.mjs","reproduction":"With an unmodified copy of safe-batch.mjs: holds(read, check, {passMs:1,refusedMs:1}). read returns {extraTokens:null} on pass 1 and {extraTokens:'1'} afterwards. check throws Error('LOGS_UNAVAILABLE x') on null, as checkRecord does. Result: 'holds threw after pass 1 : LOGS_UNAVAILABLE x'. The same holds() with a check that throws 'CHAIN_DIFFERS_FROM_RECORD x' on pass 1 only passes after 4 reads. Expected: a log refusal on one pass is retried too.","severity":"low","snippet":"   if(String(e.message).startsWith('LOGS_UNAVAILABLE'))throw e;   // already asked several times","title":"safe-batch: a transient eth_getLogs failure ends accept/open on the first pass; holds() rethrows LOGS_UNAVAILABLE instead of retrying it like other refusals"},{"citation":"resolved","description":"The provider is built with staticNetwork:true and the record's chainId (line 193), so ethers never checks the RPC's chain. eth_chainId is only compared inside chainState, after stableHead has returned. If the RPC's head is below recordBlock(d), stableHead throws HEAD_BEHIND_RECORD after 30 s. holds treats that as a refusal and retries it 10 times, and the run then exits telling the owner to try again, which can never succeed. Nothing is signed, so only the diagnosis is affected. Fix: Read eth_chainId once in the CLI block before the first stableHead and refuse there.","line":196,"path":"Submission12/tools/safe-batch.mjs","reproduction":"stableHead({getBlockNumber:async()=>1500000}, 9500000, {tries:3,ms:1}) throws 'HEAD_BEHIND_RECORD the read RPC does not show block 9500000 yet: try again'. In the CLI the eth_chainId check that would say 'the read RPC is on another chain' is never reached. Example: a Sepolia record at startBlock 9,500,000 with READ_RPC_URL pointing at a chain at height 1,500,000.","severity":"info","snippet":"  const state=async()=>{high=await stableHead(provider,high);return chainState(provider,d,high);};","title":"safe-batch: stableHead runs before the chain-id check, so a wrong-chain READ_RPC_URL with a lower head reports HEAD_BEHIND_RECORD 'try again' for ~5 minutes instead of the real cause"},{"citation":"resolved","description":"$rpcUrl (line 65) and $deployerKey (line 66) are assigned before the try at line 67. Every exit inside the try reaches the finally, which nulls both and removes the env entries. That covers the key check, pre-check failure, cancel, deploy exit 1/2 and Ctrl+C inside the try. A Ctrl+C at the key prompt, or an exception from Read-Secret, exits with $rpcUrl set and never cleared. When the script is run with -File as documented, the process ends and nothing persists. When it is dot-sourced into an interactive session, which the header does not forbid, the URL with its API key stays readable in the session. Fix: Move lines 63-66 inside the try, or open the try before the first Read-Secret.","line":66,"path":"Submission12/tools/deploy-mainnet.ps1","reproduction":"By reading the code (PowerShell was not run): `. .\\deploy-mainnet.ps1`, answer the address and RPC prompts, press Ctrl+C at 'Deployer wallet private key', then `$rpcUrl` prints the URL. Ctrl+C anywhere after line 67 leaves it empty, because the finally runs.","severity":"info","snippet":"$deployerKey = Read-Secret 'Deployer wallet private key'","title":"deploy-mainnet.ps1: the RPC URL and deployer key are read before the try, so a stop between the prompts leaves them set and the finally never clears them"},{"citation":"resolved","description":"The fallback (line 179) runs only when the step already has a hash and is not the deploy. The replacement shares the nonce, so it cannot double-execute, and with fixed calldata it reverts only if the waiting transaction would also revert. Under today's pricing the limit is enough: the largest step is 169,104 gas, 5.9x headroom. It is not tied to anything, though. Scaling the project's own preflight allowance (~6.3M to ~40M 'if EIP-8037 is live', 6.3x) to setRevealURIs gives ~1.07M, above the constant. A replacement mined on that limit would go out of gas, burn 1M gas and end the run with TX_FAILED. That would also hit the uncounted-nonce finding above. The first send of every step used a successful estimate, so a known-good limit exists. Fix: Keep the last successful limit for the step, and store it in the pending checkpoint as gasLimit so a resume can reuse it. Fall back to that, using REPLACEMENT_GAS only when no limit is known.","line":24,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"Arithmetic only, not executed: 169,104 x (40M/6.3M) = 1,073,642 > 1,000,000. Path: setRevealURIs with hashes=[h1] sent on an estimated limit, a node that estimates on top of h1 so the estimate reverts, and line 179 returns REPLACEMENT_GAS. No failure under today's pricing.","severity":"info","snippet":"const LOW128=(1n<<128n)-1n,PAYMENT_TOKEN_DELAY=2*86400,REPLACEMENT_GAS=1_000_000n;","title":"REPLACEMENT_GAS is a fixed 1,000,000 instead of the limit the step's first, estimated send already used"}],"hash":"9dcc9c7cfc68ab3788e87c0a71025a779388e77f081f456f9812d4a51a693be4","nodeId":"18688c46-ebc8-4962-a18c-f5e3a0bc573c","outcome":"completed","summary":"I found 9 problems in Submission12: 1 medium, 4 low and 4 info, all in the tools. The contract was not in scope. The findings are in `.imd-findings.json`, and no repository files were changed.\n\n**How I checked them.** The project's own test runtime (`runtime.mjs`) is not published, so I couldn't run its test suite. Instead I loaded unmodified copies of `deploy-mainnet.mjs` and `safe-batch.mjs` into a small stand-in chain I wrote in `/tmp`, using ethers 6.15.0, and ran each scenario through the real tool code. I checked the public RPC with live requests. No finding is in Solidity, so none carries a Foundry proof.\n\n**Medium**\n- **`safe-batch.mjs:107`:** on the default RPC (`ethereum-rpc.publicnode.com`), `open`, and any `accept` run more than about 33 hours after the deploy, always stops with `LOGS_UNAVAILABLE`. That RPC refuses log requests that start more than about 10,000 blocks behind its head. I measured it today:\n  - logs from 9,999 blocks back are answered;\n  - logs from 10,500, 12,000 or 50,000 blocks back are refused with -32602 \"Archive requests require a personal token\";\n  - `eth_call` is refused from 127 blocks back.\n\n  The log range always starts at the deploy block, so splitting it into 10,000-block chunks doesn't help: the first chunk is the old one. The only way past is `--logs-checked-on-etherscan`, which turns off the check that the deployer key added no second payment token. That means audit finding 5 is not really fixed for the normal case. `status` also shows `extraTokens: null` with no warning, and README section 5 describes the limit wrongly (it limits how old the range is, not how wide).\n\n**Low (each reproduced on the stand-in chain)**\n- **`deploy-mainnet.mjs:150`:** if a reorg replaces the last recorded step with the tool's own higher-fee resend, every resume stops with `RECEIPT_NOT_VISIBLE` and sends nothing. The read-back that README section 5 relies on is never reached.\n- **`:151`:** a reverted transaction's nonce never reaches the record, either on `TX_FAILED` or on a `setRevealCommit` retry. With two stale nonce reads, a resume tries the deploy again at the spent nonce 0, instead of stopping with `PLANNED_NONCE_USED`, and then stops with `NONCE_MOVED`. The same happens for `addPaymentToken` after a reverted `setRevealCommit`. No second contract can result, but each case costs an extra stop.\n- **`:171`:** if the waiting transaction lands while the resume is reading the gates, the next step stops with a misleading `RESUME_OUT_OF_ORDER`. A second resume gets past it.\n- **`:180`:** every send error raises the next bid by 25%, even when nothing was broadcast. Seven HTTP 502s took the deploy from a 2 to a 9.54 gwei tip and from 30 to 143 gwei max fee, with no cap.\n- **`safe-batch.mjs:154`:** a log request that fails only briefly stops `accept`/`open` on the first pass. Every other refusal gets up to 10 retries.\n\n**Info**\n- **`safe-batch.mjs:196`:** an RPC on the wrong chain with a lower block height reports `HEAD_BEHIND_RECORD ... try again` for about 5 minutes. The chain-id check comes after the head wait, so it is never reached. Reproduced.\n- **`deploy-mainnet.ps1:66`:** the RPC URL and the key are read before the `try` starts, so stopping at the key prompt leaves the URL in a dot-sourced session. I found this by reading the code; I didn't run PowerShell.\n- **`deploy-mainnet.mjs:24`:** the fixed 1,000,000 replacement gas limit is enough today (largest step 169,104 gas). If the tool's own estimate of about 6.3x higher gas costs under EIP-8037 is right, `setRevealURIs` would need about 1.07M. This is arithmetic only.\n\n**Specialist findings I merged or dropped.** I merged the duplicates: the two `RECEIPT_NOT_VISIBLE` reports, the two nonce-floor reports, the two `RESUME_OUT_OF_ORDER` reports, the two fee-escalation reports and the two log-retry reports. I dropped \"`REPLACEMENT_GAS` resends a reverting `setRevealCommit`\". It needs a node that consistently serves a non-canonical block two below its head, and","treeHash":null,"usage":{"cachedInputTokens":1402700,"inputTokens":35,"model":"claude-opus-5-5","outputTokens":28222,"runtime":"claude","turns":20,"wallClockMs":292584}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"63274cc86a9400de","findings":[{"citation":"resolved","description":"Audit 358f7f40 finding 5 is remediated by refusing `accept`/`open` when the PaymentTokenAdded range cannot be read (`checkRecord`, line 113) and by chunking the read in 10,000-block pieces from the deploy block (`tokenLogs`, line 97; `from=d.startBlock`, line 107). AUDIT_README section 5 says only an RPC 'whose log limit is under 10,000 blocks' would hit this and that the default RPC was not tried. I tried it. The default RPC `https://ethereum-rpc.publicnode.com` does not limit the *width* of a range: it refuses any `eth_getLogs` whose fromBlock is more than about 10,000 blocks behind its head (about 33 hours) with JSON-RPC error -32602 'Archive requests require a personal token', and it refuses pinned `eth_call` at a block 128 or more behind head with the same error. Measured on 2026-11-03 at head 26,332,474: an 11-block window starting 9,999 blocks back answers `[]`; the same window starting 10,050, 10,500, 11,000, 12,000, 14,000 and 17,000 blocks back is refused; `eth_call` 100 blocks back answers, 128/140/160 back is refused. Consequence for the tool as shipped: `tokenLogs` always starts its first chunk at the deploy block, so on the default RPC every `open` (which by the owner's own plan runs days or weeks after the deploy: metadata host, Worker, final free list are not built yet, USDC is usable only two days later) and every `accept` run more than ~33 hours after the deploy fails all 5 `steady` retries, `extraTokens` becomes null, `checkRecord` throws LOGS_UNAVAILABLE and `holds` rethrows it at once. The documented way on is `--logs-checked-on-etherscan`, which makes `s.extraTokens===null` pass with no on-chain check at all: the very check finding 5 asked for (that the deployer key, which holds every owner power until the Safe accepts, added no second token) is bypassed in the normal case, and the flag the README calls 'for that case only' becomes the routine path. `status` mode additionally prints `extraTokens: null` with no warning (the Submission 11 console.warn was removed), so an owner looking at `status` before signing sees nothing wrong. The chunk loop also makes it worse rather than better on this RPC: the refused call is the first one, so chunking never helps, and `holds` ends by throwing the raw ethers SERVER_ERROR rather than a named stop if the error reaches it through another path. The pinned `eth_call` window (128 blocks) is not a problem for the code as written, because `high` is re-read right before each pass, but it is the same mechanism and rules out any future 'read at the record's block' use of this RPC.","line":37,"path":"Submission12/tools/safe-batch.mjs","reproduction":"State: a mainnet deployment record `1-<t>.json` whose `startBlock` is more than 10,000 blocks behind the current head (deploy 34 hours ago or earlier), no READ_RPC_URL set. Run: `node contract/tools/safe-batch.mjs open <record> <wallets.json> <snapshot time> <opening time> --sample-look=<look>/<skin>` (or `accept <record>`). Expected: `chainState` returns `extraTokens:'1'` and `checkOpen` passes. Actual: the first `queryFilter('PaymentTokenAdded', startBlock, startBlock+9999)` is answered `{\"error\":{\"code\":-32602,\"message\":\"Archive requests require a personal token. Get one at: https://www.allnodes.com/publicnode\"}}` five times, `tokenLogs` rejects, `extraTokens` is null, `checkRecord` throws `LOGS_UNAVAILABLE ...` and `holds` rethrows it immediately; the run writes no batch. Re-running with `--logs-checked-on-etherscan` writes the batch with the token count unchecked. Independent reproduction without the tool (measured 2026-11-03 at head 26,332,474 = 0x191cd3a; use head-10050 and head-9999 of the current head when re-running): `curl -s -X POST https://ethereum-rpc.publicnode.com -H 'content-type: application/json' --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"eth_getLogs\",\"params\":[{\"fromBlock\":\"0x191a5f8\",\"toBlock\":\"0x191a602\",\"address\":\"0x29e7c208481f648795f677b55F6ECb2bADc2F01E\"}]}'` (an 11-block window starting 10,050 blocks behind head) returns `{\"error\":{\"code\":-32602,\"message\":\"Archive requests require a personal token...\"}}`, while the 10,000-block range 0x191a62b..0x191cd3a (starting 9,999 behind head) returns `[]`. The same request with `eth_call` `{\"to\":\"0x29e7c208481f648795f677b55F6ECb2bADc2F01E\",\"data\":\"0xe75235b8\"}` at block head-100 answers 0x..02 and at head-128 returns the same -32602 error. Fix: read the count from the contract instead of from logs, which needs no history: `extraPaymentTokenCount` (contracts/EmberEvoGenesisCandidate.sol line 134) is private but readable with `eth_getStorageAt(contract, <its slot from the build's storage layout>, head)` at the same pinned head; compare it to 1 in `checkRecord` and keep the log count only as a secondary cross-check that may be null. Alternatively make READ_RPC_URL mandatory for `accept`/`open` with a note that it must serve `eth_getLogs` from the deploy block, drop `--logs-checked-on-etherscan` for `open`, and have `status` print a visible warning when `extraTokens` is null. Also correct AUDIT_README section 5: the default RPC limits log *age* (~10,000 blocks) and state age (128 blocks), not range width.","severity":"medium","snippet":"const PUBLIC_RPC='https://ethereum-rpc.publicnode.com';","title":"safe-batch: the default read RPC refuses PaymentTokenAdded logs older than ~10,000 blocks, so `open` (and a late `accept`) always ends in LOGS_UNAVAILABLE and the only way on is the --logs-checked-on-"},{"citation":"resolved","description":"The resume order (fix for findings 1 and 2) first waits for the record's last step with `settled(steps.at(-1))`, which needs `getTransactionReceipt(step.hash)` to be non-null on 3 consecutive reads and throws `RECEIPT_NOT_VISIBLE` after `visibleTries` (90 x 2 s = 3 minutes). A recorded step only ever holds one hash: the one whose receipt `firstReceipt` saw first (line 197). The tool itself creates competing transactions for one step: after `stepTimeoutMs` an attempt resends the same step at the same nonce with a 25% higher fee, so h1 and h2 (and up to 8) are in mempools at once. If h1 is mined, seen by `settled` (three reads within about 6 s, inside one slot), pushed into `steps[]`, and the block is then dropped by a one-block reorg while the next builder takes the better-paying h2, the step is done on chain under h2 but the record names h1, whose receipt will never exist. The same state arises from any reorg that re-includes a step under a different hash. If the run was killed before the next step (the window the resume exists for), every later `--resume` runs line 150 first: 3 minutes of polling, then `RECEIPT_NOT_VISIBLE 0x<h1>` and exit 1, before `codeAbsent`, the gates, `NONCE_MOVED` or the read-back are reached. deploy-mainnet.ps1 line 112 tells the owner to 'run this script again: it offers RESUME', which loops on the same stop. AUDIT_README section 5 states 'A recorded step that a reorg removes is not resent. If it does not land again, the read-back reports a mismatch and a person must check' - but a resume never reaches the read-back in this state, and nothing in the package says what the person should change (deleting the last `steps[]` entry by hand is the only way on, and the README warns elsewhere against editing records). This is a liveness defect in the resume path (question 3/4 of the brief), not a fund or ownership risk: nothing is sent and no second contract can appear.","line":150,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"State: partial record `partial-1-<t>.json` with `steps` = [deploy, setRevealURIs, setRevealCommit, addPaymentToken{hash:h1, blockNumber:B}] and no `pending`, where the chain holds the same addPaymentToken effect at nonce planNonce+3 under hash h2 (the step's 25%-higher replacement) and `eth_getTransactionReceipt(h1)` is null (h1's block was reorged out). Reproducible on the test chain in deploy-mainnet-cli.test.mjs by taking a `saved` record from `stoppedRun`, replacing `steps.at(-1).hash` with any hash that has no receipt (the hash of a replacement that was never mined), and calling `deployMainnet({...input, resume:saved})`. Expected: the resume notices the step is done (its chain gate `paymentTokens(USDC)!==0n` answers true), keeps or repairs the record, sends approvePaidPrice and transferOwnership at planNonce+4 and +5 and finishes. Actual: `settled` polls `visibleTries` times and throws `Error('RECEIPT_NOT_VISIBLE '+h1)`; `deployMainnet` rejects before any checkpoint; the partial record is unchanged; every further resume fails the same way. Fix: record every hash of a step (the `hashes` array is already known at line 197) and let `settled` accept a receipt for any of them; when none has a receipt after the wait, do not throw: drop that entry from `steps[]` (it was the last one, so `floor` and the gates stay right), log it, and let `codeAbsent`/`notDone` decide from the chain, which is what the rest of the resume already trusts; and correct the README sentence or document the manual edit.","severity":"low","snippet":"  if(steps.length)await settled(steps.at(-1));","title":"deploy-mainnet resume: when the last recorded step's hash has no receipt (its own higher-fee replacement or a reorg took its place), every resume stops at RECEIPT_NOT_VISIBLE before any gate, so the r"}],"hash":"da258a0df603a32966ae55c413b29d435c3b780982385963625e3437047458e8","nodeId":"3d2a59b8-45b5-4006-a6f1-e06064285a26","outcome":"completed","summary":"Review of Submission12 is complete. Two findings are in `.imd-findings.json`, one medium and one low. No files other than the findings file were changed.\n\n**Medium: the default read RPC defeats the fix for audit finding 5.** The `safe-batch` tool now refuses `accept` and `open` unless it can count the PaymentTokenAdded logs from the deploy block. I probed the default RPC, publicnode, today. It does not limit a log range's width, which is what the README guards against with 10,000-block chunks. It refuses any `eth_getLogs` whose start is more than about 10,000 blocks behind its head, roughly 33 hours, and any pinned `eth_call` 128 or more blocks back, both with error -32602 \"Archive requests require a personal token\". Since the chunk walk always starts at the deploy block, every `open`, which by the owner's own plan runs days or weeks after the deploy, and any `accept` later than about 33 hours ends in LOGS_UNAVAILABLE on the default RPC. The only documented way on is the `--logs-checked-on-etherscan` flag, which turns the deployer-key token check off entirely. `status` prints a null count with no warning. The proposed fix reads the private `extraPaymentTokenCount` slot with `eth_getStorageAt` at the pinned head, which needs no history, or makes a log-serving READ_RPC_URL mandatory and removes the bypass for `open`.\n\n**Low: a resume can be blocked for good at RECEIPT_NOT_VISIBLE.** The resume first waits for the last recorded step's receipt. A step records only the first hash seen, but the tool itself puts up to eight fee-bumped replacements of a step into mempools. If the mined one is reorged out and a replacement lands instead, the step is done on chain but its recorded hash never gets a receipt. Every resume then polls three minutes, throws before any gate or checkpoint, and the PowerShell script sends the owner back to RESUME. The README says the read-back would report this case, but the run never reaches the read-back. Fix: record all hashes of a step, accept a receipt for any, and on none drop the entry and let the chain gates decide.\n\n**Checked and found sound**, with the reasoning traced against the code:\n- Resume order: the floor is only a lower bound and cannot exceed the real nonce except after a reorg. Dropping the seed happens only after a landed receipt or a chain nonce past it. A step enters `steps[]` only from a status-1 receipt of its own hashes.\n- REPLACEMENT_GAS: it applies only with a prior hash and a target, so the same nonce bounds damage to one transaction of at most one million gas. The five post-deploy steps stay far under the limit even with EIP-8037's roughly 98,000 gas per new storage slot.\n- The new stops are all recoverable by a repeated resume once reads are fresh. HEAD_BEHIND_RECORD needs only a current head.\n- Fees: refused bids raise the next bid, eight attempts cap at about 4.8 times market. The overpay is on the priority fee only, so it is small.\n- PowerShell: the key is set only around the key check and the deploy call, removed on each path, and the finally block runs on throw and Ctrl+C. The remaining process-memory residue is the accepted item.\n- Audit 358f7f40 items 1 to 4 and 6 to 8 are fixed as described. Item 5 is fixed in code but is inoperative on the default RPC, which is the medium finding.\n\n**Not verified**: the test suite cannot run here, because `runtime.mjs`, the vendored ethers build and the private artifacts are not in the package, so the behavioural claims rest on code reading plus the live RPC probes.\n\nSources for the EIP-8037 gas numbers used above: [EIP-8037](https://eips.ethereum.org/EIPS/eip-8037), [EIP-8038 draft](https://eips.ethereum.org/EIPS/eip-8038), [evm repricing cost analysis](https://awesome.ecosyste.ms/projects/github.com%2Fatiselsts%2Fevm-repricing-cost-analysis), [reth eip8037 module](https://reth.rs/docs/reth_revm/bytecode/primitives/eip8037/index.html).","treeHash":null,"usage":{"cachedInputTokens":1278579,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":48824,"runtime":"claude","turns":22,"wallClockMs":708174}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"89214b73ec1e0b7b","findings":[{"citation":"resolved","description":"Audit 358f7f40 finding 1 is answered by bounding the resume nonce from below with planNonce+steps.length and pending.nonce. That bound is only complete when every nonce the deployer spent is either a recorded step or the waiting transaction. Two paths in send() consume a nonce without leaving either: (a) a reverted setRevealCommit under revertRetries (line 194: `hashes=[];last=placed=null;continue;` after `nonce++`), after which the retry at the next nonce is the step that enters steps[]; (b) TX_FAILED (line 195) which writes `checkpoint(state())` with no pending. In both cases the record has fewer entries than nonces spent, floor is below the true next nonce, and the resume falls back entirely on stableNonce(), which is exactly the read the finding showed can be stale twice in a row. The README's statement that a reverted transaction's nonce is counted holds only for the waiting (seed) transaction. The consequence is bounded (NONCE_MOVED or a NONCE_EXPIRED after 8 attempts, never a second deploy or a skipped step) but it turns a resume that should progress into another stop that must be re-run until the RPC answers fresh, and if the node stays behind the run never progresses even though the record held enough information to.","line":151,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"State: planNonce P; first run: deploy at P, setRevealURIs at P+1, setRevealCommit attempt 1 at P+2 reverts (REVEAL_ACCEPTANCE_EXPIRED or EPOCH_REARM_*), revertRetries resends at P+3 and it lands; steps=[P,P+1,P+3] (length 3); process killed before addPaymentToken's first sendTransaction returns, so the record has no pending. Chain latest nonce is P+4. Resume with an RPC whose two consecutive eth_getTransactionCount answers are P+3 (the same lag the project's own test 'two stale nonce reads' scripts). Expected: floor=P+4, addPaymentToken sent at P+4. Actual: floor=P+3, nonce=max(P+3,P+3)=P+3, sendTransaction -> NONCE_EXPIRED -> hashes empty -> stableNonce()>P+3 -> 'NONCE_MOVED addPaymentToken(USDC, 1600 GOLD): the chain is past nonce P+3' (or, if the lag persists, 8 attempts then NONCE_EXPIRED thrown with pending {nonce:P+3,hashes:[]}). Fix: carry the run's own next nonce in every checkpoint (`state=extra=>({...,nextNonce:nonce,...})`) and use `floor=Math.max(planNonce+steps.length, resume.nextNonce??0)`; alternatively record reverted attempts in steps[] with status 0 so planNonce+steps.length stays exact and totalGas counts the burned gas.","severity":"low","snippet":"  let floor=planNonce+steps.length;","title":"Resume nonce floor does not count nonces burned by reverted attempts (setRevealCommit revertRetries, TX_FAILED), so a stale nonce read still sends at a spent nonce"},{"citation":"resolved","description":"To fix the 'refused sends kept the same fee' part of finding 1, `last` is now set before sendTransaction, so any failure of the send raises the next bid by 25%. That is right for REPLACEMENT_UNDERPRICED (a node holds a competing bid), but sendTransaction also fails for reasons that leave no bid anywhere: HTTP 5xx from the gateway, a timeout, SERVER_ERROR, INSUFFICIENT_FUNDS, or a NONCE_EXPIRED that the loop swallows while hashes is non-empty. On those paths `sent` stays false, the loop waits only pollMs*3 (6 s) and retries, so seven failures fit in under a minute and the eighth attempt bids 1.25^7 = 4.77x the node's suggested maxPriorityFeePerGas and maxFeePerGas. Both are then paid in full as the tip because nothing above market is in any mempool. The README asks whether a run can overpay; it can, and the only trigger needed is a gateway hiccup, not congestion. The recorded fee is `placed`, so the record is right, but the chain has already paid.","line":180,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"First run, deploy step (5.9M gas, suggested tip 2 gwei). RPC returns HTTP 502 (ethers SERVER_ERROR) for the first 7 eth_sendRawTransaction calls within ~45 s, then serves normally. Trace: attempt 1 fee={tip 2}, last=fee, send throws; attempt 2 fees(last) -> max(2, 2*1.25+1 wei) = 2.5 gwei; ... attempt 8 tip = 2*1.25^7 = 9.5 gwei, maxFee likewise 4.77x. Expected: a tip near 2 gwei, because no earlier bid exists. Actual: the deploy pays ~9.5 gwei tip, about 0.044 ETH more than needed on 5.9M gas (more at higher base tips; the escalation is uncapped except by the 8 attempts). The project's test 'finding 1: a failed resume keeps what the record knows' already drives sendTransaction with SERVER_ERROR and asserts only that the record is unchanged; adding `assert.ok(srv.state.sent[0].maxPriorityFeePerGas <= suggested*125n/100n)` after such a failure burst fails on this code. Fix: bump only when a bid may exist: set `last=fee` after a successful send (where `placed=fee` is set) and in the catch when `e.code==='REPLACEMENT_UNDERPRICED'` (or the message matches 'underpriced'/'already known'); for other errors keep `last` as it was.","severity":"low","snippet":"    last=fee;   // a refused bid counts too: the next attempt must beat it, or it would be refused the same way","title":"Fee is raised 25% on every failed attempt even when nothing was bid: 7 transient RPC errors make the deploy pay ~4.8x the suggested priority fee"},{"citation":"resolved","description":"Finding 5 made a refused log range stop `accept` and `open`. The implementation maps every error from tokenLogs to null, including transient ones (gateway timeout, 'header not found' from a node behind the pinned head, a 429 burst after the 20 parallel eth_call reads that precede it), and checkRecord then throws LOGS_UNAVAILABLE, which holds() rethrows immediately (line 154) instead of treating it like every other refusal that may be a lagging node. steady() retries 5 times 1 s apart per chunk, so one chunk failing for 5 seconds is enough; for `open` weeks after deployment there are 20+ chunks per pass and 3 passes. The error text tells the owner to count the logs on Etherscan and pass --logs-checked-on-etherscan, a flag that disables the extraTokens check entirely and that the tool cannot verify (README section 5). A transient fault therefore produces a false refusal whose suggested remedy removes the check, while the stable, intended case (an RPC whose range limit is below 10,000 blocks) is indistinguishable from it.","line":107,"path":"Submission12/tools/safe-batch.mjs","reproduction":"Record d with startBlock S, chain head H = S + 150,000 (about three weeks later), `node safe-batch.mjs accept <record>` against an RPC that answers 20 eth_call reads at blockTag H correctly but times out on one of the 15 eth_getLogs chunks for 5 consecutive seconds (unit form: `provider.getLogs=async()=>{throw Error('timeout')}` for one chunk, as the project's own test does for all). Expected: holds() waits refusedMs and reads again, as it does for every other failing read; a structural range refusal (JSON-RPC -32005 / 'query returned more than' / 'block range') ends in LOGS_UNAVAILABLE. Actual: chainState returns extraTokens:null on the first pass, checkRecord throws LOGS_UNAVAILABLE, holds() rethrows without a retry, exit 1 with a message that points at the bypass flag. Fix: in tokenLogs/chainState catch only the RPC's explicit range or result-limit errors (match the code/message) into null and let other errors propagate so holds() retries them; keep the LOGS_UNAVAILABLE short-circuit for the structural case only, and have holds() require the null to repeat over `passes` reads before accepting it.","severity":"low","snippet":" const from=d.startBlock??d.deployBlock,extraTokens=from==null?null:await tokenLogs(g,Number(from),head).catch(()=>null);","title":"safe-batch turns any eth_getLogs failure into LOGS_UNAVAILABLE, which holds() rethrows at once and whose message steers the owner to the --logs-checked-on-etherscan bypass"},{"citation":"resolved","description":"The fallback is sound in its invariant: it is used only when hashes is non-empty, so a transaction of the same step at the same nonce already exists and at most one of the two can land, and no step but the deploy can be sent on it. It is sufficient today (largest step 169,104 gas, so 5.9x headroom). It is, however, an untethered constant: the preflight comment at line 333 already anticipates a repricing (~6.3M -> ~40M for the six transactions 'if EIP-8037 is live'), under which a storage-heavy step such as setRevealURIs could exceed 1,000,000 and the replacement would land out of gas with status 0, burning the whole limit and ending the run with TX_FAILED (the resume then counts the reverted nonce and resends, so the loss is one failed transaction). The first send of every step always came from a successful estimate (hashes empty, no fallback), so that limit is known and is the natural value to reuse.","line":179,"path":"Submission12/tools/deploy-mainnet.mjs","reproduction":"Step setRevealURIs (not locked) with a pending hash h1 sent on an estimated limit of 1.2*G, and a node that estimates on top of the pending transaction so the estimate for the replacement fails. If G > 1,000,000 (not the case under today's pricing; the case the preflight comment itself allows for), attempt 2 sends h2 with gasLimit 1,000,000 at a 25% higher fee; h2 is mined first, runs out of gas, status 0, 1,000,000 gas paid, TX_FAILED stop. Expected: the replacement reuses the limit h1 was sent with (or a limit derived from it), which is known to suffice. Fix: keep the last successful `limit` in the step scope (and in the pending checkpoint as `gasLimit`, so a resume can reuse it) and fall back to that, with REPLACEMENT_GAS only as a last resort when no earlier limit is known.","severity":"info","snippet":"    const req=await request(),limit=await gasLimit(req).catch(e=>{if(hashes.length&&req.to)return REPLACEMENT_GAS;throw e;});","title":"REPLACEMENT_GAS is a fixed 1,000,000 instead of the gas limit already carried by the pending transaction it replaces"}],"hash":"f1d5f69eeacb7d7a7de483c3cf96bd89c2ca8aedddd3bf471f2cd7823b53eaf2","nodeId":"c3d4229c-a885-4653-a90b-94071626135f","outcome":"completed","summary":"The review of Submission12 is complete. The findings file holds 4 findings (3 Low, 1 Info), each with its path, line and snippet verified against the tree. No critical or high finding, so no Foundry proof was needed. No files other than the findings file were changed.\n\n**What I found**\n\n- **Low, resume nonce floor is incomplete** (`tools/deploy-mainnet.mjs:151`). The floor counts recorded steps and the waiting transaction, but not nonces burned by a reverted `setRevealCommit` retry or a `TX_FAILED` step. With the same two stale nonce reads the audit's finding 1 modelled, the resume sends at a spent nonce and stops with NONCE_MOVED instead of progressing. Bounded, never a second deploy. Fix: checkpoint the run's own next nonce and use it in the floor.\n- **Low, fee bump on non-price failures** (`tools/deploy-mainnet.mjs:180`). `last=fee` is set before the send, so seven transient gateway errors in under a minute make the eighth attempt bid about 4.8x the suggested tip, which is then paid in full. Fix: bump only after an accepted send or a REPLACEMENT_UNDERPRICED error.\n- **Low, transient log failure becomes LOGS_UNAVAILABLE** (`tools/safe-batch.mjs:107`). Any `eth_getLogs` error maps to null, `holds()` rethrows immediately, and the message steers the owner to the unverifiable bypass flag. Fix: map only explicit range-limit errors to null and let other errors retry.\n- **Info, REPLACEMENT_GAS is an untethered constant** (`tools/deploy-mainnet.mjs:179`). Safe today with 5.9x headroom, but the pending transaction's own successful estimate is the natural fallback.\n\n**What checked out clean**\n\n- The same-nonce invariant behind the replacement fallback holds: at most one of the two lands, and the deploy never uses it.\n- NONCE_MOVED, RESUME_OUT_OF_ORDER and HEAD_BEHIND_RECORD cannot block a valid run for good. Each recurs only while the RPC stays stale, and a fresh read clears it.\n- The seed is dropped only when its nonce is provably spent or its receipt was settled.\n- PowerShell secret scoping is correct on every throw and Ctrl+C path. The in-memory residue is already accepted.\n- Every item in audit 358f7f40 is addressed in code as the README describes.\n- The reorg false-mismatch on recorded block numbers is real but was already accepted in Submission 11 section 6, so it is not re-reported.\n\n**Limits**\n\n- The test suite could not be run here. The package omits `runtime.mjs` and the private artifacts, so reproductions are by code trace.\n- Real load-balanced RPC behaviour, including the public node's `eth_getLogs` limit, was not exercised, matching the README's own section 7.","treeHash":null,"usage":{"cachedInputTokens":1299877,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":62492,"runtime":"claude","turns":19,"wallClockMs":1008207}}],"verification":[]}