{"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":"b0b1d96a-bd57-44c7-a43c-8b7d123b9bb2","kind":"audit","nodes":[{"acceptedSubmissionHash":"962ff48cd42e3de5eb68e1f6bd8d04514c41dd13b364687308b32248ee6bf8f9","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":"b1a24c492c2a8f0b1069a07fdb523817e309ec7269b75270c056052b45ec037f","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":"c9d3d60fec6865b8cc80c61792e100d10e4b2eee3bca42db5c908acf498d9658","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":"51d8d40eebf50ce3abb0d84ee34c664c1ef5a2a442d30005c831c76092bbff30","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":"40f4002adec7c61261bd8685456eb7b11351004d9d010e0c43188de32d1952fc","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 Submission4/ only. Read Submission4/AUDIT_README.md, then Submission4/FINDINGS_RESPONSE.md (how each finding of the third audit, job d726b1cd, was handled, plus the owner changes and a gas review). Primary scope: Submission4/contracts/EmberEvoGenesisCandidate.sol (verified on Sepolia 0xD5f3A0cdC26c3d19B7E657586f2eA2483A7Bf8Af, solc 0.8.38). Not in scope: contracts/test, contracts/testnet, OpenZeppelin.\n\nCheck the fixes: an expiry re-arm moves only its own epoch, while a later epoch keeps the last commitment re-arm block (new revealState layout, bits 64-215); MAX_COMMIT_REARMS counted from the last two commitment blocks; the one-hour RevealCommitAcceptance window (one unchecked subtraction); readyReveals reads pinned to one block.\nNew since Submission 3: payment goes to an immutable paymentRecipient (the treasury Safe) instead of a burn address. mintFree takes the wallet's free-list index and claims are one bit per index (leaf = keccak256(keccak256(abi.encode(index, wallet)))): can any wallet claim twice, use another wallet's slot, or block it? mintPaid verifies only a PaidAuthorization the contract rebuilds from the caller and its own recipient, prices and signer epoch (no Appearance signature, no order struct, no totalSupply check): is any binding lost compared with Submission 3? ERC-2981 royaltyInfo: 2.5% to the treasury, fixed.\nSecondary: the signer (free-approval snapshotTree and indexOf, paid-approval) and the GOLD book changes (registry recipient, domain version 7 accepted in world-gold-result.ts). Report severity, reproduction and fix.","parentJobId":null,"planHash":"bae2bc11396ddb15e3755fb3991909da3aff99d770622ce1c5b259efe40e43b3","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"b0b1d96a-bd57-44c7-a43c-8b7d123b9bb2","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":"52624","feedbackHash":"88ef95c175d452b823e74503c640b4209d9fef0ef175f021b946d5239fe69285","nodeKey":"audit_economics","submissionHash":"962ff48cd42e3de5eb68e1f6bd8d04514c41dd13b364687308b32248ee6bf8f9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52626","feedbackHash":"bc592a8334d317e5ade60f16f5d819f9ac507065f04280782548944f5e2d8fac","nodeKey":"audit_flow","submissionHash":"b1a24c492c2a8f0b1069a07fdb523817e309ec7269b75270c056052b45ec037f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51199","feedbackHash":"de43c75bcfa35511fa2b74b92e564a18b000e9d1cc470b60370d7bd8889e5680","nodeKey":"audit_judge","submissionHash":"c9d3d60fec6865b8cc80c61792e100d10e4b2eee3bca42db5c908acf498d9658","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52619","feedbackHash":"53ceabf4da5c874df9152fbda882e570b5e061d20067d94b986d0ca2178ec011","nodeKey":"audit_math","submissionHash":"51d8d40eebf50ce3abb0d84ee34c664c1ef5a2a442d30005c831c76092bbff30","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52623","feedbackHash":"8bece0ff7bb4e0101c10dd0339c3a5d0b387b36d29ea22c220418ba9401f925d","nodeKey":"audit_permissions","submissionHash":"40f4002adec7c61261bd8685456eb7b11351004d9d010e0c43188de32d1952fc","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"1a4cc78551729114c6d258dbba05081bcde45bc619f11c1710ea2279879cdea5","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"a800aae85243bc87","findings":[{"citation":"resolved","description":"The Submission 4 fix makes an expiry re-arm move only its own epoch (epochSeedBlock line 445 applies the expiry block only when uint32(rs >> 184) == e). That is implemented correctly, but it does not deliver the property the package claims for it (README 5 'Expiry', README 8.2 'Signer alone: withholding': 'later epochs keep their own seed blocks and their own secrets, and each needs its own 27-hour withhold for its own one fresh draw'; FINDINGS_RESPONSE finding 2: 'a withheld secret buys one fresh draw for that epoch alone'). A later epoch keeps a seed block that is the same as, or only 25 blocks newer than, the one that just expired, and it cannot be settled before the expired epoch (strict order, and only the signer has its secret). So by the time the epoch ahead is sealed, the later epoch's own seed block has expired as well (immediately when it shares the last commitment re-arm block, after about 25 more blocks per epoch otherwise). The next secret supplied for it hits h == 0 at line 623 and re-arms it too. Its original outcome, which the signer has known since that seed block was mined, is thrown away without a second withhold. The per-epoch bound (at most one expiry draw while the secret is private, because the secret is public afterwards) still holds. What is wrong is the cost the README states. The withholds run in parallel: one stall of about 8,191 + 25*k blocks lets the signer, after seeing every outcome, re-draw the stalled epoch and the k epochs behind it. When those epochs share a commitment re-arm block it costs no extra blocks at all. This is the backlog effect finding 2 of audit d726b1cd described, reached through each epoch's own expiry instead of through a shared re-arm. It is a signer-trust (Trust Gap) issue only. No minter or owner gains a lever.","line":623,"path":"Submission4/contracts/EmberEvoGenesisCandidate.sol","reproduction":"State: random epochs A and B, both closed and unsealed. A setRevealCommit at block c re-armed both (A was next, B had closed), so C1 = c+1 and epochSeedBlock(A) == epochSeedBlock(B) == c+1. The signer withholds A's secret sA. 1) At block c+1+8192 anyone calls settle([sA]). _blockHash(c+1) == 0, so line 623 calls _rearm(A, true): X = (c+8194, A). epochSeedBlock(B) is still c+1 (line 445 skips B). 2) At block c+8195 the keeper calls settle([]). A seals with the hash of block c+8194. 3) In the same or the next block the signer publishes sB at once and the keeper calls settle([sB]). Expected per README 8.2: B seals with the hash of block c+1, the outcome fixed since c+1, because the signer did not withhold B. Actual: _blockHash(c+1) == 0 (more than 8,191 blocks old), so line 623 runs _rearm(B, true). The log shows EpochRearmed(B, block+1, true), epochSecretOpened(B) == true, and B is drawn again from a new block. The variant without a commitment re-arm: A and B consecutive with natural seed blocks n and n+25. The signer holds sA until block n+8192+25, settles A as above, and B's natural seed block has then expired too, with the same result. Fix (no design change possible on chain, since the old hash is gone): restate README 5 / 8.2 and FINDINGS_RESPONSE finding 2. One withhold of the next epoch forces a re-draw of every later closed epoch whose seed block expires before it can be settled (all epochs that share a commitment re-arm block, and roughly every epoch that closed within the withholding period). The keeper alert should treat a ready epoch with no published secret as affecting the whole backlog behind it. If that bound is not acceptable for the 48 gold skins, the VRF option in 8.2 is the alternative. Confirmed with a scratch Foundry test built outside the repository from build/standard-input.json. Setup: TestUSD, a random paid mint in epoch 0 and another in epoch 1, then setRevealCommit after the cooldown. Both epochs then had seed block c1. With no EIP-2935 code the window is 256 blocks. settle([u1]) at c1+300 re-armed only epoch 0, and settle([]) two blocks later sealed it. settle([u2]) right after that re-armed epoch 1 instead of sealing it: nextRevealIndex stayed 1, and the test's final require failed.","severity":"low","snippet":"            if (h == bytes32(0)) { _rearm(e, true); return progressed; }","title":"An expiry still re-draws later epochs that share (or nearly share) the expired seed block: one withheld secret buys fresh draws for a whole backlog, contrary to README 8.2 / FINDINGS_RESPONSE finding "},{"citation":"resolved","description":"The Submission 4 fix to encodeMintAuth replaces the hard-coded version '5' with a set {'5','7'} that does not depend on the deployment. Its comment (line 66) says 'Domain version 5, or the registry's domainVersion 7'. The recipient check (line 73) accepts any lowercase non-zero address (comment: 'the burn address, or the registry's treasury'). encodeMintAuth only receives the binding (chainInstance, contract, chainId), not the registry entry, so it cannot check either value against the registry. Today parseOrder/genesisAuthDocument in genesis-gold.ts build the document from the registry (domain version from d.domainVersion, recipient required to equal d.recipient ?? GENESIS_DEAD), so no wrong hold is stored through reserveGenesisGold. The stored-document validator is weaker than documented, and any other writer of a genesis-r5-paid-v1 document gets no domain-version or recipient check from it.","line":70,"path":"Submission4/offchain/gold-book/server/world-gold-result.ts","reproduction":"Call encodeMintAuth with binding {chainInstance:'sepolia-r6-rehearsal-2', chainId:11155111, contract:'0xdd4ae794483b81e21e02a6561e7c0a64b1c92026', ...} (a deployment with no domainVersion, so EIP-712 version 5) and a genesis-r5-paid-v1 document whose domain.version is '7' and whose order.recipient is '0x29e7c208481f648795f677b55f6ecb2badc2f01e' (that deployment burns to 0x…dead). Expected per the comment: INVALID_MINT_AUTH. Actual: the document is accepted and returned encoded. Fix: pass the registry entry (or its domainVersion and recipient) into the binding and require d.version === (entry.domainVersion ?? '5') and o.recipient === (entry.recipient ?? GENESIS_DEAD). Otherwise, correct the comment to say that only parseOrder enforces them.","severity":"info","snippet":"   if(!record(o)||!record(d)||!record(c)||d.name!=='EmberEvoGenesisCandidate'||!['5','7'].includes(d.version as string)||d.chainId!==binding.chainId||!sameAddress(d.verifyingContract,binding.contract)","title":"GOLD book accepts domain version 7 (and any non-zero recipient) for every Genesis deployment, not 'the registry's domainVersion' as its comment states"}],"hash":"40f4002adec7c61261bd8685456eb7b11351004d9d010e0c43188de32d1952fc","nodeId":"34c02be8-d13b-4bf0-998a-006274b13e00","outcome":"completed","summary":"The fixes and new code in `Submission4/contracts/EmberEvoGenesisCandidate.sol` hold up under this review; I found no Critical, High or Medium issue. `.imd-findings.json` has two findings: one Low, confirmed by a test, and one Info. No repository files were changed.\n\n**Findings**\n\n1. **Low: one withheld secret still forces re-draws of the epochs behind it** (`EmberEvoGenesisCandidate.sol:623`).\n   - The fix itself is right: an expiry re-arm moves only its own epoch.\n   - But a later epoch keeps a seed block that is the same one, or only about 25 blocks newer, and it cannot be settled first. So by the time the epoch ahead is sealed, the later epoch's block has usually expired too, and it gets re-armed and drawn again.\n   - The failing state: a new commitment re-armed epochs A and B to the same block. The signer withholds A's secret until that block is too old to read. Then A expires and is sealed, and `settle([sB])` re-arms B instead of sealing it, with no withholding of B at all.\n   - So README 8.2 and the answer to finding 2 overstate the cost. Each epoch still gets at most one re-draw while its secret is private, but one stall of about 27 hours re-draws the whole backlog, not just one epoch.\n   - The old block hash is gone, so no on-chain fix exists. The fix is to restate the bound in the docs and widen the keeper alert to the whole backlog.\n   - I confirmed it with a Foundry test built outside the repository from `build/standard-input.json`. Without the EIP-2935 contract the hash window there is 256 blocks rather than 8,191, which does not change the logic. The test fails at exactly the step described. I did not attach it as a proof, since proofs are only required for Critical and High.\n\n2. **Info: the GOLD book's stored-document check is looser than its comment** (`world-gold-result.ts:70`). It accepts domain version 5 or 7 and any non-zero recipient for every deployment, while the comment says it uses the registry's values. Today `parseOrder` enforces the registry's values when a hold is placed, so no wrong hold gets stored. The fix is to pass the registry entry into the check, or correct the comment.\n\n**What I checked and found correct**\n\n- **Expiry and commitment re-arm bookkeeping:**\n  - Only epoch e takes the expiry block.\n  - The last commitment block applies only to epochs that had closed when it was set.\n  - A new commitment clears the expiry record whenever that record could still matter.\n  - `MAX_COMMIT_REARMS`: the commitments that moved an epoch are always the most recent ones, so the last two blocks count it exactly, and an epoch behind can never have a higher count than the one ahead.\n- **One-hour acceptance window:** the unchecked subtraction makes a past deadline wrap around and fail.\n- **`readyReveals`:** every read is pinned to one block, and the expiry-opened epoch is skipped correctly.\n- **Free mints:**\n  - No wallet can claim twice, provided the off-chain list has no duplicates (`snapshotTree` removes them; README 7 already notes the contract cannot check this).\n  - No wallet can use or block another wallet's slot, because the leaf binds index and wallet and is hashed twice, and the free signature binds the caller.\n  - The off-chain tree (sorted pairs, odd node promoted) matches OpenZeppelin's `MerkleProof`.\n- **Paid mints:** compared with Submission 3, no binding is lost. The rebuilt authorization covers everything the old signed paid approval (`Appearance`) and order struct did, and the old caller-supplied recipient and cost checks are meaningless now that both are fixed at deployment. The exact balance checks still reject tokens that take a fee or burn on transfer.\n- **Royalty:** fixed at 2.5% to the immutable recipient, with no setter.\n- **GOLD book reader:** settlement still matches the new contract's events, and order holds check the recipient against the registry.\n- **Permissions:** owner and signer powers match README section 4. The owner rotating the signer to a key it holds is already disclose","treeHash":null,"usage":{"cachedInputTokens":1458750,"inputTokens":30,"model":"claude-opus-5-5","outputTokens":36337,"runtime":"claude","turns":17,"wallClockMs":419343}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"523b06a3dd921f5d","findings":[{"citation":"resolved","description":"Boundary: setRevealCommit's deadline check (audit d726b1cd finding 4 fix). Assumption (comment at lines 286-288, README 5 'Randomness' and 8.2 'Owner alone'): 'an acceptance lives at most an hour ... so a spare one cannot wait for an epoch to become re-armable'. Actual: the check bounds deadline - block.timestamp at the time of submission. The contract never learns when the acceptance was signed, so any RevealCommitAcceptance whose signed deadline D is more than an hour after signing is not dead. It is accepted throughout [D - 3600, D]. The arithmetic is correct: the unchecked wrap rejects past deadlines, and deadline == now and deadline == now + 3600 are both accepted. What it enforces is a usable window just before D, not a lifetime. The project's test ('deadlines 30 days and 3,700 s ahead refused') checks only submission at signing time, so it passes while the property it claims fails. The only actual protection is that the off-chain signer signs latest-block-time + 3600 (tools/deploy-testnet.mjs:43). Nothing on chain enforces this. A spare acceptance with D = signing time + 1 day + 1 hour stays usable after REARM_COOLDOWN (7,200 blocks, about 24 h) as long as revealCommit still equals its `previous` (the first epoch's secret has not been opened, for example because it is withheld). That is the exact case finding 4 was meant to close. Impact: an owner holding such an acceptance replaces the chain later without the signer's fresh consent. Low, because it requires the signer to have signed a deadline more than an hour ahead.","line":289,"path":"Submission4/contracts/EmberEvoGenesisCandidate.sol","reproduction":"Deploy (owner O, signer S). At t0 the signer signs RevealCommitAcceptance(commit=c, previous=0, signerEpoch=1, deadline=t0+30 days). setRevealCommit(c, t0+30d, acc) at t0 reverts REVEAL_ACCEPTANCE_EXPIRED, as the project's test expects. Warp to t0+30d-3600 and O alone calls setRevealCommit(c, t0+30d, acc). Expected per the fix: refused, since the acceptance is 29 days 23 hours old. Actual: succeeds and revealCommit == c. Verified with a Foundry test (OZ 5.4.0 from build/standard-input.json, solc 0.8.38 via-IR): test_longDeadlineAcceptanceUsableInLastHour passes. Fix: bind the acceptance to something the contract can check for freshness instead of a signer-chosen deadline. For example, add `uint256 signedAtBlock` to RevealCommitAcceptance and require block.number - signedAtBlock <= 300 (and signedAtBlock <= block.number). Alternatively bind it to the reveal state it was signed for (nextRevealIndex, randomEpochCount, epochSeedBlock of the next epoch), so it goes stale as soon as the state moves. Or, at minimum, document that the guarantee depends on the signer never signing deadline > now + 1h and test the signer for it.","severity":"low","snippet":"        unchecked { require(deadline - block.timestamp <= ACCEPTANCE_WINDOW, \"REVEAL_ACCEPTANCE_EXPIRED\"); }","title":"ACCEPTANCE_WINDOW bounds deadline minus submission time, not the acceptance's lifetime: an acceptance with a long deadline becomes usable by the owner alone in its last hour"}],"hash":"51d8d40eebf50ce3abb0d84ee34c664c1ef5a2a442d30005c831c76092bbff30","nodeId":"bafb92a9-4176-43bc-b0ff-2edccc264a2e","outcome":"completed","summary":"The math review of `Submission4/contracts/EmberEvoGenesisCandidate.sol` turned up one low-severity finding. The re-arm fixes, free claims, paid route and royalty all hold up. The finding is in `.imd-findings.json`, and its file, line and snippet match the source.\n\n**Finding (low): the one-hour acceptance window doesn't limit how long an acceptance stays usable** (`EmberEvoGenesisCandidate.sol:289`)\n- The check bounds `deadline - block.timestamp` at the moment of submission. The contract never learns when the acceptance was signed.\n- So a `RevealCommitAcceptance` signed with a far deadline D is refused today but accepted throughout the last hour before D, by the owner alone.\n- **Reproduction:** an acceptance signed at t0 with deadline t0 + 30 days is refused at t0, as the project's own test expects. The owner alone uses it successfully at t0 + 30 days − 1 hour.\n- This reopens finding 4's case (a spare acceptance used once the cooldown has passed) whenever the signer signs a deadline more than an hour ahead. Today only the off-chain tool keeps deadlines at one hour (`deploy-testnet.mjs:43`, latest block time + 3600).\n- **Fix:** bind the acceptance to something the contract can check for freshness, such as a signed block number within a few hundred blocks, or the current reveal state. At minimum, document that the guarantee depends on how the signer sets deadlines.\n\n**The fixes you asked me to check hold:**\n- **Re-arm bookkeeping.** All `revealState` packing and masks are correct: bits 0–63 unchanged, a commitment shifts the last block into bits 104–143 and clears the expiry fields, an expiry keeps bits 0–143. I checked the boundaries where a commitment lands in the block an epoch closes, one block before it, or the same block as an expiry. Commitment blocks are counted only for epochs that had closed and were therefore moved. I also traced why the 40-bit and 32-bit fields can't overflow in practice.\n- **Scenario test.** With a scratch Foundry setup outside the repo (OZ 5.4.0 taken from `build/standard-input.json`, solc 0.8.38 via-IR, Prague), I ran: a commitment re-arm moves epochs 0 and 1; epoch 0 expires and only it moves, while epoch 1 keeps the commitment block; a second commitment is allowed (count 1); a third reverts with `EPOCH_REARM_LIMIT`; both epochs then settle. Everything behaved as the README describes.\n- **The one unchecked subtraction.** A deadline in the past wraps around and is refused. Deadlines of exactly now and now + 3600 are accepted.\n- **`readyReveals`.** Every read is pinned to the same `latest` block, and stale expiry fields can't mark a later epoch as already opened.\n- **Free claims by index.** The leaf ties each index to its wallet, so no wallet can use or block another wallet's slot. A wallet could claim twice only if it were listed twice; `snapshotTree` removes duplicates and the deploy tool passes that tree's count. The off-chain encoding and the \"odd node promoted\" tree match OZ's `MerkleProof`.\n- **`mintPaid`.** Nothing is lost compared with Submission 3. The rebuilt `PaidAuthorization` covers every field the old paid-route `Appearance` covered, plus order, recipient and costs. The separate type hash keeps the free and paid routes apart. The exact balance checks still reject fee-on-transfer and burn-on-transfer tokens, and a caller who is the recipient.\n- **Royalty and skins.** `royaltyInfo` is 2.5%, rounded down; the interface id 0x2a55205a is correct. The skin draw (remaining capacity modulo the total) and the epoch token packing (15 + 10 ids, 16 bits each) are sound.\n\n**Secondary scope:** the paid-approval signer reads the recipient from the contract. The GOLD book accepts domain versions 5 and 7, and checks the order's recipient against the registry's recipient, or the burn address when none is set.\n\n**Coverage limits:** I did the Math Precision, Boundary and Numerical Gap passes myself; no helpers ran. I read the off-chain code only where it touches the contract, and didn't run the project's ","treeHash":null,"usage":{"cachedInputTokens":2360804,"inputTokens":52,"model":"claude-opus-5-5","outputTokens":37311,"runtime":"claude","turns":27,"wallClockMs":404414}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"af4be7a5238dbcbf","findings":[{"citation":"resolved","description":"Since Submission 4, an expiry re-arm moves only the expired epoch (_rearm(e, true) writes bits 144-215 for e alone). Later epochs keep their natural seed block (or the last commitment block). README 8.2 ('Signer alone: withholding') and FINDINGS_RESPONSE finding 2 say a later epoch 'needs its own 27-hour withhold for its own one fresh draw'. That does not hold, because the withholds overlap. While the signer withholds epoch e's secret, random mints go on (_checkMint has no pending-reveal check on the random path). So up to ~8,191/25 later epochs fill and close, and the signer learns each outcome as soon as its natural seed block is mined: it holds every later secret, and the block hash is public. The keeper cannot settle any of them, because publishing a later secret would also publish e's (x[i-1] = keccak(x[i])). When e finally expires and is sealed at its re-armed block, the next epoch e2 is still on natural(e2) = natural(e) + 25k, a block whose hash the signer has known for ~27 h. For e2 and every epoch after it, the signer now chooses: publish now and keep the outcome it already knows, or wait until that seed block also leaves the window, which takes about 25 blocks per epoch, not another 8,191, and get a fresh draw. That is a best-of-two per epoch, chosen with the outcome in view, for the price of one visible ~27 h freeze. For a rare skin with probability p (gold: 48/1688), the signer's own random mints in the backlog reach about 1-(1-p)^2, roughly 2p. Under Submission 3 the epochs behind were re-drawn whether the signer liked their outcome or not. The new layout makes that re-draw optional per epoch, which is a stronger lever than the one finding 2 described, and the README bound understates it. Severity is low because it needs the signer (a documented trust party) to withhold, and the withhold is visible. Fix (needs an owner decision, since it is the trust model): either (a) when an expiry re-arm happens, also move every later unsealed epoch whose natural seed block is already mined to the same new block, at most once per epoch. Keep the first such block per backlog and do not move again an epoch that one expiry already moved, so draws do not compound as in Submission 3. Then a withhold forces one fresh draw on the backlog instead of offering a per-epoch choice. Or (b) keep the code and correct README 8.2 and FINDINGS_RESPONSE: one withhold of about 27 h gives the signer a keep-or-redraw choice for every random epoch that closed during it, at about 25 blocks of extra delay per epoch. The keeper should also alert when random mints continue while a ready epoch has no published secret. Pausing random mints while a ready epoch has been waiting more than N blocks would also cap the backlog exposed.","line":623,"path":"Submission4/contracts/EmberEvoGenesisCandidate.sol","reproduction":"Ran in Foundry (forge 1.8.3, solc 0.8.38 viaIR cancun, OZ sources from Submission4/build/standard-input.json). This test chain has no EIP-2935 code, so the hash window is 256 blocks instead of 8,191; the logic is the same. Setup: startBlock S. Wallet A mints random at S+1 (epoch 0, seed block S+25). Wallet B mints random at S+26 (epoch 1, seed block S+50). Chain x0=keccak(x1), x1=keccak(x2). The signer withholds x1. At block S+282: settle([x1]) -> epochSecretOpened(0)=true, epoch 0 re-armed to S+283. At S+284: settle([]) seals epoch 0. Now nextRevealEpoch()=1 and epochSeedBlock(1)=S+50, a hash the signer has known since S+51. Option A: settle([x2]) at S+284 seals epoch 1 with hash(S+50) (nextRevealIndex=2), the outcome the signer already knew. Option B, from the same state: roll to S+307 (23 blocks later), settle([x2]) -> epochSecretOpened(1)=true, epochSeedBlock(1)=S+308, a fresh draw. Expected (README 8.2): a second epoch's fresh draw needs its own full-window withhold. Actual: 23 extra blocks, at the signer's option, after seeing the outcome. On mainnet the extra delay is 25*(k-1) blocks for the k-th epoch behind, against 8,191. (The scenario was run as a Foundry test against the unmodified contract: settle([x1]) at S+282, settle([]) at S+284, then from the same snapshot either settle([x2]) at S+284 (sealed on S+50) or settle([x2]) at S+307 (expiry re-arm to S+308). Both pass, which shows the signer holds both options.)","severity":"low","snippet":"            if (h == bytes32(0)) { _rearm(e, true); return progressed; }","title":"One withheld secret gives the signer a keep-or-redraw choice on every epoch that closes during the withhold, not one fresh draw for one epoch"}],"hash":"962ff48cd42e3de5eb68e1f6bd8d04514c41dd13b364687308b32248ee6bf8f9","nodeId":"d06ee1ac-173a-4f99-9654-0bb90ce291b6","outcome":"completed","summary":"I found one low-severity issue; the fixes and new code you asked me to check hold up. The finding is in `.imd-findings.json`, which parses, and its quoted line matches the file. I covered the economics passes only (Economic Security, Invariant, Flow Gap). No helpers were used, and Slither was not run.\n\n## The finding (low): one withheld secret gives the signer a keep-or-redraw choice on many epochs\n\n**Where:** `Submission4/contracts/EmberEvoGenesisCandidate.sol:623`, `if (h == bytes32(0)) { _rearm(e, true); return progressed; }`\n\n**What's wrong:** README 8.2 and FINDINGS_RESPONSE (finding 2) say each later epoch \"needs its own 27-hour withhold for its own one fresh draw\". It doesn't, because the withholds overlap.\n\n- While the signer holds back epoch e's secret, random mints continue. Later epochs fill and close, and the signer can work out each outcome as soon as its seed block is mined.\n- No one else can settle them: publishing a later secret would also reveal e's.\n- Once e expires and is sealed, the next epoch is still on its original seed block. The signer can publish now and keep an outcome it already knows. Or it can wait about 25 blocks per epoch (not another 8,191) for that block to expire too, and get a fresh draw.\n\nThat is a best-of-two per epoch, chosen after seeing the outcome, for the cost of one visible ~27 h freeze. For a gold skin, that roughly doubles the chance of the signer's own random mints (from about p to 1−(1−p)²). Submission 3 re-drew the epochs behind whether the signer liked them or not. Now the re-draw is optional per epoch, which is a stronger lever, and the documented limit understates it. It stays low because it needs the signer (a documented trust party) and the stall is visible.\n\n**Reproduction:** I ran this as a Foundry test against the unmodified contract. The test chain has a 256-block hash window instead of 8,191, which changes only the numbers.\n- Setup: one random mint in epoch 0 and one in epoch 1.\n- At S+282, `settle([x1])` triggers epoch 0's expiry; at S+284, `settle([])` seals it.\n- From the same state, `settle([x2])` at S+284 seals epoch 1 on S+50, the outcome the signer already knew.\n- Alternatively, `settle([x2])` at S+307 (23 blocks later) re-arms epoch 1 for a fresh draw.\n\nBoth paths pass, which shows the signer holds both options. I didn't attach the test as a proof, because it passes on the current code; the steps are in the reproduction field.\n\n**Fix (needs your decision, since it is the trust model):**\n- **Code:** when an expiry happens, move every unsealed epoch that has already closed to the same new block, at most once per epoch. That forces one re-draw on the whole backlog instead of offering a choice, without the compounding of Submission 3.\n- **Docs and keeper only:** correct the README and add a keeper alert for random mints continuing while a ready epoch has no published secret.\n\n## Checked and found sound\n- **Re-arm layout (bits 64–215):** an expiry re-arm moves only its own epoch, and the last commitment block stays in force for later epochs. A commitment block can never apply to an epoch added after it.\n- **`MAX_COMMIT_REARMS`:** counted correctly from the last two commitment blocks; expiries never count.\n- **One-hour acceptance window:** the unchecked subtraction accepts deadlines from now to now + 3600 and rejects past deadlines.\n- **`readyReveals`:** every read is pinned to one block.\n- **Free claims:** the leaf binds index to wallet and is double-hashed, so no wallet can claim twice, use another wallet's slot, or block it. The signer's `snapshotTree` removes duplicates and its tree matches OpenZeppelin's proof check.\n- **Paid route:** compared with Submission 3, no binding is lost. The old `Appearance` fields are a subset of `PaidAuthorization`, and the dropped `expectedRecipient`/`expectedCost` guarded values that are now fixed at deployment.\n- **Royalty:** ERC-2981 returns 2.5% to the treasury and the interface id is correct.\n- **GOLD book:** it releases a hol","treeHash":null,"usage":{"cachedInputTokens":2324067,"inputTokens":46,"model":"claude-opus-5-5","outputTokens":39407,"runtime":"claude","turns":26,"wallClockMs":436179}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"790f4e545968ee92","findings":[{"citation":"resolved","description":"setRevealCommit (lines 283-305) checks `deadline - block.timestamp <= ACCEPTANCE_WINDOW` when the acceptance is used. The signed struct RevealCommitAcceptance(commit, previous, signerEpoch, deadline) carries no issue time, so the contract cannot tell how long an acceptance has existed: it only enforces that it is used in the final hour before its own deadline. AUDIT_README sections 1/5/8.2 and FINDINGS_RESPONSE finding 4 state the opposite (\"valid for at most an hour, so a spare acceptance cannot be kept for later\"; \"deadlines 30 days ... ahead refused\"). The signer side does not cap the deadline either: offchain/signer/reveal-chain.mjs signRevealCommit signs whatever `deadline` it is given (only tools/deploy-testnet.mjs happens to pass now+3600). So an acceptance the signer gives with deadline D stays usable by the owner alone in [D-1h, D] for as long as `previous` is still the contract's revealCommit. That is exactly the d726b1cd finding-4 scenario: after a commitment re-arm, while the new chain's first secret is unopened, the owner can hold a spare acceptance whose deadline falls after REARM_COOLDOWN and replace the chain again without asking the signer. Impact is bounded: it still needs a signature the signer once gave, and MAX_COMMIT_REARMS and the cooldown still apply. But the fix's stated property (a spare acceptance cannot be kept for later) does not hold on chain.","line":289,"path":"Submission4/contracts/EmberEvoGenesisCandidate.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.38;\n\nimport \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {EmberEvoGenesisCandidate} from \"Submission4/contracts/EmberEvoGenesisCandidate.sol\";\n\ncontract PayToken is ERC20 { constructor() ERC20(\"Test USD\", \"tUSD\") { _mint(msg.sender, 1e30); } }\n\ncontract AcceptanceWindowTest is Test {\n    EmberEvoGenesisCandidate g;\n    uint256 constant SIGNER_PK = 0xA11CE;\n    address signer;\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(1_800_000_000);\n        vm.roll(1000);\n        signer = vm.addr(SIGNER_PK);\n        PayToken t = new PayToken();\n        g = new EmberEvoGenesisCandidate(address(this), address(t), signer, address(0xBEEF), 20e18, 1200, \"tEEVO\");\n    }\n\n    function _acceptance(bytes32 commit, bytes32 previous, uint256 deadline) internal view returns (bytes memory) {\n        bytes32 domainSep = keccak256(abi.encode(\n            keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"),\n            keccak256(\"EmberEvoGenesisCandidate\"), keccak256(\"7\"), block.chainid, address(g)));\n        bytes32 structHash = keccak256(abi.encode(\n            keccak256(\"RevealCommitAcceptance(bytes32 commit,bytes32 previous,uint256 signerEpoch,uint256 deadline)\"),\n            commit, previous, uint256(g.signerEpoch()), deadline));\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PK, keccak256(abi.encodePacked(\"\\x19\\x01\", domainSep, structHash)));\n        return abi.encodePacked(r, s, v);\n    }\n\n    /// The README (sections 1, 5, 8.2) and the d726b1cd finding-4 fix state that an acceptance is valid for at most one\n    /// hour, so a spare one cannot be kept for later. An acceptance signed today with a 30-day deadline is refused today\n    /// but accepted 30 days later, in the last hour before its deadline: the owner alone can hold it and use it then.\n    function test_spareAcceptanceWithFarDeadlineIsUsableLater() public {\n        bytes32 commit = keccak256(\"fresh chain anchor\");\n        uint256 signedAt = block.timestamp;\n        uint256 deadline = signedAt + 30 days;\n        bytes memory acceptance = _acceptance(commit, bytes32(0), deadline);\n\n        vm.expectRevert(bytes(\"REVEAL_ACCEPTANCE_EXPIRED\"));\n        g.setRevealCommit(commit, deadline, acceptance);   // refused on the day it was signed (the tested behaviour)\n\n        vm.warp(deadline - 10 minutes);                    // 30 days later; the signer has not been asked again\n        // Expected (README: \"valid for at most an hour ... cannot be kept for later\"): still refused.\n        // Actual: accepted, the owner alone replaces the commitment with a 30-day-old acceptance.\n        vm.expectRevert();\n        g.setRevealCommit(commit, deadline, acceptance);\n    }\n}","reproduction":"Deployed contract, paused, revealCommit = 0, signerEpoch = 1. At T, the signer signs RevealCommitAcceptance(commit=C, previous=0, signerEpoch=1, deadline=T+30 days). At T, setRevealCommit(C, T+30d, sig) reverts REVEAL_ACCEPTANCE_EXPIRED, which is what the r5 test checks. Warp to T+30d-10min (no new signer action) and the same call succeeds: expected a revert (the acceptance is 30 days old), actual success. The Foundry proof reproduces this; it fails on the current code with 'next call did not revert as expected'. Fix: bound the lifetime itself rather than the time left. For example, add `uint256 notBefore` (or issuedAt) to RevealCommitAcceptance and require notBefore <= now <= deadline && deadline - notBefore <= ACCEPTANCE_WINDOW. Alternatively, bind the acceptance to a contract nonce the owner cannot advance on its own. Also make signRevealCommit refuse a deadline more than 3600 s past the latest block time, and correct README 8.2.","severity":"low","snippet":"unchecked { require(deadline - block.timestamp <= ACCEPTANCE_WINDOW, \"REVEAL_ACCEPTANCE_EXPIRED\"); }","title":"RevealCommitAcceptance one-hour window bounds only deadline-now at use time: an acceptance with a far deadline is a spare the owner alone can use later (finding 4 fix incomplete)"}],"hash":"b1a24c492c2a8f0b1069a07fdb523817e309ec7269b75270c056052b45ec037f","nodeId":"58f9c3ed-c2c3-4d5c-aa9c-bd25ea0c6dfe","outcome":"completed","summary":"I found one low-severity issue in Submission 4 and nothing above low. The finding is in `.imd-findings.json`, with a Foundry proof that fails on the current code.\n\n## Finding (low): the one-hour acceptance window doesn't stop a spare acceptance being used later\n`Submission4/contracts/EmberEvoGenesisCandidate.sol:289`, `setRevealCommit`\n\n- **What's wrong:** the check `deadline - block.timestamp <= ACCEPTANCE_WINDOW` only measures how much time is left when the acceptance is used. The signed `RevealCommitAcceptance(commit, previous, signerEpoch, deadline)` has no issue time, so the contract can't tell how old an acceptance is.\n- **What the signer does:** `signRevealCommit` in `offchain/signer/reveal-chain.mjs` signs whatever deadline it is given. Only the deploy tool happens to pass now + 3600.\n- **Failing input:** the signer signs an acceptance at time T with a deadline of T + 30 days. At T it is refused with `REVEAL_ACCEPTANCE_EXPIRED`, which is all the r5 test checks. At T + 30 days − 10 minutes, the same signature succeeds with no new action from the signer. The Foundry test fails with \"next call did not revert as expected\".\n- **Impact:** this partly reopens the third audit's finding 4. The owner alone can keep a spare acceptance and use it after `REARM_COOLDOWN`, as long as `previous` is still the current commitment. That contradicts README §8.2 (\"a spare acceptance cannot be kept for later\"). It is low because it still needs a signature the signer once gave, and the re-arm limit and cooldown still apply.\n- **Fix:** sign a `notBefore` (or issue time) and require `notBefore <= now <= deadline` and `deadline - notBefore <= 1 hour`. Also make `signRevealCommit` refuse deadlines more than an hour past the latest block, and correct the README.\n\n## Areas checked with no defect found\n- **Re-arm bookkeeping (bits 64–215):**\n  - A new commitment shifts the last two commitment blocks and clears the expiry fields; an expiry keeps both commitment blocks.\n  - `epochSeedBlock` applies the expiry block only to its own epoch, and the expiry block is always later than the last commitment block.\n  - `_commitRearms` gives the right count when the limit is 2: every commitment re-arm after an epoch closes moves it while it is unsealed, and commitments set before it closed or expiries don't count.\n  - The cooldown counts from a pending expiry block, and the chosen-skin freeze uses the epoch's original seed block.\n  - Epoch 0 with no expiry set is handled, and none of the values can overflow its bit field.\n- **One-hour window arithmetic:** a past deadline wraps around and is refused; now and now + 3600 are accepted.\n- **`mintFree` index bits:**\n  - No wallet can claim twice: the leaf binds the index to the caller, and the signer de-duplicates and sorts the list.\n  - No wallet can use or block another's slot. Double hashing rules out using an internal tree node as a leaf.\n  - The free-mint count check matches the tree only if the owner configures the matching `eligibleWalletCount`, which the deploy tool does.\n- **`mintPaid`:** nothing was lost compared with Submission 3.\n  - The rebuilt authorization covers order id, caller, look, choice, URI hash, the fixed recipient and both costs, deadline and signer epoch.\n  - The domain still binds chain and contract.\n  - It can't be swapped with a free-mint approval, because the type hashes differ.\n  - The dropped `expectedRecipient`/`expectedCost` values are now fixed at deployment.\n  - The exact payer and recipient balance checks reject fee-on-transfer tokens, and reentrancy is guarded.\n- **`royaltyInfo`:** correct, and `supportsInterface` reports ERC-2981.\n- **Signer:** the tree, `indexOf`, ABIs and field order in the free and paid approvals match the contract. `readyReveals` reads everything at one block, which is now safe because an expiry no longer moves later epochs.\n- **GOLD book:** the Sepolia registry entry (domain version 7, recipient `0x29e7…f01e`) matches the deployment record.\n\n## Limits of thi","treeHash":null,"usage":{"cachedInputTokens":1287787,"inputTokens":32,"model":"claude-opus-5-5","outputTokens":26529,"runtime":"claude","turns":17,"wallClockMs":290624}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4fcac33561ba733d","findings":[{"citation":"resolved","description":"Merged from audit_flow (d5ff4d7d) and audit_math (3b8ff7a3), which report the same mechanism. setRevealCommit (lines 283-305) checks `deadline - block.timestamp <= ACCEPTANCE_WINDOW` when it is called. RevealCommitAcceptance(commit, previous, signerEpoch, deadline) carries no issue time or nonce the contract can check, so the contract enforces a usable window in the last hour before the signer-chosen deadline, not a lifetime of at most one hour. The unchecked arithmetic itself is correct: a past deadline wraps and is refused, and deadline == now and deadline == now + 3600 are accepted. AUDIT_README line 13 ('valid for at most one hour'), line 98, line 223 ('valid for at most an hour, so a spare acceptance cannot be kept for later'), the code comment at lines 286-288 and FINDINGS_RESPONSE finding 4 all claim the stronger property. Off chain, signRevealCommit (offchain/signer/reveal-chain.mjs:46) signs any deadline it is given. Only tools/deploy-testnet.mjs passes latest block time + 3600. Consequence: a spare acceptance with deadline D can still be used by the owner alone in [D-1h, D], provided `previous` is still the contract's revealCommit. That is the d726b1cd finding-4 case: after a commitment re-arm, while the new chain's first secret is unopened, the owner holds an acceptance with D after REARM_COOLDOWN and replaces the chain again without asking the signer. Bounded: the signer must once have signed a deadline more than an hour ahead, and MAX_COMMIT_REARMS and REARM_COOLDOWN still apply.","line":289,"path":"Submission4/contracts/EmberEvoGenesisCandidate.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.38;\n\nimport \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {EmberEvoGenesisCandidate} from \"Submission4/contracts/EmberEvoGenesisCandidate.sol\";\n\ncontract PayToken is ERC20 { constructor() ERC20(\"Test USD\", \"tUSD\") { _mint(msg.sender, 1e30); } }\n\ncontract AcceptanceWindowTest is Test {\n    EmberEvoGenesisCandidate g;\n    uint256 constant SIGNER_PK = 0xA11CE;\n    address signer;\n\n    function setUp() public {\n        vm.chainId(11155111);\n        vm.warp(1_800_000_000);\n        vm.roll(1000);\n        signer = vm.addr(SIGNER_PK);\n        PayToken t = new PayToken();\n        g = new EmberEvoGenesisCandidate(address(this), address(t), signer, address(0xBEEF), 20e18, 1200, \"tEEVO\");\n    }\n\n    function _acceptance(bytes32 commit, bytes32 previous, uint256 deadline) internal view returns (bytes memory) {\n        bytes32 domainSep = keccak256(abi.encode(\n            keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"),\n            keccak256(\"EmberEvoGenesisCandidate\"), keccak256(\"7\"), block.chainid, address(g)));\n        bytes32 structHash = keccak256(abi.encode(\n            keccak256(\"RevealCommitAcceptance(bytes32 commit,bytes32 previous,uint256 signerEpoch,uint256 deadline)\"),\n            commit, previous, uint256(g.signerEpoch()), deadline));\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PK, keccak256(abi.encodePacked(\"\\x19\\x01\", domainSep, structHash)));\n        return abi.encodePacked(r, s, v);\n    }\n\n    /// The README (sections 1, 5, 8.2) and the d726b1cd finding-4 fix state that an acceptance is valid for at most one\n    /// hour, so a spare one cannot be kept for later. An acceptance signed today with a 30-day deadline is refused today\n    /// but accepted 30 days later, in the last hour before its deadline: the owner alone can hold it and use it then.\n    function test_spareAcceptanceWithFarDeadlineIsUsableLater() public {\n        bytes32 commit = keccak256(\"fresh chain anchor\");\n        uint256 signedAt = block.timestamp;\n        uint256 deadline = signedAt + 30 days;\n        bytes memory acceptance = _acceptance(commit, bytes32(0), deadline);\n\n        vm.expectRevert(bytes(\"REVEAL_ACCEPTANCE_EXPIRED\"));\n        g.setRevealCommit(commit, deadline, acceptance);   // refused on the day it was signed (the tested behaviour)\n\n        vm.warp(deadline - 10 minutes);                    // 30 days later; the signer has not been asked again\n        // Expected (README: \"valid for at most an hour ... cannot be kept for later\"): still refused.\n        // Actual: accepted, the owner alone replaces the commitment with a 30-day-old acceptance.\n        vm.expectRevert();\n        g.setRevealCommit(commit, deadline, acceptance);\n    }\n}","reproduction":"Fresh deployment (owner = test, signer key 0xA11CE, chainId 11155111, paused, revealCommit = 0, signerEpoch = 1). At T the signer signs RevealCommitAcceptance(commit=C, previous=0, signerEpoch=1, deadline=T+30 days). setRevealCommit(C, T+30d, sig) at T reverts REVEAL_ACCEPTANCE_EXPIRED, which is what the project's test checks. vm.warp(T+30d-10min), with no new signer action. Expected per README 8.2: revert, because the acceptance is 30 days old. Actual: success, and revealCommit == C. I ran the attached proof with forge 1.8.3, solc 0.8.38 via-IR cancun, OZ from Submission4/build/standard-input.json. It fails on the current code with 'next call did not revert as expected'. Fix: bind the acceptance to something fresh the contract can check. One option: add `uint256 notBefore` (issue time) to the struct and require notBefore <= now <= deadline && deadline - notBefore <= ACCEPTANCE_WINDOW. Another: bind it to reveal state the owner cannot hold still, such as nextRevealIndex or a commit nonce. Also make signRevealCommit refuse deadline > latest block time + 3600. Otherwise restate README 1/5/8.2 so the guarantee is the signer's policy, not the contract's.","severity":"low","snippet":"        unchecked { require(deadline - block.timestamp <= ACCEPTANCE_WINDOW, \"REVEAL_ACCEPTANCE_EXPIRED\"); }","title":"RevealCommitAcceptance window bounds only deadline - now at use time, not the acceptance's lifetime: an acceptance signed with a far deadline becomes usable by the owner alone in its last hour (d726b1"},{"citation":"resolved","description":"Merged from audit_permissions (48e083b9) and audit_economics (25e1a446), which report the same mechanism. The Submission 4 layout is implemented as described: _rearm(e, true) writes only bits 144-215, and epochSeedBlock (line 445) applies the expiry block only to epoch uint32(rs >> 184). But the property the package claims for it does not hold. The claims are in the code comment at lines 324-325 ('a withheld secret buys one fresh draw for its own epoch only'), in AUDIT_README line 225 ('later epochs keep their own seed blocks ... and each needs its own 27-hour withhold for its own one fresh draw'), line 227 ('only the expired epoch is exposed'), and FINDINGS_RESPONSE finding 2. Later epochs' secrets sit deeper in the same chain (x[i-1] = keccak(x[i])), so nobody but the signer can settle them while it withholds the epoch ahead. Their seed blocks age in parallel with the withheld one. (a) Epochs that share the last commitment re-arm block expire at the same moment: once the epoch ahead is sealed, the next secret published for the epoch behind hits h == 0 at line 623 and re-arms it. Its outcome, fixed since that block, is discarded with no second withhold. This is the Submission 3 backlog effect, reached through each epoch's own expiry. (b) Epochs on natural seed blocks (n, n+25, ...): when the epoch ahead is sealed, the signer, who already knows the next epoch's outcome, can publish its secret to keep that outcome, or wait about 25 more blocks per epoch (not another 8,191) and get a fresh draw. That is a per-epoch keep-or-redraw choice for every random epoch that closed during one visible ~27 h withhold. Random mints are not stopped by a pending reveal (_checkMint, random branch), so the backlog can grow to ~8,191/25 epochs. This is signer trust only: no minter or owner gains a lever, and per epoch the expiry draw still cannot repeat while the secret is private.","line":623,"path":"Submission4/contracts/EmberEvoGenesisCandidate.sol","reproduction":"I ran Foundry (forge 1.8.3, solc 0.8.38 via-IR cancun, OZ from build/standard-input.json) against the unmodified contract. There is no EIP-2935 code, so the window is 256 blocks instead of 8,191. Setup: chain x0 = keccak(x1), x1 = keccak(x2); free random mints at S+1 (epoch 0, seed S+25) and S+26 (epoch 1, seed S+50). Case (b): at S+282, settle([x1]) gives epochSecretOpened(0) == true. At S+284, settle([]) seals epoch 0, and epochSeedBlock(1) == S+50. From that state, option A: settle([x2]) at S+284 seals epoch 1 on hash(S+50), which the signer already knew (nextRevealIndex == 2). Option B: roll to S+307 and settle([x2]) gives an expiry re-arm, epochSecretOpened(1) == true and epochSeedBlock(1) == S+308, a fresh draw for 23 extra blocks. Case (a): pause, roll to S+50+7201, setRevealCommit(y0, previous=x0). Now epochSeedBlock(0) == epochSeedBlock(1) == c1. At c1+257, settle([y1]) re-arms epoch 0 alone. At c1+259, settle([]) seals it. Then settle([y2]), published at once. Expected per README 8.2: epoch 1 seals on hash(c1), because its secret was not withheld. Actual: nextRevealIndex stays 1, epochSecretOpened(1) == true, epochSeedBlock(1) == c1+260, so epoch 1 is re-drawn. Both tests pass, which confirms the behaviour. Fix (owner decision, since this is the trust model): restate the code comment, README 5/8.2 and FINDINGS_RESPONSE finding 2. One withhold of the next epoch forces a re-draw on every later epoch sharing a commitment re-arm block, and gives the signer a keep-or-redraw choice on every epoch that closed during the withhold, at ~25 blocks each. Have the keeper treat a ready epoch with no published secret as affecting the whole backlog. Optionally stop random mints while a ready epoch has waited more than N blocks, to cap the backlog. Or, on an expiry re-arm, also move every later closed epoch once, so the re-draw is forced rather than optional (without compounding). If that bound is unacceptable for the 48 gold skins, use the VRF option from 8.2.","severity":"low","snippet":"            if (h == bytes32(0)) { _rearm(e, true); return progressed; }","title":"An expiry re-arm still re-draws, or offers a keep-or-redraw choice on, every later closed epoch whose seed block expires during the same withhold: one withheld secret buys more than 'one fresh draw fo"},{"citation":"resolved","description":"From audit_permissions (678495c7). encodeMintAuth's binding (line 41: id, wallet, gold, deadline, chainId, contract, chainInstance) carries no registry entry. So the genesis-r5-paid-v1 branch checks d.version against the fixed set {'5','7'} (line 70) and o.recipient only for being a lowercase non-zero address (line 73). The comment at lines 66-68 says 'Domain version 5, or the registry's domainVersion 7'. For the recipient it already says parseOrder enforces the registry value. Today genesis-gold.ts parseOrder (line 53) and genesisAuthDocument (line 63) build the document from the registry, so reserveGenesisGold stores no wrong hold. The stored-document check is a weaker second line than documented, and it gives no protection to any other writer of this schema. There is no on-chain impact: a mismatched domain or recipient fails the contract's PaidAuthorization signature check.","line":70,"path":"Submission4/offchain/gold-book/server/world-gold-result.ts","reproduction":"Call encodeMintAuth(auth, {chainInstance:'sepolia-r6-rehearsal-2', chainId:11155111, contract:<a deployment whose registry entry has no domainVersion, so v5>, ...}) with a genesis-r5-paid-v1 document whose domain.version is '7' and whose order.recipient is any non-zero lowercase address other than that deployment's. Expected per the comment: rejected (throw, giving INVALID_MINT_AUTH). Actual: accepted, because line 70 tests only ['5','7'].includes(d.version) and line 73 only for a non-zero recipient. I verified this by reading the code; it is not executed. Fix: add domainVersion and recipient (from the registry entry) to the binding at world-gold.ts:135 and require d.version === (entry.domainVersion ?? '5') and o.recipient === (entry.recipient ?? GENESIS_DEAD). Otherwise reword the comment to say that only parseOrder enforces these values.","severity":"info","snippet":"   if(!record(o)||!record(d)||!record(c)||d.name!=='EmberEvoGenesisCandidate'||!['5','7'].includes(d.version as string)||d.chainId!==binding.chainId||!sameAddress(d.verifyingContract,binding.contract)","title":"GOLD book stored-hold validator accepts domain version 5 or 7 and any non-zero recipient for every deployment, not the registry's values its comment names"}],"hash":"c9d3d60fec6865b8cc80c61792e100d10e4b2eee3bca42db5c908acf498d9658","nodeId":"8eefeb2f-5d65-4c77-b9c9-6a0e9946f027","outcome":"completed","summary":"I found three things worth reporting in Submission 4: two low and one info. None is critical or high. The findings are in `.imd-findings.json` at the repo root. I changed no repository files; all the Foundry work ran in `/tmp/fp`, outside the repo.\n\n**Kept findings:**\n\n1. **Low: the one-hour acceptance limit only checks the time left, not the acceptance's age** (`EmberEvoGenesisCandidate.sol:289`). I merged two specialist reports. The contract only checks that `deadline - now <= 1 hour` when the acceptance is used. So an acceptance the signer signed with a 30-day deadline is refused on day one but accepted in its last hour. Until then the owner alone can hold it and replace the chain without asking the signer. The README and FINDINGS_RESPONSE say this cannot happen. I ran the specialist's proof: it fails on the current code with \"next call did not revert as expected\", so I attached it. Fix: sign an issue time (or a nonce the contract checks) and bound the lifetime, and have the signer refuse deadlines more than an hour ahead.\n\n2. **Low: one withheld secret still re-draws later epochs** (`:623`). I merged two specialist reports. The fix itself works: an expiry re-arm moves only its own epoch. But the claim that each later epoch \"needs its own 27-hour withhold\" is wrong, because later epochs' seed blocks age during the same withhold. My two Foundry tests both pass, confirming the behaviour:\n   - **Epochs sharing a commitment block:** the next epoch is re-drawn even though its secret is published at once.\n   - **Epochs on their own seed blocks:** the signer can keep an outcome it already knows, or wait about 23 more blocks for a new draw.\n   \n   This needs the owner's decision: either correct the documentation (and have the keeper alert on the whole backlog), or change how later epochs move on an expiry.\n\n3. **Info: the GOLD book accepts version 5 or 7 for every deployment** (`world-gold-result.ts:70`). The code check accepts both versions and any non-zero recipient for every deployment; its comment says it uses the registry's values. I confirmed this by reading the code but did not run it. There is no on-chain effect today, because the server builds each stored document from the registry.\n\n**Checked and nothing found:**\n- **Free mints:** no wallet can claim twice, take another wallet's index, or block one. The leaf ties the index to the caller, the signer's tree removes duplicate wallets, and the leaf is hashed twice.\n- **Paid mints:** nothing Submission 3 bound is lost. The rebuilt PaidAuthorization covers every field the old Appearance signature and order did, and the two routes can't be swapped.\n- **Re-arm bookkeeping, re-arm limit, deadline arithmetic, royalty, payment recipient and pinned `readyReveals` reads:** all behave as described.\n\n**Coverage:** I did the twelve review areas as my own passes; no helper agents ran. Only the Solidity runs were done in Foundry, with forge-std fetched into `/tmp`. The specialist's proof used the project's real build settings (solc 0.8.38, OZ sources from `build/standard-input.json`). Forge has no EIP-2935 block-hash contract, so the tests use the 256-block window instead of 8,191.","treeHash":null,"usage":{"cachedInputTokens":1367784,"inputTokens":42,"model":"claude-opus-5-5","outputTokens":26397,"runtime":"claude","turns":22,"wallClockMs":337499}}],"verification":[]}