{"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":"31bb373a-4cd3-49b2-973c-758dde5b6ec4","kind":"audit","nodes":[{"acceptedSubmissionHash":"63c69fabf5666ff08930d70f648610a31babf4bf0ec2dc9801f8b6a82b9255a9","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":"c2cda8e3a8522984a04461c130772093202ffdf5ebfd0e31829dd74347a8c4c8","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":"9d11a56700669e2550572c120f8315aa2cd2fbfee19f087b5d6acd020cba44b5","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":"af2e8d6edb35a0ac991058ef5ae8a8438f7bc5dfd403b4ae660200bfd68d861a","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":"739c0929aae86786b738a9ea975de73b5ea616b92b415974a619c5933d2bbd0b","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":"Read Submission1/AUDIT_README.md first. Primary scope: Submission1/contracts/EmberEvoGenesisCandidate.sol (verified on Sepolia 0x8ce67008570ED41F64e4Be149ecAA66d1aBE2efa, solc 0.8.38). Not in scope: contracts/test, contracts/testnet, OpenZeppelin. 1) Commit-reveal randomness: can a minter, the owner or the signer steer a random skin (timing, chosen-skin mints around a reveal, out-of-order sealing, stale or extra secrets, expired seed block, EIP-2935 fallback, setRevealCommit re-arm)? 2) Supply and per-skin caps: can supply exceed 1,288, a skin exceed its cap, or a random token end with no skin? 3) Signatures: replay of Appearance / PaidAuthorization across wallets, choices, deadlines, signer rotation (signerEpoch), chains or contracts; ERC-1271 signers. 4) Paid mint: exact paidCost burn, order consumed once, revoked orders, fee-on-transfer tokens, reentrancy, the transient one-paid-mint-per-transaction marker with smart wallets. 5) Gas bounds when one mint settles up to 8 epochs x 25 tokens; REVEAL_PENDING griefing. Secondary (trust model): Submission1/offchain signer and GOLD ledger. Report severity, reproduction and fix.","parentJobId":null,"planHash":"b74189f3d05de0b1aa06849bbe51e2a388e591e545fde564f305ac9f79a4ef2f","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"31bb373a-4cd3-49b2-973c-758dde5b6ec4","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":"51506","feedbackHash":"4d7ac08a0471c343d2500f3e41a1670bffa246f8b5fab4d1e755be4fc61be3d8","nodeKey":"audit_economics","submissionHash":"63c69fabf5666ff08930d70f648610a31babf4bf0ec2dc9801f8b6a82b9255a9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52419","feedbackHash":"c15bed46f151b0a813025847af642c7d19b9e0d069fd998b6d135d8e7a38a675","nodeKey":"audit_flow","submissionHash":"c2cda8e3a8522984a04461c130772093202ffdf5ebfd0e31829dd74347a8c4c8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51882","feedbackHash":"ece1902bd068775af1fb6eaa4a7b0f53f0974624932f82fc4ab8763de1ba27da","nodeKey":"audit_judge","submissionHash":"9d11a56700669e2550572c120f8315aa2cd2fbfee19f087b5d6acd020cba44b5","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52180","feedbackHash":"e7ab90afc0f77087795031ec6e56cb8ed67c42071340a7558e53b2ee6fb8db00","nodeKey":"audit_math","submissionHash":"af2e8d6edb35a0ac991058ef5ae8a8438f7bc5dfd403b4ae660200bfd68d861a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52150","feedbackHash":"e461809f4ce4dc3ea12269dbfec200f444a2a6fb1acb9c949bb2d00b41441b79","nodeKey":"audit_permissions","submissionHash":"739c0929aae86786b738a9ea975de73b5ea616b92b415974a619c5933d2bbd0b","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"75837aff81f569d8f254c3031dfa5a453f9f9c434647537821dd3f64d29e1506","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7236faf97e42979a","findings":[{"citation":"resolved","description":"createGenesisR5Reader.read() proves consumption from the transaction receipt of the PaidOrderConsumed log. It rejects the receipt unless `receipt.to` is the Genesis contract (line 61) and unless the receipt holds exactly one GenesisMinted log from the contract (line 67: `minted.length!==1`, where `minted` is not filtered by tokenId). Both conditions hold only for a paid mint sent directly by an EOA. The contract itself accepts contract callers (ERC-1271 is used for the signer, `_safeMint` calls onERC721Received, and the transient marker comment at line 374 is explicitly about smart wallets), and the README asks about smart-contract wallets (8.7). A Safe `execTransaction`, an ERC-4337 bundle (`receipt.to` = EntryPoint) or an EIP-7702 batch (`receipt.to` = the EOA itself, plus possibly a second GenesisMinted from a free mint in the same batch, which the transient marker does not prevent) makes read() throw MINT_PROOF_CONFLICT. reconcileHeldMintGold (world-gold.ts line 184-188) only settles when read() returns, so the order is neither 'consumed' nor 'released': the hold is retried by cron forever and the wallet's GOLD stays reserved although the token was minted and the EMBEO burned. Two users whose paid mints are bundled into one 4337 transaction are both stuck, through no action of their own. There is no admin command that settles a mint hold (goldAdminCommand has none).","line":61,"path":"Submission1/offchain/gold-book/server/genesis-gold-reader.ts","reproduction":"State: a paid order O for wallet W, GOLD held in the book. W is a Safe (or any contract wallet); W executes mintPaid(...) through its own execTransaction; the mint succeeds on chain (tokenId = 7, PaidOrderConsumed(O, W, 7) emitted, EMBEO burned). Reader: paidOrderTokenId(O) at the finalized block = 7 != 0 -> fetch receipt -> `String(receipt.to).toLowerCase()` is the Safe address, not the Genesis contract -> fail('MINT_PROOF_CONFLICT'). reconcileHeldMintGold outcome: {id: O, status: 'MINT_PROOF_CONFLICT'}; world_gold_mints.state stays 'held' permanently; the wallet's `reserved` never decreases. Same with an ERC-4337 EntryPoint.handleOps bundle carrying two users' mintPaid userOps: receipt.to = EntryPoint, and even with the `to` check removed, `minted.length` is 2 at line 67 -> both orders stuck. Expected: consumption is proven by the PaidOrderConsumed(orderId) log and the GenesisMinted(wallet, tokenId) log with the same tokenId, regardless of who the outer transaction targeted. Fix: drop the `receipt.to` requirement (the log's `address` is already checked to be the contract); select `minted` as the GenesisMinted log whose topics[1] == wallet and topics[2] == tokenId instead of requiring exactly one; alternatively prove consumption from the PaidOrderConsumed log alone (it already carries wallet, tokenId and goldCost) plus the eth_call of paidOrderTokenId.","severity":"medium","snippet":"   if(qty(receipt.status)!==1n||String(receipt.to).toLowerCase()!==contract||rBlock>finNumber)fail('MINT_PROOF_CONFLICT');","title":"GOLD book never settles a paid mint sent through a smart-contract wallet or an ERC-4337 bundle: the reader requires receipt.to == Genesis and exactly one GenesisMinted per receipt, so the hold stays '"},{"citation":"resolved","description":"setRevealCommit accepts any non-zero commitment from the owner while paused and re-arms every unsealed epoch whose seed block is already mined to block.number + 1. Nothing binds the new commitment to the approval signer (no SignerAcceptance-style signature as for signer rotation), and nothing limits how often an epoch may be re-armed. So the owner can generate its own secret s, call setRevealCommit(keccak(s)), wait two blocks, compute the epoch's full outcome off chain (seed = keccak(h(B+1), s, e, this) and the draw loop are public), and if it dislikes the result call setRevealCommit again with a fresh secret: the epoch moves to the next block and is rolled again. Each roll is fair, but the owner keeps the roll it likes. With 25 tokens in an epoch and the molten-gold cap of 48/1288, a given token lands on gold with p ~ 3.7% per roll, so about 27 rolls (about one hour, one owner transaction every two blocks) put a chosen token on the rarest skin. README 8.2 states that re-rolling needs 'Owner + signer together'; the signer is not needed, because the commitment chain is replaced by the owner, not extended by the signer. A related weakness: the owner may set the commitment to the hash of a value that is already public (for example the secret that was just consumed, whose preimage chain is published), so the secret is known before the re-armed seed block and the proposer of that block can grind the hash, which is exactly what the commit-reveal scheme was added to prevent. Property 4 of section 6 ('No minter can choose or steer...') holds for minters; this finding documents that the owner power is larger than described and unbounded.","line":222,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.38;\n\nimport \"forge-std/Test.sol\";\nimport {EmberEvoGenesisCandidate} from \"Submission1/contracts/EmberEvoGenesisCandidate.sol\";\n\ncontract MockEmbeo {\n    string public name = \"T\"; string public symbol = \"T\"; uint8 public decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; totalSupply += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Finding: the owner alone can re-arm the same ready, unsealed epoch again and again through setRevealCommit,\n/// each time with a commitment of its own choosing, and so pick the best of many random outcomes for that epoch.\n/// Fails on the current code (the second re-arm of epoch 0 succeeds); passes once an epoch can be re-armed at most once.\ncontract OwnerRerollProof is Test {\n    EmberEvoGenesisCandidate g;\n    MockEmbeo token;\n    address owner = address(0xA11CE);\n    uint256 signerKey = 0xB0B;\n    uint256 constant COST = 1e18;\n    uint256 constant GOLD = 5;\n    bytes32 constant APPEARANCE_TYPEHASH = keccak256(\"Appearance(address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,uint256 deadline,uint256 signerEpoch)\");\n    bytes32 constant PAID_AUTH_TYPEHASH = keccak256(\"PaidAuthorization(bytes32 orderId,address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,address recipient,uint256 embeoCost,uint256 goldCost,uint256 deadline,uint256 signerEpoch)\");\n\n    function setUp() public {\n        token = new MockEmbeo();\n        vm.roll(100);\n        g = new EmberEvoGenesisCandidate(owner, address(token), vm.addr(signerKey), COST, GOLD, \"EEG\");\n        bytes32 x1 = keccak256(\"signer-secret-1\");\n        vm.startPrank(owner);\n        g.configureSnapshot(bytes32(0), 0, block.timestamp, block.timestamp);\n        g.setRevealURIs(\"ipfs://pending\", \"ipfs://base/\", true);\n        g.setRevealCommit(keccak256(abi.encode(x1)));   // the signer's chain: x0 = keccak(x1)\n        g.setMintPaused(false);\n        g.approvePaidPrice();\n        vm.stopPrank();\n    }\n\n    function sign(bytes32 structHash) internal view returns (bytes memory) {\n        bytes32 dom = keccak256(abi.encode(keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"),\n            keccak256(\"EmberEvoGenesisCandidate\"), keccak256(\"6\"), block.chainid, address(g)));\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, keccak256(abi.encodePacked(\"\\x19\\x01\", dom, structHash)));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function mintRandom(address w) internal {\n        token.mint(w, COST);\n        vm.prank(w); token.approve(address(g), COST);\n        bytes32 lookKey = keccak256(\"look\");\n        uint256 deadline = block.timestamp + 100;\n        bytes memory sig = sign(keccak256(abi.encode(APPEARANCE_TYPEHASH, w, lookKey, uint8(3), keccak256(bytes(\"\")), deadline, uint256(1))));\n        EmberEvoGenesisCandidate.PaidAuthorization memory o = EmberEvoGenesisCandidate.PaidAuthorization({\n            orderId: keccak256(\"order-1\"), wallet: w, lookKey: lookKey, choice: 3, uriHash: keccak256(bytes(\"\")),\n            recipient: 0x000000000000000000000000000000000000dEaD, embeoCost: COST, goldCost: GOLD, deadline: deadline, signerEpoch: 1});\n        bytes memory gsig = sign(keccak256(abi.encode(PAID_AUTH_TYPEHASH, o)));\n        bytes32[] memory none;\n        vm.prank(w);\n        g.mintPaid(lookKey, 3, \"\", deadline, sig, 0x000000000000000000000000000000000000dEaD, COST, o, gsig, none);\n    }\n\n    function test_owner_cannot_rearm_the_same_epoch_twice() public {\n        vm.roll(101);\n        mintRandom(address(0x1111));                 // one random token in epoch 0; seed block = 125\n        assertEq(g.epochSeedBlock(0), 125);\n        vm.roll(126);                                // epoch 0 is ready; the signer's secret x1 is public, nobody sealed it\n        vm.startPrank(owner);\n        g.setMintPaused(true);\n        // First replacement: a legitimate recovery re-arms epoch 0 to block 127 (secrets fixed before that hash exists).\n        g.setRevealCommit(keccak256(abi.encode(keccak256(\"owner-secret-A\"))));\n        assertEq(g.epochSeedBlock(0), 127);\n        vm.roll(128);                                // block 127 is mined: with secret A the owner can compute epoch 0's outcome\n        // Second replacement of an epoch that was already re-armed: must be refused, otherwise the owner re-rolls\n        // the outcome as often as it likes (every two blocks) and keeps the roll it prefers.\n        vm.expectRevert();\n        g.setRevealCommit(keccak256(abi.encode(keccak256(\"owner-secret-B\"))));\n        vm.stopPrank();\n        assertEq(g.epochSeedBlock(0), 127, \"epoch 0 must keep its single re-armed seed block\");\n    }\n}","reproduction":"State: startBlock = 100, one random token minted in epoch 0 (seed block 125), block 126 mined (epoch 0 ready, unsealed, signer's secret x1 public). Owner: setMintPaused(true); setRevealCommit(keccak(abi.encode(A))) at block 126 -> EpochRearmed(0, 127). At block 128 the owner computes epoch 0's outcome with secret A and blockhash(127). Owner: setRevealCommit(keccak(abi.encode(B))) at block 128 -> succeeds, EpochRearmed(0, 129): the same epoch is re-rolled. Repeat without limit. Expected (README 8.2): a re-roll needs the signer's cooperation and is a one-off recovery. Actual: the owner alone re-rolls indefinitely. The attached test fails on the current code (the second setRevealCommit does not revert) and passes once an epoch can be re-armed at most once. Minimal fix preserving the recovery feature: refuse to re-arm an epoch whose epochRearmBlock is already set (one re-arm per epoch), and require the new commitment to be countersigned by the appearance signer (EIP-712 'RevealCommitAcceptance(bytes32 commit, uint256 nonce, uint256 deadline)' checked with SignatureChecker like activateAppearanceSigner), so that neither owner nor signer alone can replace the chain; also update README 8.2.","severity":"medium","snippet":"            epochRearmBlock[e] = block.number + 1; ++rearmed; emit EpochRearmed(e, block.number + 1);","title":"Owner alone can re-roll a ready epoch's random outcome any number of times via setRevealCommit (no signer needed), contrary to README 8.2"},{"citation":"resolved","description":"Random mints are allowed while a reveal is pending (REVEAL_PENDING only stops chosen-skin mints), so while the signer is not publishing secrets (service down, key or seed lost), every 25-block epoch can still gain random tokens, each creating one more unsealed epoch in randomEpochs. setRevealCommit, the only recovery for a lost or compromised chain, then re-arms all of them in one call: SLOADs of randomEpochs[i], epochSeed[e], epochRearmBlock[e], one new SSTORE and one event per epoch. Measured in Foundry (solc 0.8.38, viaIR, 200 runs): 8,623,586 gas for 300 ready one-token epochs, i.e. ~28.7k per epoch; at the 1,288-token maximum (one random token per epoch, about 5.4 days of a non-publishing signer) the call needs ~37M gas, above a 36M block gas limit, and the owner can never replace the chain: all pending random tokens stay unrevealed (skin 0, pendingURI) forever. Minters are not harmed otherwise, which keeps this Low, but it defeats the designed recovery exactly when it is needed.","line":218,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"State: signer stops publishing; a wallet mints one random token per epoch for 1,288 epochs (or any attacker-controlled number of wallets); owner pauses and calls setRevealCommit(newCommit). Actual: the loop at line 218 runs 1,288 iterations (~37M gas) and the transaction cannot be included; with 300 epochs it costs 8.62M gas (measured). Expected: recovery cost independent of how many epochs are pending. Fix: do not re-arm in a loop. Store a single `rearmFromBlock = block.number + 1` with the new commitment and have epochSeedBlock(e) return max(natural seed block, rearmFromBlock) for unsealed epochs at or after nextRevealIndex at the time of the commit (record `rearmFromIndex = nextRevealIndex`); or bound the loop and let the owner call setRevealCommit repeatedly, each call re-arming at most N epochs from a persisted cursor.","severity":"low","snippet":"        for (uint256 i = nextRevealIndex; i < randomEpochs.length; ++i) {","title":"setRevealCommit loops over every unsealed ready epoch (about 28.7k gas each): with enough one-token epochs the lost-chain recovery no longer fits in a block and every pending random token is stuck"},{"citation":"resolved","description":"readyReveals hands out x[k] when epochSeedBlock(e) <= provider.getBlockNumber(), i.e. the moment the seed block is the latest block. The contract seals with whatever hash block `seedBlock` has when _seal runs (blockhash / EIP-2935). If the seed block is reorged out after the secret is public (one-block reorgs still happen on mainnet, and the signer's RPC may be on a minority fork), the proposer of the replacement block knows the secret and can grind its block hash to pick the epoch's outcome, which is the exact attack the commit-reveal was introduced against (README 5: 'whoever produces that block cannot know the secret'). The guarantee only holds for blocks that are no longer reorgable when the secret leaves the signer.","line":39,"path":"Submission1/offchain/signer/reveal-chain.mjs","reproduction":"State: epoch e with seed block S. The signer's RPC reports latest = S; GET /v1/reveals returns x[k]; the page or anyone broadcasts it. Block S is orphaned by a competing block S' whose proposer read x[k] from the mempool or the API; the proposer builds S' with a chosen extra-nonce/transaction ordering until keccak(abi.encode(blockhash(S'), x[k], e, this)) gives the preferred draws, then publishes S'. The next settle seals epoch e with blockhash(S'). Expected: the secret is released only once the seed block cannot change. Fix: in readyReveals, require `seedBlock + CONFIRMATIONS <= latest` (e.g. 2 blocks, or use the `safe` tag), accepting that chosen-skin mints are blocked for that many extra blocks per random epoch (the contract already blocks them from seedBlock + 1 until the reveal); document the residual risk in README 8.2.","severity":"low","snippet":"  if(Number(seedBlock)>latest)break;         // seed block not mined yet: its secret stays with the signer","title":"Signer publishes an epoch's secret as soon as the seed block is the chain head (zero confirmations): a one-block reorg lets the replacement block's proposer grind the outcome"},{"citation":"resolved","description":"Question 5 of the brief. Measured in Foundry (solc 0.8.38, viaIR, cancun) with the mock token: settle() over 8 ready epochs of 25 random tokens = 3,089,165 gas; a chosen-skin mintPaid that must do that work first = 3,290,498 gas (a plain chosen mintPaid = 241,308). So the bound is about 386k per full epoch rather than the 650-690k in README 10, and the worst case fits comfortably in a block; the forced cost falls on whichever chosen-skin minter comes first (a griefer needs 200 random mints, at paidCost each, to impose ~3M gas on one victim: not economical). The 8-epoch guard means that once 9 or more epochs are ready (nine consecutive epochs each holding at least one random token while nobody settled, i.e. about 38 minutes of inactivity with one random mint per epoch), _settleReady settles 8, returns, and _approval reverts with REVEAL_PENDING for every chosen-skin mint until someone sends a separate settle(); the mint page's dry run cannot pass. Random mints are unaffected. This is by design and visible, so informational; the signer's MAX_REVEALS = 8 matches.","line":429,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"Foundry: mint one random token in each of epochs 0..8 (blocks startBlock+1, +26, ..., +201), roll to startBlock+226, then mintPaid(choice 0) carrying the 9 correct secrets: reverts REVEAL_PENDING (test test_nine_ready_epochs_chosen_mint_reverts in the harness passes on the current code). Expected: documented in README 8.3/8.4 or the page falls back to calling settle() first. Suggested: have the page call settle(reveals) when nextRevealEpoch() shows more than 8 ready epochs, and note the 386k-per-epoch measurement in the gas doc.","severity":"info","snippet":"        for (uint256 guard; guard < 8; ++guard) {","title":"Gas bound of a mint that settles 8 ready epochs x 25 tokens measured at ~3.3M gas; with 9 or more ready epochs a chosen-skin mint cannot succeed in one transaction even when it carries every secret"},{"citation":"resolved","description":"Question 4 / README 8.7. The marker slot is keyed by msg.sender and never cleared, so inside one transaction the same caller cannot mintPaid twice (also across sequential calls from a Safe batch or an EIP-7702 EOA). It does not stop: (a) mintFree followed by mintPaid by the same wallet in one batch (the receipt then holds two GenesisMinted logs, which breaks GOLD settlement, see the reader finding); (b) one outer transaction driving N distinct contract wallets, each with its own signer-issued order (the paid signer issues orders to any address, there is no allowlist), so N paid mints per transaction remain possible; (c) ERC-4337 bundles, where every userOp has a different msg.sender. As the README itself says there is no per-wallet total limit across transactions, the marker only shapes transaction composition and is safe to keep; it should not be relied on as a rate or supply limit, and the off-chain reader must not assume one mint per receipt.","line":379,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"Deploy two minimal wallets A and B (contracts that forward a call and implement onERC721Received); obtain orders for A and B from /v1/paid/prepare + /v1/approval/paid; from one EOA call A.exec(mintPaid(...)) and B.exec(mintPaid(...)) in one transaction. Actual: both succeed (different tload slots); the receipt holds two PaidOrderConsumed and two GenesisMinted logs. Expected per README 8.7 wording 'one paid mint per transaction': only one. No code change needed if the limit is understood as per-sender; otherwise key the marker by a constant slot (one paid mint per transaction for everyone) and accept that 4337 bundles then serialize paid mints.","severity":"info","snippet":"        require(used == 0, \"ONE_PAID_MINT_PER_TRANSACTION\");","title":"Transient one-paid-mint-per-transaction marker is per (msg.sender, tx) only: it does not limit free+paid in one transaction, nor N contract wallets in one transaction, and provides no economic bound"},{"citation":"resolved","description":"genesisAuthDocument writes domain {name, version:'5', chainId, verifyingContract} into every stored hold (auth_json). The deployed contract's EIP712 domain is ('EmberEvoGenesisCandidate', '6') (constructor, line 148) and the signer's DOMAIN_VERSION is '6' (free-approval.mjs line 7). The stored document is used for byte-identical retry comparison and encodeMintAuth binding, so there is no live mismatch today, but anything that later recomputes the PaidAuthorization digest from the stored document (audit replay, a dispute tool, a future verifier) will derive a digest the contract never checked. Keep the constant in one place (read it from the deployment registry entry together with codeHash) so the mainnet domain change in README 7 cannot leave the book stale.","line":13,"path":"Submission1/offchain/gold-book/server/genesis-gold.ts","reproduction":"Compute _hashTypedDataV4(PaidAuthorization) with domain version '5' for any stored order and compare with the digest the contract verified (version '6'): they differ. Expected: identical. Fix: set version to '6' (or source it from the GenesisDeployment record) and bump GENESIS_R5_AUTH_SCHEMA.","severity":"info","snippet":"export const GENESIS_R5_DOMAIN={name:'EmberEvoGenesisCandidate',version:'5'} as const;","title":"GOLD book stores the Genesis EIP-712 domain as version '5' while the contract and the signer use version '6'"}],"hash":"63c69fabf5666ff08930d70f648610a31babf4bf0ec2dc9801f8b6a82b9255a9","nodeId":"b0a6b9c1-ae4c-48ce-87b8-77c8ccbfdab3","outcome":"completed","summary":"All seven snippets match the tree exactly, and the proof test still fails on the current code for the stated reason. The findings file is complete, so I'll finish here.\n\n## Recap\n\nI audited the Ember Evo Genesis release candidate (`Submission1/contracts/EmberEvoGenesisCandidate.sol`) against the five questions in the brief, plus the signer and GOLD ledger as the secondary trust-model scope. Checks ran in a scratch Foundry project outside the repository with OpenZeppelin 5.4.0 and solc 0.8.38 (viaIR, cancun). Findings are written to `.imd-findings.json` in the repository root.\n\n**What held up.** Supply and per-skin caps cannot be exceeded and every random token finds a skin, because the sum of caps equals the max supply and chosen mints are blocked while a ready epoch is unrevealed. Signatures are bound to wallet, look, choice, URI hash, deadline, signer epoch, chain and contract. The paid path burns exactly the cost, consumes each order once, rejects revoked orders and fee-on-transfer tokens, and is guarded against reentrancy. No minter can steer a random skin by timing, out-of-order sealing, stale or extra secrets, or chosen mints around a reveal.\n\n**Findings, by severity**\n\n- **Medium, GOLD ledger.** The finalized-chain reader requires the receipt's `to` to be the Genesis contract and exactly one `GenesisMinted` log per receipt. Any paid mint through a Safe, an ERC-4337 bundle, or an EIP-7702 batch makes the reader throw, so the hold stays \"held\" forever with no admin path to settle it.\n- **Medium, on-chain trust model.** The owner alone can re-roll a ready epoch any number of times through `setRevealCommit`, since nothing binds the new commitment to the signer and nothing limits re-arms per epoch. README 8.2 says this needs owner and signer together. A Foundry proof is attached that fails now and passes once an epoch can be re-armed at most once.\n- **Low.** The re-arm loop in `setRevealCommit` costs about 28.7k gas per unsealed ready epoch (8.6M measured for 300). With enough one-token epochs the lost-chain recovery no longer fits in a block.\n- **Low, signer.** Secrets are published when the seed block is merely the chain head, so a one-block reorg lets the replacement proposer grind the outcome.\n- **Info.** Gas bound measured at about 3.3M for a chosen mint settling 8 epochs × 25 tokens, well below the README's estimate. With 9 or more ready epochs a chosen mint always reverts with `REVEAL_PENDING` until a separate settle. The transient one-paid-mint marker is per sender only and gives no economic limit. The GOLD book stores EIP-712 domain version \"5\" while the contract uses \"6\".\n\n**Not covered.** No fork of Sepolia was used, so EIP-2935 history-contract behaviour was reasoned from the specification rather than executed. The project's own Hardhat test suite was not run.","treeHash":null,"usage":{"cachedInputTokens":1847346,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":59513,"runtime":"claude","turns":29,"wallClockMs":1086081}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e764f15311426447","findings":[{"citation":"resolved","description":"setRevealCommit (owner, paused) replaces the commitment with any chain the owner generated itself and re-arms every ready, unsealed epoch to block.number + 1. Nothing records that an epoch was already re-armed, so once the re-armed block is mined the owner, who knows the new chain's next secret, computes the epoch's seed and the per-token draws off chain (keccak256(abi.encode(blockhash(R), secret, e, address(this))) then the _revealRange walk) and, if it dislikes the result, calls setRevealCommit again with a fresh chain: the epoch is re-armed again. Repeating this selects the outcome of every token in the epoch (e.g. a molten-gold skin for a token in a wallet the owner controls). The signer is not needed, contradicting AUDIT_README 8.2 ('Owner + signer together can re-roll', implying one re-roll) and the contract comment at line 421 ('nobody (including the owner) can choose a token's skin'). Each repetition is visible (RevealCommitSet / EpochRearmed) and needs minting paused, but no on-chain rule bounds it. Fix (minimal, preserves the lost-chain recovery): bound replacements, e.g. allow one re-arm per epoch (require(epochRearmBlock[e] == 0) before moving it, otherwise revert 'EPOCH_ALREADY_REARMED') and/or enforce a minimum interval between commitment replacements (e.g. block.number >= lastRevealCommitChange + 7200) so a re-roll loop costs a day of paused minting per try; also state in the README that the owner alone can re-roll. Trade-off: a second genuinely lost chain for the same epoch would then need the interval to pass; that is the scope decision.","line":222,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.38;\n\nimport \"forge-std/Test.sol\";\nimport {EmberEvoGenesisCandidate} from \"Submission1/contracts/EmberEvoGenesisCandidate.sol\";\n\n/// Minimal 18-decimal ERC-20 used as the payment token (the constructor requires decimals() == 18).\ncontract Embeo {\n    string public name = \"EMBEO\"; string public symbol = \"EMBEO\"; uint8 public decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { totalSupply += a; balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Finding: the owner alone can re-roll the random outcome of a ready epoch as many times as it likes.\n/// setRevealCommit re-arms every ready unsealed epoch to block.number + 1. Once that block is mined the owner (who\n/// generated the replacement chain, so it knows the next secret) can compute the epoch's outcome off chain and, if it\n/// dislikes it, call setRevealCommit again with yet another chain: the epoch is re-armed again. Nothing bounds the\n/// repetitions, so the owner selects the outcome by rejection sampling. The signer is not needed (the owner supplies\n/// the commitment). Expected (fixed): a second replacement that would re-arm the same epoch again, two blocks later,\n/// is refused (one re-arm per epoch, or a minimum interval between commitment replacements).\ncontract RearmReroll is Test {\n    EmberEvoGenesisCandidate g;\n    Embeo token;\n    uint256 constant SIGNER_KEY = 0xA11CE;\n    address signer = vm.addr(SIGNER_KEY);\n    address minter = address(0xBEEF);\n    uint256 start;\n\n    function chain(bytes32 seed, uint256 n) internal pure returns (bytes32[] memory x) {\n        x = new bytes32[](n + 1); x[n] = seed;\n        for (uint256 i = n; i > 0; --i) x[i - 1] = keccak256(abi.encode(x[i]));\n    }\n\n    function setUp() public {\n        vm.roll(1000);\n        token = new Embeo();\n        g = new EmberEvoGenesisCandidate(address(this), address(token), signer, 1e18, 1, \"EEG\");\n        start = block.number;\n        g.configureSnapshot(bytes32(0), 0, block.timestamp, block.timestamp);\n        g.setRevealURIs(\"ipfs://pending\", \"ipfs://base/\", true);\n        bytes32[] memory x = chain(keccak256(\"signer seed\"), 8);\n        g.setRevealCommit(x[0]);\n        g.setMintPaused(false);\n        g.approvePaidPrice();\n        token.mint(minter, 100e18);\n        vm.prank(minter); token.approve(address(g), type(uint256).max);\n    }\n\n    function sign(bytes32 digest) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, digest);\n        return abi.encodePacked(r, s, v);\n    }\n\n    function domain() internal view returns (bytes32) {\n        return keccak256(abi.encode(keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"),\n            keccak256(\"EmberEvoGenesisCandidate\"), keccak256(\"6\"), block.chainid, address(g)));\n    }\n\n    function mintRandomPaid(bytes32 orderId) internal {\n        bytes32 lookKey = keccak256(\"look\");\n        uint256 deadline = block.timestamp + 100;\n        bytes32 appearance = keccak256(abi.encode(\n            keccak256(\"Appearance(address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,uint256 deadline,uint256 signerEpoch)\"),\n            minter, lookKey, uint8(3), keccak256(\"\"), deadline, g.signerEpoch()));\n        EmberEvoGenesisCandidate.PaidAuthorization memory o = EmberEvoGenesisCandidate.PaidAuthorization(\n            orderId, minter, lookKey, 3, keccak256(\"\"), g.BURN_ADDRESS(), g.paidCost(), g.paidGoldCost(), deadline, g.signerEpoch());\n        bytes32 paid = keccak256(abi.encode(\n            keccak256(\"PaidAuthorization(bytes32 orderId,address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,address recipient,uint256 embeoCost,uint256 goldCost,uint256 deadline,uint256 signerEpoch)\"), o));\n        bytes memory sig = sign(keccak256(abi.encodePacked(\"\\x19\\x01\", domain(), appearance)));\n        bytes memory gsig = sign(keccak256(abi.encodePacked(\"\\x19\\x01\", domain(), paid)));\n        bytes32[] memory none;\n        address burn = g.BURN_ADDRESS(); uint256 cost = g.paidCost();\n        vm.prank(minter);\n        g.mintPaid(lookKey, 3, \"\", deadline, sig, burn, cost, o, gsig, none);\n    }\n\n    function test_ownerCanRearmTheSameEpochAgainAndAgain() public {\n        // one random token in epoch 0\n        mintRandomPaid(keccak256(\"order-1\"));\n        assertEq(g.randomEpochCount(), 1);\n        uint256 seedBlock = g.epochSeedBlock(0);\n        assertEq(seedBlock, start + 25);\n\n        // epoch 0 becomes ready; the owner pauses and replaces the commitment with a chain only it knows\n        vm.roll(seedBlock + 1);\n        g.setMintPaused(true);\n        bytes32[] memory c1 = chain(keccak256(\"owner chain 1\"), 4);\n        g.setRevealCommit(c1[0]);\n        uint256 rearm1 = g.epochRearmBlock(0);\n        assertEq(rearm1, block.number + 1, \"first re-arm moves the seed block to the next block\");\n\n        // the re-armed seed block is mined: the owner now knows blockhash(rearm1) and secret c1[1], so it can compute\n        // epoch 0's draw off chain exactly as _seal/_revealRange would. Suppose it dislikes the result.\n        vm.roll(rearm1 + 1);\n        bytes32 outcome1 = keccak256(abi.encode(blockhash(rearm1), c1[1], uint256(0), address(g)));\n\n        // Expected (fixed): the same epoch cannot be re-armed a second time (or the replacement is rate-limited).\n        // Actual: the call succeeds, the seed block moves again, and the draw is re-rolled.\n        bytes32[] memory c2 = chain(keccak256(\"owner chain 2\"), 4);\n        vm.expectRevert();\n        g.setRevealCommit(c2[0]);\n\n        // (unreached on current code once expectRevert fails; kept to document the re-roll)\n        assertEq(g.epochRearmBlock(0), rearm1, \"seed block of epoch 0 must not move again\");\n        vm.roll(block.number + 2);\n        bytes32[] memory reveals = new bytes32[](1); reveals[0] = c1[1];\n        g.settle(reveals);\n        assertEq(g.epochSeed(0), outcome1 == bytes32(0) ? bytes32(uint256(1)) : outcome1);\n    }\n}","reproduction":"State: one random token minted in epoch 0 (seed block startBlock+25). At block startBlock+26 the owner calls setMintPaused(true) then setRevealCommit(c1[0]) where c1 is a chain only the owner knows: epochRearmBlock(0) == startBlock+27. At block startBlock+28 the owner computes seed1 = keccak256(abi.encode(blockhash(startBlock+27), c1[1], 0, address(this))) and the draw. Expected: a second setRevealCommit(c2[0]) that would re-arm epoch 0 again is refused (or rate-limited). Actual: it succeeds, epochRearmBlock(0) becomes startBlock+29 and the draw is re-rolled; this can be repeated indefinitely. Test test/scratch/RearmReroll.t.sol fails with 'next call did not revert as expected'.","severity":"medium","snippet":"            epochRearmBlock[e] = block.number + 1; ++rearmed; emit EpochRearmed(e, block.number + 1);","title":"Owner alone can re-roll a ready epoch's random outcome without limit via repeated setRevealCommit re-arms (rejection sampling)"},{"citation":"resolved","description":"_settleReady settles at most 8 epochs per call and then returns. _approval for a chosen skin (line 337) requires that no ready epoch remains, so when 9 epochs are ready the mint reverts after having sealed and revealed 8 of them, undoing that work and wasting the minter's gas. The off-chain signer caps GET /v1/reveals at MAX_REVEALS = 8, so the mint page can never supply enough secrets to get through in one transaction: every chosen-skin mint fails until someone calls settle() in a separate transaction. Nine ready epochs need only nine random mints (one per 25-block epoch) with no settle in between, which a single wallet can produce in 225 blocks while the collection is quiet, or which happens naturally when the signer service is down for 40 minutes and then recovers. Random mints are unaffected. Fix: let _settleReady continue while secrets are still supplied and gas allows (e.g. bound by gasleft() instead of a fixed 8, or raise the bound to match the gas analysis), or let the mint page call settle() first when more than 8 epochs are ready; at minimum raise MAX_REVEALS in the signer in step with the contract bound.","line":429,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.38;\n\nimport \"forge-std/Test.sol\";\nimport {EmberEvoGenesisCandidate} from \"Submission1/contracts/EmberEvoGenesisCandidate.sol\";\n\ncontract Embeo {\n    string public name = \"EMBEO\"; string public symbol = \"EMBEO\"; uint8 public decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { totalSupply += a; balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Finding: when nine or more epochs are ready, a chosen-skin mint reverts with REVEAL_PENDING even though it carries\n/// every secret: _settleReady stops after 8 epochs, _approval then sees the 9th ready epoch and reverts the whole\n/// transaction, undoing the 8 settlements it just performed. One random mint per epoch (9 tokens) with no settle in\n/// between is enough; every chosen-skin mint then fails until someone calls settle() separately.\n/// Expected (fixed): a chosen-skin mint that supplies all pending secrets succeeds (settle more than 8 when the\n/// secrets are supplied, or let the mint proceed once its own settlement made progress and nothing ready remains).\ncontract NineEpochs is Test {\n    EmberEvoGenesisCandidate g;\n    Embeo token;\n    uint256 constant SIGNER_KEY = 0xA11CE;\n    address signer = vm.addr(SIGNER_KEY);\n    address minter = address(0xBEEF);\n    uint256 start;\n    bytes32[] x;\n\n    function chain(bytes32 seed, uint256 n) internal pure returns (bytes32[] memory c) {\n        c = new bytes32[](n + 1); c[n] = seed;\n        for (uint256 i = n; i > 0; --i) c[i - 1] = keccak256(abi.encode(c[i]));\n    }\n\n    function setUp() public {\n        vm.roll(1000);\n        token = new Embeo();\n        g = new EmberEvoGenesisCandidate(address(this), address(token), signer, 1e18, 1, \"EEG\");\n        start = block.number;\n        g.configureSnapshot(bytes32(0), 0, block.timestamp, block.timestamp);\n        g.setRevealURIs(\"ipfs://pending\", \"ipfs://base/\", true);\n        x = chain(keccak256(\"signer seed\"), 16);\n        g.setRevealCommit(x[0]);\n        g.setMintPaused(false);\n        g.approvePaidPrice();\n        token.mint(minter, 100e18);\n        vm.prank(minter); token.approve(address(g), type(uint256).max);\n    }\n\n    function sign(bytes32 digest) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, digest);\n        return abi.encodePacked(r, s, v);\n    }\n\n    function domain() internal view returns (bytes32) {\n        return keccak256(abi.encode(keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"),\n            keccak256(\"EmberEvoGenesisCandidate\"), keccak256(\"6\"), block.chainid, address(g)));\n    }\n\n    function mintPaid(bytes32 orderId, uint8 choice, string memory uri, bytes32[] memory reveals) internal {\n        bytes32 lookKey = keccak256(\"look\");\n        uint256 deadline = block.timestamp + 100;\n        bytes32 appearance = keccak256(abi.encode(\n            keccak256(\"Appearance(address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,uint256 deadline,uint256 signerEpoch)\"),\n            minter, lookKey, choice, keccak256(bytes(uri)), deadline, g.signerEpoch()));\n        EmberEvoGenesisCandidate.PaidAuthorization memory o = EmberEvoGenesisCandidate.PaidAuthorization(\n            orderId, minter, lookKey, choice, keccak256(bytes(uri)), g.BURN_ADDRESS(), g.paidCost(), g.paidGoldCost(), deadline, g.signerEpoch());\n        bytes32 paid = keccak256(abi.encode(\n            keccak256(\"PaidAuthorization(bytes32 orderId,address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,address recipient,uint256 embeoCost,uint256 goldCost,uint256 deadline,uint256 signerEpoch)\"), o));\n        bytes memory sig = sign(keccak256(abi.encodePacked(\"\\x19\\x01\", domain(), appearance)));\n        bytes memory gsig = sign(keccak256(abi.encodePacked(\"\\x19\\x01\", domain(), paid)));\n        address burn = g.BURN_ADDRESS(); uint256 cost = g.paidCost();\n        vm.prank(minter);\n        g.mintPaid(lookKey, choice, uri, deadline, sig, burn, cost, o, gsig, reveals);\n    }\n\n    function test_chosenMintWithAllNineSecretsSucceeds() public {\n        bytes32[] memory none;\n        // one random token in each of epochs 0..8, nobody settles in between\n        for (uint256 e; e < 9; ++e) {\n            vm.roll(start + e * 25 + 1);\n            mintPaid(keccak256(abi.encode(\"order\", e)), 3, \"\", none);\n        }\n        assertEq(g.randomEpochCount(), 9);\n        // every one of the nine epochs is ready\n        vm.roll(g.epochSeedBlock(8) + 1);\n        (, , bool ready) = g.nextRevealEpoch();\n        assertTrue(ready);\n\n        // the chosen-skin minter supplies all nine committed secrets, in order\n        bytes32[] memory reveals = new bytes32[](9);\n        for (uint256 i; i < 9; ++i) reveals[i] = x[i + 1];\n\n        // Expected: the mint settles the nine epochs and succeeds. Actual: REVEAL_PENDING after settling eight.\n        mintPaid(keccak256(\"chosen\"), 0, \"ipfs://base/look/0\", reveals);\n        assertEq(g.nextRevealIndex(), 9);\n        assertEq(g.pendingRandom(), 0);\n        assertEq(g.totalMinted(), 10);\n    }\n}","reproduction":"Mint one random token (choice 3) in each of epochs 0..8 (blocks startBlock+25e+1), no settle in between; roll to epochSeedBlock(8)+1 so all nine are ready. Call mintPaid/mintFree with choice 0, a non-empty uri and reveals = [x1..x9] (the nine committed secrets in chain order). Expected: the nine epochs are settled and the chosen mint succeeds. Actual: revert REVEAL_PENDING (nextRevealIndex stays 0 because the whole transaction reverts). Test test/scratch/NineEpochs.t.sol fails with REVEAL_PENDING.","severity":"low","snippet":"        for (uint256 guard; guard < 8; ++guard) {","title":"With nine or more ready epochs a chosen-skin mint reverts with REVEAL_PENDING even when it carries every secret; the 8 settlements it performed are undone"},{"citation":"resolved","description":"_blockHash falls back to the EIP-2935 history contract and, when the seed block is older than 8,191 blocks, to zero. The seed then becomes keccak256(abi.encode(bytes32(0), secret, e, address(this))), fully determined by the secret. Anyone holding the secret can compute both outcomes (hash-based and zero-based) and pick by sealing before or after block seedBlock+8191. The README (8.2) documents this for the signer. It also applies to a minter: GET /v1/reveals hands the secret to anyone once the seed block is mined, and nothing in the signer or book auto-settles, so if no other party calls settle/sealEpoch for ~27 hours (quiet period, or the minter is the only active wallet), a minter with random tokens in the epoch chooses the better of two draws by waiting. Chosen-skin mints revert during that time, so it is visible but not prevented. Fix options: have the signer service (or any keeper) submit settle() itself before the window closes; or on expiry refuse to seal (require h != 0) and require the owner to re-arm via setRevealCommit, which removes the choice at the cost of an owner action; or shorten the window and make the fallback explicit in the README for minters as well.","line":455,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"State: epoch 0 holds a random token of wallet A; seed block S = startBlock+25 is mined; the signer publishes x1 at block S+1; nobody else transacts. Wallet A computes o1 = keccak256(abi.encode(blockhash(S), x1, 0, this)) and o2 = keccak256(abi.encode(bytes32(0), x1, 0, this)) and the resulting draw for its token under each. If o1 gives the wanted skin it calls settle([x1]) now; otherwise it waits until block.number > S + 8191 (blockhash and the EIP-2935 contract both return nothing) and calls settle([x1]) then: EpochSealed is emitted with blockHash == 0 and seed == o2. Expected: one outcome fixed at S; actual: the secret holder picks one of two.","severity":"low","snippet":"        if (ok && r.length == 32) h = abi.decode(r, (bytes32));","title":"Expired seed block gives whoever holds the next secret a binary choice over the epoch outcome (signer always; a lone minter when nobody else settles)"},{"citation":"resolved","description":"readyReveals compares the seed block against provider.getBlockNumber() (the latest head) and returns the secret the moment the seed block exists at the head. The commit-reveal security argument ('whoever knows the secret cannot change that block's hash') assumes the seed block is final. If the seed block is reorged out after the secret is public (single-block reorgs still occur on Ethereum; a proposer can also deliberately orphan a late block), the proposer building the replacement block at the same height knows the secret and can choose among candidate block contents to pick the hash, and therefore the draws of that epoch, for tokens it holds. The on-chain readiness check (block.number > seedBlock) is only one block, so the contract does not protect against this either. Fix: release secrets only once the seed block has k confirmations (k = 2..6) or is reported as 'safe' by the node, and make the contract's readiness check match (block.number > epochSeedBlock(e) + k) so chosen-skin mints are not blocked longer than the signer waits; document the residual risk.","line":39,"path":"Submission1/offchain/signer/reveal-chain.mjs","reproduction":"State: epoch e has a random token; its seed block N is proposed at slot s and appears at head. GET /v1/reveals immediately returns x_k. The proposer of slot s+1 builds on N-1 instead of N (orphaning N), reads x_k from the API, and computes for each candidate body of its block the resulting hash H and seed keccak256(abi.encode(H, x_k, e, contract)) and the draw for its own token in epoch e; it publishes the candidate whose draw it prefers, which becomes block N. Expected: the hash of N is fixed before anyone knows x_k. Actual: x_k is public while N can still change.","severity":"low","snippet":"  if(Number(seedBlock)>latest)break;         // seed block not mined yet: its secret stays with the signer","title":"Signer releases a reveal secret as soon as the seed block is the chain head, so a reorg of that block lets its replacement proposer grind the epoch outcome"},{"citation":"resolved","description":"genesisAuthDocument records domain {name:'EmberEvoGenesisCandidate', version:'5', chainId, verifyingContract} with every hold, while the contract's EIP712 constructor (line 148) and the signer (free-approval.mjs DOMAIN_VERSION='6') use version '6'. Settlement does not depend on this field (it reads finalized logs), so there is no fund impact, but any downstream check that recomputes the PaidAuthorization digest from the stored document (world-gold-result.ts is referenced but not in the package) would never match the on-chain signature, and the stored record misdescribes what the wallet's mint was authorized under. Fix: take the domain version from the pinned deployment registry entry (next to codeHash) instead of a constant, or update the constant to '6' and re-check before mainnet when the final domain is set (README section 7).","line":13,"path":"Submission1/offchain/gold-book/server/genesis-gold.ts","reproduction":"Call reserveGenesisGold for any order of the Sepolia deployment and read back the stored auth: auth.domain.version == '5'. Expected: '6', the version the contract at 0x8ce67008570ED41F64e4Be149ecAA66d1aBE2efa verifies signatures under (eip712Domain() returns version '6').","severity":"info","snippet":"export const GENESIS_R5_DOMAIN={name:'EmberEvoGenesisCandidate',version:'5'} as const;","title":"GOLD book stores paid-mint authorizations under EIP-712 domain version '5' while the deployed contract uses version '6'"}],"hash":"739c0929aae86786b738a9ea975de73b5ea616b92b415974a619c5933d2bbd0b","nodeId":"299e5140-c4ba-4015-b018-2cf741e11e49","outcome":"completed","summary":"The review is complete. Findings are in `.imd-findings.json` at the repository root (five entries, two with Foundry proofs that fail on the current code).\n\n**What I found**\n\n- **Medium. Owner alone can re-roll a ready epoch without limit.** Each `setRevealCommit` while paused re-arms every ready unsealed epoch to the next block. The owner generates its own chain, waits one block, computes the draw off chain, and if it dislikes it replaces the commitment again. Nothing bounds repetitions, and the signer is not needed, so the README's \"owner + signer, one re-roll\" understates the power and the contract comment \"nobody (including the owner) can choose a token's skin\" does not hold. Proof: `test/scratch/RearmReroll.t.sol`. Fix: one re-arm per epoch and/or a minimum interval between commitment replacements.\n- **Low. Nine ready epochs block every chosen-skin mint.** The settle loop stops at 8, then the chosen path reverts with REVEAL_PENDING, undoing the 8 settlements. The signer also caps reveals at 8, so the mint page can never recover without a separate `settle()` call. Nine random mints over nine quiet epochs is enough. Proof: `test/scratch/NineEpochs.t.sol`.\n- **Low. Expired seed block gives the secret holder a binary choice.** After 8,191 blocks the hash becomes zero and the seed depends on the secret alone. The README documents this for the signer, but the secret is public via the API, so a minter who is the only active party for about 27 hours can also pick the better of two draws.\n- **Low (off-chain). Secrets are released when the seed block is merely at the head.** A reorg of that block after release lets the replacement proposer grind the epoch outcome. Recommend k confirmations in both the signer and the contract readiness check.\n- **Info (off-chain). GOLD book stores authorizations under EIP-712 domain version 5** while the contract and signer use version 6.\n\n**What held up under review**\n\n- Supply and caps: the sum-of-caps-equals-supply invariant guarantees every random token finds a skin. An extra invariant test (caps filled by chosen mints with randoms pending) passed.\n- Signatures: domain, wallet, choice, uriHash, deadline and signerEpoch are all bound. Free replay is stopped by `freeClaimed`, paid replay by the consumed orderId, rotation by the epoch. No nonce on Appearance, but no path abuses it.\n- Paid mint: exact burn enforced by balance and totalSupply checks, order consumed once, revoked orders rejected, all external calls behind `nonReentrant`. The transient marker is per caller as stated, so distinct sub-accounts are not limited, but each would still need its own signer-approved order.\n- Minter steering by timing, chosen mints around a reveal, or out-of-order sealing: blocked by the chain order and the REVEAL_PENDING guard. Front-running a mint's secrets with `settle` or `sealEpoch` does not break the mint.\n- Gas: 8 epochs of 25 tokens stays well under the block limit.\n\n**Limits.** Slither was not run. The Hardhat test suite was read, not executed. Tests ran in a scratch Foundry project outside the repository; `test/scratch/` holds the three test files and nothing else was written.","treeHash":null,"usage":{"cachedInputTokens":2041300,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":54206,"runtime":"claude","turns":29,"wallClockMs":914270}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"5c1e4764d4e20ef5","findings":[{"citation":"resolved","description":"setRevealCommit (owner, paused) accepts any non-zero commitment different from the current one and re-arms every unsealed epoch whose seed block is already mined to block.number + 1. Nothing records that an epoch was already re-armed, nothing binds the new chain to the approval signer (no SignerAcceptance-style countersignature as for signer rotation), and nothing limits how often the owner may replace the chain. The owner can therefore generate its own hash chain, re-arm, wait one block, compute the epoch's seed (keccak256(abi.encode(blockhash(rearm), secret, e, this))) and the _revealRange draws off chain, and if it dislikes the outcome call setRevealCommit again with a fresh chain: the same epoch is re-armed to a new block and rolled again. Rejection sampling over ~27 attempts (about one hour of paused minting, one transaction every two blocks) puts a chosen token on the 48/1288 molten-gold skin. This contradicts AUDIT_README section 6 property 4 / section 8.2 (re-roll needs 'Owner + signer together', implying one recovery) and the contract comment at line 421 ('nobody (including the owner) can choose a token's skin'). It is an owner power, visible through RevealCommitSet / EpochRearmed, and on mainnet the owner is a multisig, but the audit package understates it. Reported by three specialists (economics, flow, permissions); merged. Fix (keeps the lost-chain recovery): refuse to re-arm an epoch whose epochRearmBlock is already set (require(epochRearmBlock[e] == 0, 'EPOCH_ALREADY_REARMED')) and/or require the new commitment to be countersigned by the appearance signer with an EIP-712 acceptance checked like activateAppearanceSigner, so neither party alone can replace the chain; update README 8.2 to describe the actual power.","line":222,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.38;\n\nimport \"forge-std/Test.sol\";\nimport {EmberEvoGenesisCandidate} from \"Submission1/contracts/EmberEvoGenesisCandidate.sol\";\n\ncontract MockEmbeo {\n    string public name = \"T\"; string public symbol = \"T\"; uint8 public decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; totalSupply += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Finding: the owner alone can re-arm the same ready, unsealed epoch again and again through setRevealCommit,\n/// each time with a commitment of its own choosing, and so pick the best of many random outcomes for that epoch.\n/// Fails on the current code (the second re-arm of epoch 0 succeeds); passes once an epoch can be re-armed at most once.\ncontract OwnerRerollProof is Test {\n    EmberEvoGenesisCandidate g;\n    MockEmbeo token;\n    address owner = address(0xA11CE);\n    uint256 signerKey = 0xB0B;\n    uint256 constant COST = 1e18;\n    uint256 constant GOLD = 5;\n    bytes32 constant APPEARANCE_TYPEHASH = keccak256(\"Appearance(address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,uint256 deadline,uint256 signerEpoch)\");\n    bytes32 constant PAID_AUTH_TYPEHASH = keccak256(\"PaidAuthorization(bytes32 orderId,address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,address recipient,uint256 embeoCost,uint256 goldCost,uint256 deadline,uint256 signerEpoch)\");\n\n    function setUp() public {\n        token = new MockEmbeo();\n        vm.roll(100);\n        g = new EmberEvoGenesisCandidate(owner, address(token), vm.addr(signerKey), COST, GOLD, \"EEG\");\n        bytes32 x1 = keccak256(\"signer-secret-1\");\n        vm.startPrank(owner);\n        g.configureSnapshot(bytes32(0), 0, block.timestamp, block.timestamp);\n        g.setRevealURIs(\"ipfs://pending\", \"ipfs://base/\", true);\n        g.setRevealCommit(keccak256(abi.encode(x1)));   // the signer's chain: x0 = keccak(x1)\n        g.setMintPaused(false);\n        g.approvePaidPrice();\n        vm.stopPrank();\n    }\n\n    function sign(bytes32 structHash) internal view returns (bytes memory) {\n        bytes32 dom = keccak256(abi.encode(keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"),\n            keccak256(\"EmberEvoGenesisCandidate\"), keccak256(\"6\"), block.chainid, address(g)));\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, keccak256(abi.encodePacked(\"\\x19\\x01\", dom, structHash)));\n        return abi.encodePacked(r, s, v);\n    }\n\n    function mintRandom(address w) internal {\n        token.mint(w, COST);\n        vm.prank(w); token.approve(address(g), COST);\n        bytes32 lookKey = keccak256(\"look\");\n        uint256 deadline = block.timestamp + 100;\n        bytes memory sig = sign(keccak256(abi.encode(APPEARANCE_TYPEHASH, w, lookKey, uint8(3), keccak256(bytes(\"\")), deadline, uint256(1))));\n        EmberEvoGenesisCandidate.PaidAuthorization memory o = EmberEvoGenesisCandidate.PaidAuthorization({\n            orderId: keccak256(\"order-1\"), wallet: w, lookKey: lookKey, choice: 3, uriHash: keccak256(bytes(\"\")),\n            recipient: 0x000000000000000000000000000000000000dEaD, embeoCost: COST, goldCost: GOLD, deadline: deadline, signerEpoch: 1});\n        bytes memory gsig = sign(keccak256(abi.encode(PAID_AUTH_TYPEHASH, o)));\n        bytes32[] memory none;\n        vm.prank(w);\n        g.mintPaid(lookKey, 3, \"\", deadline, sig, 0x000000000000000000000000000000000000dEaD, COST, o, gsig, none);\n    }\n\n    function test_owner_cannot_rearm_the_same_epoch_twice() public {\n        vm.roll(101);\n        mintRandom(address(0x1111));                 // one random token in epoch 0; seed block = 125\n        assertEq(g.epochSeedBlock(0), 125);\n        vm.roll(126);                                // epoch 0 is ready; the signer's secret x1 is public, nobody sealed it\n        vm.startPrank(owner);\n        g.setMintPaused(true);\n        // First replacement: a legitimate recovery re-arms epoch 0 to block 127 (secrets fixed before that hash exists).\n        g.setRevealCommit(keccak256(abi.encode(keccak256(\"owner-secret-A\"))));\n        assertEq(g.epochSeedBlock(0), 127);\n        vm.roll(128);                                // block 127 is mined: with secret A the owner can compute epoch 0's outcome\n        // Second replacement of an epoch that was already re-armed: must be refused, otherwise the owner re-rolls\n        // the outcome as often as it likes (every two blocks) and keeps the roll it prefers.\n        vm.expectRevert();\n        g.setRevealCommit(keccak256(abi.encode(keccak256(\"owner-secret-B\"))));\n        vm.stopPrank();\n        assertEq(g.epochSeedBlock(0), 127, \"epoch 0 must keep its single re-armed seed block\");\n    }\n}","reproduction":"State: deploy at block 100, one random token minted in epoch 0 (seed block 125), block 126 mined, epoch 0 ready and unsealed. Owner: setMintPaused(true); setRevealCommit(keccak256(abi.encode(A))) -> EpochRearmed(0, 127). Owner computes epoch 0's outcome with secret A and blockhash(127) at block 128. Owner: setRevealCommit(keccak256(abi.encode(B))) at block 128. Expected: refused (one re-arm per epoch, or signer countersignature required). Actual: accepted, EpochRearmed(0, 129), epochRearmBlock(0) == 129; repeatable without bound. Ran the attached test: FAIL 'next call did not revert as expected' on the current code (also Proof_540678224909 from the permissions specialist fails identically).","severity":"medium","snippet":"            epochRearmBlock[e] = block.number + 1; ++rearmed; emit EpochRearmed(e, block.number + 1);","title":"Owner alone can re-roll a ready random epoch any number of times through repeated setRevealCommit re-arms (README 8.2 says owner + signer, once)"},{"citation":"resolved","description":"createGenesisR5Reader.read() proves consumption from the receipt of the transaction that emitted PaidOrderConsumed. Line 61 rejects the receipt unless receipt.to is the Genesis contract, and line 67 (minted.length!==1, where minted is every GenesisMinted log of the contract in the receipt, not filtered by tokenId) rejects it unless exactly one token was minted in the whole transaction. Both hold only for a direct EOA call. The contract accepts contract callers (it uses _safeMint and ERC-1271 via SignatureChecker, and the transient marker comment at line 374 is explicitly about smart wallets), and the paid signer issues orders to any wallet address with no EOA check (paid-approval.mjs). A Safe execTransaction, an ERC-4337 bundle (receipt.to = EntryPoint), or an EIP-7702 batch (receipt.to = the EOA; two GenesisMinted logs if the batch also does a mintFree, which the per-sender marker does not stop) makes read() throw MINT_PROOF_CONFLICT. reconcileHeldMintGold (world-gold.ts lines 184-188) only settles when read() returns, so the hold is neither consumed nor released, is retried by every reconciliation run forever, and the wallet's GOLD stays reserved although the token was minted and the EMBEO burned. goldAdminCommand has no action that settles or releases a mint hold ('repair-topup' and 'refund-service' only). Fix: drop the receipt.to requirement (the log's address is already checked), select the GenesisMinted log whose topics[1] == wallet and topics[2] == tokenId instead of requiring exactly one, or prove consumption from the PaidOrderConsumed log (which already carries wallet, tokenId and goldCost) plus the eth_call of paidOrderTokenId.","line":61,"path":"Submission1/offchain/gold-book/server/genesis-gold-reader.ts","reproduction":"State: paid order O for wallet W (a Safe or any contract implementing onERC721Received), GOLD held in the book. W executes mintPaid(...) through its own execTransaction; the mint succeeds (PaidOrderConsumed(O, W, 7), GenesisMinted(W, 7, ...), EMBEO burned). Reader: paidOrderTokenId(O) at the finalized block == 7 -> eth_getLogs finds the log -> eth_getTransactionReceipt -> String(receipt.to).toLowerCase() is W, not the Genesis contract -> fail('MINT_PROOF_CONFLICT') at line 61. reconcileHeldMintGold records {id: O, status: 'MINT_PROOF_CONFLICT'}; world_gold_mints.state stays 'held' and the wallet's reserved GOLD never decreases. Same with an ERC-4337 EntryPoint.handleOps bundle carrying two users' mintPaid userOps: receipt.to = EntryPoint, and even with the to-check removed minted.length == 2 at line 67 -> both orders stuck. Expected: consumption proven by PaidOrderConsumed(orderId, wallet, tokenId) and the matching GenesisMinted(wallet, tokenId) regardless of the outer transaction target. Verified by reading the reader and world-gold.ts; the on-chain part (a contract wallet's mintPaid succeeds) is shown by test_twoWalletsTwoPaidMintsOneTransaction in test/scratch/Judge.t.sol.","severity":"medium","snippet":"   if(qty(receipt.status)!==1n||String(receipt.to).toLowerCase()!==contract||rBlock>finNumber)fail('MINT_PROOF_CONFLICT');","title":"GOLD book never settles a paid mint sent through a smart-contract wallet or a bundled transaction: receipt.to must equal the Genesis contract and the receipt must hold exactly one GenesisMinted"},{"citation":"resolved","description":"_settleReady settles at most 8 epochs per call and then returns; _approval for a chosen skin (line 337) requires that no ready epoch remains. When 9 epochs are ready the mint seals and reveals 8 of them and then reverts, undoing that work. The off-chain signer caps GET /v1/reveals at MAX_REVEALS = 8 (reveal-chain.mjs line 11), so the mint page can never supply enough secrets to get through in one transaction: every chosen-skin mint fails until someone calls settle() in a separate transaction, which the README flow (sections 5 and 8.3: fetch fresh secrets and retry) never does. Nine ready epochs need only nine random mints, one per 25-block epoch, with no settle in between: one wallet can create the state deliberately in about 225 blocks while the collection is quiet, or it happens when the signer service is down for about 40 minutes. Random mints are unaffected, so this is availability of chosen-skin minting, not a safety break. Reported by economics (info), flow, permissions and math; merged. Fix: let _settleReady continue while secrets are supplied (bound by gasleft() or raise the fixed bound in step with the gas measurement), raise MAX_REVEALS in the signer accordingly, and have the mint page call settle() first when more than 8 epochs are ready.","line":429,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.38;\n\nimport \"forge-std/Test.sol\";\nimport {EmberEvoGenesisCandidate} from \"Submission1/contracts/EmberEvoGenesisCandidate.sol\";\n\ncontract Embeo {\n    string public name = \"EMBEO\"; string public symbol = \"EMBEO\"; uint8 public decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { totalSupply += a; balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// Finding: when nine or more epochs are ready, a chosen-skin mint reverts with REVEAL_PENDING even though it carries\n/// every secret: _settleReady stops after 8 epochs, _approval then sees the 9th ready epoch and reverts the whole\n/// transaction, undoing the 8 settlements it just performed. One random mint per epoch (9 tokens) with no settle in\n/// between is enough; every chosen-skin mint then fails until someone calls settle() separately.\n/// Expected (fixed): a chosen-skin mint that supplies all pending secrets succeeds (settle more than 8 when the\n/// secrets are supplied, or let the mint proceed once its own settlement made progress and nothing ready remains).\ncontract NineEpochs is Test {\n    EmberEvoGenesisCandidate g;\n    Embeo token;\n    uint256 constant SIGNER_KEY = 0xA11CE;\n    address signer = vm.addr(SIGNER_KEY);\n    address minter = address(0xBEEF);\n    uint256 start;\n    bytes32[] x;\n\n    function chain(bytes32 seed, uint256 n) internal pure returns (bytes32[] memory c) {\n        c = new bytes32[](n + 1); c[n] = seed;\n        for (uint256 i = n; i > 0; --i) c[i - 1] = keccak256(abi.encode(c[i]));\n    }\n\n    function setUp() public {\n        vm.roll(1000);\n        token = new Embeo();\n        g = new EmberEvoGenesisCandidate(address(this), address(token), signer, 1e18, 1, \"EEG\");\n        start = block.number;\n        g.configureSnapshot(bytes32(0), 0, block.timestamp, block.timestamp);\n        g.setRevealURIs(\"ipfs://pending\", \"ipfs://base/\", true);\n        x = chain(keccak256(\"signer seed\"), 16);\n        g.setRevealCommit(x[0]);\n        g.setMintPaused(false);\n        g.approvePaidPrice();\n        token.mint(minter, 100e18);\n        vm.prank(minter); token.approve(address(g), type(uint256).max);\n    }\n\n    function sign(bytes32 digest) internal view returns (bytes memory) {\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_KEY, digest);\n        return abi.encodePacked(r, s, v);\n    }\n\n    function domain() internal view returns (bytes32) {\n        return keccak256(abi.encode(keccak256(\"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)\"),\n            keccak256(\"EmberEvoGenesisCandidate\"), keccak256(\"6\"), block.chainid, address(g)));\n    }\n\n    function mintPaid(bytes32 orderId, uint8 choice, string memory uri, bytes32[] memory reveals) internal {\n        bytes32 lookKey = keccak256(\"look\");\n        uint256 deadline = block.timestamp + 100;\n        bytes32 appearance = keccak256(abi.encode(\n            keccak256(\"Appearance(address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,uint256 deadline,uint256 signerEpoch)\"),\n            minter, lookKey, choice, keccak256(bytes(uri)), deadline, g.signerEpoch()));\n        EmberEvoGenesisCandidate.PaidAuthorization memory o = EmberEvoGenesisCandidate.PaidAuthorization(\n            orderId, minter, lookKey, choice, keccak256(bytes(uri)), g.BURN_ADDRESS(), g.paidCost(), g.paidGoldCost(), deadline, g.signerEpoch());\n        bytes32 paid = keccak256(abi.encode(\n            keccak256(\"PaidAuthorization(bytes32 orderId,address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,address recipient,uint256 embeoCost,uint256 goldCost,uint256 deadline,uint256 signerEpoch)\"), o));\n        bytes memory sig = sign(keccak256(abi.encodePacked(\"\\x19\\x01\", domain(), appearance)));\n        bytes memory gsig = sign(keccak256(abi.encodePacked(\"\\x19\\x01\", domain(), paid)));\n        address burn = g.BURN_ADDRESS(); uint256 cost = g.paidCost();\n        vm.prank(minter);\n        g.mintPaid(lookKey, choice, uri, deadline, sig, burn, cost, o, gsig, reveals);\n    }\n\n    function test_chosenMintWithAllNineSecretsSucceeds() public {\n        bytes32[] memory none;\n        // one random token in each of epochs 0..8, nobody settles in between\n        for (uint256 e; e < 9; ++e) {\n            vm.roll(start + e * 25 + 1);\n            mintPaid(keccak256(abi.encode(\"order\", e)), 3, \"\", none);\n        }\n        assertEq(g.randomEpochCount(), 9);\n        // every one of the nine epochs is ready\n        vm.roll(g.epochSeedBlock(8) + 1);\n        (, , bool ready) = g.nextRevealEpoch();\n        assertTrue(ready);\n\n        // the chosen-skin minter supplies all nine committed secrets, in order\n        bytes32[] memory reveals = new bytes32[](9);\n        for (uint256 i; i < 9; ++i) reveals[i] = x[i + 1];\n\n        // Expected: the mint settles the nine epochs and succeeds. Actual: REVEAL_PENDING after settling eight.\n        mintPaid(keccak256(\"chosen\"), 0, \"ipfs://base/look/0\", reveals);\n        assertEq(g.nextRevealIndex(), 9);\n        assertEq(g.pendingRandom(), 0);\n        assertEq(g.totalMinted(), 10);\n    }\n}","reproduction":"Mint one random token (choice 3) in each of epochs 0..8 (blocks startBlock+25e+1), no settle in between; roll to epochSeedBlock(8)+1 so all nine are ready. Call mintPaid with choice 0, a non-empty uri and reveals = [x1..x9] in chain order. Expected: the nine epochs are settled and the chosen mint succeeds (nextRevealIndex == 9, pendingRandom == 0). Actual: revert REVEAL_PENDING; nextRevealIndex stays 0 because the whole transaction reverts. The attached test (from the permissions specialist) fails with REVEAL_PENDING on the current code.","severity":"low","snippet":"        for (uint256 guard; guard < 8; ++guard) {","title":"With nine or more ready epochs every chosen-skin mint reverts with REVEAL_PENDING even when it carries every secret; recovery needs a separate settle() the documented flow never sends"},{"citation":"resolved","description":"The commit-reveal argument (README section 5, reveal-chain.mjs header) is that whoever produces the seed block cannot know the secret and whoever knows the secret cannot change the block. readyReveals() breaks only when seedBlock > latest, so it hands out x[k] the moment the seed block is the chain head, with zero confirmations. On chain, readiness and the REVEAL_PENDING freeze use block.number > epochSeedBlock(e) (contract line 316), so transactions included in the seed block itself may still change skin counts (verified: a chosen mint at block.number == seedBlock succeeds). If the seed block S is reorged out after x[k] is public (from the API or a pending settle/mint in the mempool), the proposer of the replacement block at height S knows x[k], can evaluate keccak256(abi.encode(hash(S'), x[k], e, this)) for every candidate block it builds, can grind S' (ordering, extra data, included transactions) toward a preferred draw for a token it holds in epoch e, and can include its own chosen-skin mints in S' to move the remaining[] weights. Single-slot reorgs by the next proposer occur on mainnet; the attacker needs to be the proposer of that slot and hold a pending random token, so likelihood is low and the payoff is one rare skin, hence Low. Reported by economics, permissions and math; merged. Fix: in readyReveals require seedBlock + K <= latest (K = 2, or use the 'safe' tag) before releasing a secret; in the contract start the freeze in the seed block itself (block.number >= epochSeedBlock(e) in the REVEAL_PENDING check) and consider matching readiness to seedBlock + K so chosen mints are not blocked longer than the signer waits; document the residual risk in README 8.2.","line":39,"path":"Submission1/offchain/signer/reveal-chain.mjs","reproduction":"Off-chain: epoch e has a random token, S = epochSeedBlock(e); the signer's provider reports latest == S; readyReveals (line 39: Number(seedBlock) > latest is false) returns x[k]. On-chain part, test_chosenMintInSeedBlockAllowed in test/scratch/Judge.t.sol: one random token in epoch 0, vm.roll(epochSeedBlock(0)), nextRevealEpoch() returns seedKnown == false and a chosen mintPaid(choice 0) succeeds with skinCount[0] == 1; one block later seedKnown == true. Expected: the hash of S is fixed before anyone knows x[k] and nothing can change the counts in block S. Actual: x[k] is public while S can still be replaced, and a replacement block S' can both grind its hash and include chosen-skin mints.","severity":"low","snippet":"  if(Number(seedBlock)>latest)break;         // seed block not mined yet: its secret stays with the signer","title":"Signer releases an epoch's secret with zero confirmations on the seed block, and the contract still accepts chosen-skin mints inside the seed block itself: a one-block reorg lets the replacement propo"},{"citation":"resolved","description":"_blockHash returns blockhash(n) for the last 256 blocks, then the EIP-2935 history contract, else zero. A staticcall to an address with no code succeeds with empty return data, so on a chain without the Prague history contract (the Hardhat EDR Cancun chain the test suite uses, Foundry's default, some L2s and testnets) h == 0 for every seed block older than 256 blocks; on Sepolia and mainnet after 8,191 blocks. _seal then uses seed = keccak256(abi.encode(0, secret, e, this)), fully determined by the committed secret, which the signer service knows at deployment for every epoch. README 8.2 calls this 'one extra option for the signer'. It is wider than that: (a) the signer can tell a colluding minter before it mints which token id in which epoch will draw molten gold on the expiry path (remaining[] is frozen by REVEAL_PENDING while the epoch waits), at the cost of withholding the secret for ~27 h during which chosen mints revert (visible, not prevented); (b) GET /v1/reveals hands the secret to anyone once the seed block is mined and nothing auto-settles, so if no other party settles for ~27 h a minter holding a random token in that epoch picks the better of two draws by settling before or after the window; (c) none of the 78 tests exercise the history-contract path. Reported by flow, permissions and math (two entries); merged. Fix: when h == 0 in _seal do not seal; re-arm the epoch (epochRearmBlock[e] = block.number + 1, emit EpochRearmed) so an expired epoch always takes a hash unknown when its secret was fixed (anyone can settle two blocks later), or have the signer/keeper submit settle() itself before the window closes; require BLOCK_HASH_HISTORY.code.length != 0 in the constructor and add a test with the history contract etched.","line":455,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"test_expiredSeedBlockSecretOnly in test/scratch/Judge.t.sol (passes on the current code, demonstrating the behaviour): one random token in epoch 0; vm.roll(epochSeedBlock(0) + 300); BLOCK_HASH_HISTORY.code.length == 0; sealEpoch(0, x1). Expected per the commit-reveal intent: the seed depends on a block hash unknown when the chain was committed. Actual: epochSeed(0) == keccak256(abi.encode(bytes32(0), x1, uint256(0), address(g))), computable by the signer at deployment; EpochSealed emitted with blockHash == 0. On mainnet the same state is reached by waiting until block.number > seedBlock + 8191; the secret holder chooses between the pre-expiry draw (settle now) and this one (settle later).","severity":"low","snippet":"        if (ok && r.length == 32) h = abi.decode(r, (bytes32));","title":"Expired seed block degrades the seed to secret-only: the signer knows every such outcome in advance, a secret holder gets a binary choice by waiting, and on any chain without the EIP-2935 contract thi"},{"citation":"resolved","description":"Every secret that sealed an epoch is public (calldata, EpochSealed event) and the original anchor x[0] is in RevealCommitSet. setRevealCommit rejects only zero and the current value. If the owner re-runs the setup tooling with the previous REVEAL_SEED (deploy-testnet.mjs / new-reveal-seed.mjs have no on-chain guard) or sets any earlier chain value, the re-armed epoch needs a preimage everyone already has: the signer is no longer the gate, the re-arm block's proposer knows the secret in advance and can grind the hash, and any user can seal the epoch as soon as the re-arm block is mined. The contract can refuse this cheaply. Reported by math; the economics specialist noted the same weakness inside the re-roll finding. Fix: record every commitment the contract has held or opened (mapping(bytes32 => bool) seenCommit, set in setRevealCommit and in _seal for the consumed secret) and require !seenCommit[commit] in setRevealCommit.","line":214,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"test_setRevealCommitAcceptsOpenedCommitment in test/scratch/Judge.t.sol (passes on the current code, demonstrating the behaviour): mint one random token in epoch 0, settle it with x1 (x1 now public, revealCommit == x1), mint another random token in epoch 2, roll past its seed block, setMintPaused(true), setRevealCommit(x0). Expected: REVEAL_COMMIT_INVALID (x0 was already opened). Actual: accepted, epochRearmBlock(2) == block.number + 1, and two blocks later sealEpoch(2, x1) succeeds with the public secret x1 (epochSeed(2) != 0).","severity":"low","snippet":"        require(commit != bytes32(0) && commit != revealCommit, \"REVEAL_COMMIT_INVALID\");","title":"setRevealCommit accepts a commitment that was already opened (for example the original anchor x0), so a re-armed epoch can be sealed by anyone with a secret that is already public"},{"citation":"resolved","description":"Random mints are allowed while a reveal is pending (REVEAL_PENDING only stops chosen-skin mints), so while the signer is not publishing secrets (service down, key or seed lost) every 25-block epoch can still gain random tokens, each adding one unsealed epoch to randomEpochs. setRevealCommit, the only recovery for a lost or compromised chain, then re-arms all of them in one call (SLOADs of randomEpochs[i], epochSeed[e], epochRearmBlock[e], one SSTORE and one event per epoch). Measured: 8,623,569 gas for 300 ready one-token epochs, about 28.7k per epoch; at the 1,288-token maximum (one random token per epoch, about 5.4 days of a silent signer) the call needs about 37M gas, above a 36M block gas limit, so the owner can never replace the chain and every pending random token stays unrevealed (skin 0, pendingURI) for good. Minters are not otherwise harmed, which keeps this Low, but it defeats the designed recovery exactly when it is needed. Reported by economics. Fix: do not re-arm in a loop. Store a single rearmFromBlock = block.number + 1 together with rearmFromIndex = nextRevealIndex and have epochSeedBlock(e) return max(natural seed block, rearmFromBlock) for unsealed epochs at or after that index; or bound the loop with a persisted cursor and let the owner call setRevealCommit repeatedly.","line":218,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"test_gasSetRevealCommitManyEpochs in test/scratch/Judge.t.sol: one random mint per epoch for 300 epochs with no settle, roll past the last seed block, setMintPaused(true), setRevealCommit(keccak256('new')). Measured 8,623,569 gas (log 'gas setRevealCommit with 300 ready epochs'); epochRearmBlock(299) == block.number + 1. Expected: recovery cost independent of the number of pending epochs. Actual: linear, about 28.7k per epoch, exceeding the block gas limit beyond roughly 1,250 pending epochs.","severity":"low","snippet":"        for (uint256 i = nextRevealIndex; i < randomEpochs.length; ++i) {","title":"setRevealCommit re-arms every ready unsealed epoch in one unbounded loop (~28.7k gas each): with enough one-token epochs the lost-chain recovery no longer fits in a block"},{"citation":"resolved","description":"The marker slot is derived from msg.sender and never cleared, so inside one transaction the same caller cannot mintPaid twice (also across sequential calls from a Safe batch or an EIP-7702 EOA; a smart-wallet batch with two paid mints reverts as a whole). It does not stop (a) mintFree followed by mintPaid by the same wallet in one batch (two GenesisMinted logs in one receipt, which the GOLD reader rejects, see the reader finding); (b) one outer transaction driving N distinct contract wallets each holding its own signer-issued order (the paid signer issues orders to any address, no EOA or allowlist check); (c) ERC-4337 bundles, where every userOp has a different msg.sender. The README says there is no per-wallet total limit across transactions, so the marker only shapes transaction composition; it must not be relied on as a rate or supply limit, and the off-chain reader must not assume one mint per receipt. Answers README 8.7. Reported by economics and flow; merged. No contract change needed if the limit is understood as per-sender; otherwise key the slot on a constant (one paid mint per transaction for everyone) and accept that 4337 bundles then serialize paid mints, and state the batching behaviour for wallet integrations.","line":376,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"test_twoWalletsTwoPaidMintsOneTransaction in test/scratch/Judge.t.sol: two minimal contract wallets A and B (forwarding call + onERC721Received) with EMBEO and approvals and one signer-issued order each; from the test contract call A.exec(mintPaid(orderA)) then B.exec(mintPaid(orderB)) in one transaction. Actual: both succeed, totalMinted == 2; a second mintPaid through A in the same transaction reverts ONE_PAID_MINT_PER_TRANSACTION. Expected per the README 8.7 wording 'one paid mint per transaction': only one.","severity":"info","snippet":"        bytes32 slot = keccak256(abi.encode(PAID_TX_SLOT_PREFIX, msg.sender));","title":"Transient one-paid-mint-per-transaction marker is keyed on msg.sender only: N contract wallets (or a 4337 bundle) can do N paid mints in one transaction, and a free+paid pair by one wallet is not limi"},{"citation":"resolved","description":"The Appearance struct binds wallet, lookKey, choice, uriHash, deadline, signerEpoch, chain id and contract, but has no nonce or route byte and the digest is not marked used. Today this is harmless: a second mintFree by the same wallet is stopped by freeClaimed, and a second mintPaid needs a fresh PaidAuthorization (orderId consumed once). But an Appearance issued for a paid order is also valid for mintFree, and for every later PaidAuthorization of that wallet with the same look/uri until the deadline. If either gate is relaxed later (several free claims per wallet, an Appearance-only paid path) the missing nonce becomes a replay. Reported by flow. Fix: add a route byte (free/paid) and a per-wallet nonce to Appearance, or mark the digest used in mintFree.","line":342,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"test_appearanceReusedAcrossRoutes in test/scratch/Judge.t.sol: snapshot with wallet W (single-leaf tree, root == leaf), one Appearance A = (W, look L, choice 0, uri 'u', deadline D, epoch 1). W calls mintFree(L, 0, 'u', D, A, [], []) -> succeeds. W then calls mintPaid(L, 0, 'u', D, A, BURN, cost, order O1, goldSig, []) with the same A -> succeeds; totalMinted == 2. Expected per README property 3 (one approval, one mint): the second use of A is refused. Actual: one Appearance, two mints, bounded only by freeClaimed and the paid order.","severity":"info","snippet":"        bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(APPEARANCE_TYPEHASH, msg.sender, lookKey, choice, keccak256(bytes(uri)), deadline, signerEpoch)));","title":"Appearance signature has no nonce and is never consumed: the same signature is accepted by mintFree and mintPaid and can be reused with every later PaidAuthorization of the wallet for the same look un"},{"citation":"resolved","description":"genesisAuthDocument writes domain {name, version:'5', chainId, verifyingContract} into every stored hold (auth_json). The deployed contract's EIP712 domain is ('EmberEvoGenesisCandidate', '6') (constructor, contract line 148) and the signer's DOMAIN_VERSION is '6' (free-approval.mjs line 7). The stored document is only used for byte-identical retry comparison and binding, so there is no live mismatch, but anything that later recomputes the PaidAuthorization digest from the stored document (audit replay, dispute tooling) derives a digest the contract never verified, and the mainnet domain change announced in README 7 would leave the constant stale again. Reported by economics, flow and permissions; merged. Fix: set the version to '6' or source it from the GenesisDeployment registry entry next to codeHash, and bump GENESIS_R5_AUTH_SCHEMA.","line":13,"path":"Submission1/offchain/gold-book/server/genesis-gold.ts","reproduction":"Compute _hashTypedDataV4(PaidAuthorization) for any stored order with the stored domain (version '5') and compare with the digest the contract verified (version '6', eip712Domain() on 0x8ce67008570ED41F64e4Be149ecAA66d1aBE2efa): they differ. Expected: identical. Confirmed by reading genesis-gold.ts line 13 against the contract constructor at line 148 and free-approval.mjs line 7.","severity":"info","snippet":"export const GENESIS_R5_DOMAIN={name:'EmberEvoGenesisCandidate',version:'5'} as const;","title":"GOLD book stores the Genesis EIP-712 domain as version '5' while the deployed contract and the signer use version '6'"},{"citation":"resolved","description":"The signer derives exactly 2,048 secrets and the comment claims this is >= maxSupply. The contract allows configureSupply up to SUPPLY_CEILING = 2222 (line 36, checked in _setSupply at line 187) before minting opens, and each epoch holding at least one random token consumes one secret, so more than 2,048 random epochs is reachable with a larger supply. After the 2,048th seal chain.next(commit) returns null, readyReveals throws REVEAL_CHAIN_MISMATCH (503), the next ready epoch cannot be sealed and every chosen-skin mint reverts with REVEAL_PENDING until the owner pauses and replaces the chain (re-rolling the stuck epoch). With the deployed caps (1,288, fixed by the first mint) this is unreachable, so informational. Reported by math. Fix: have the setup tooling assert REVEAL_CHAIN_LENGTH >= maxSupply after lockSupply, or store the chain length on chain next to the commitment and check it in configureSupply.","line":10,"path":"Submission1/offchain/signer/reveal-chain.mjs","reproduction":"State: before opening, owner calls configureSupply([1702,168,168,68,68,48]) (sum 2222, accepted by _setSupply); minting then produces at least one random mint in 2,049 distinct epochs. Expected (reveal-chain.mjs header): the chain covers every epoch. Actual: at the 2,049th ready epoch readyReveals returns 503 REVEAL_CHAIN_MISMATCH and nobody can seal; chosen mints revert with REVEAL_PENDING until setRevealCommit. Verified by reading reveal-chain.mjs lines 10, 28, 41-42 and the contract's SUPPLY_CEILING.","severity":"info","snippet":"export const REVEAL_CHAIN_LENGTH=2048;   // >= maxSupply: at most one epoch per random token","title":"Reveal chain length 2,048 is below SUPPLY_CEILING 2,222 and nothing ties the two: a supply configured above 2,048 could exhaust the committed secrets"},{"citation":"resolved","description":"Answer to brief question 5 and README 8.4. Measured in Foundry (solc 0.8.38, viaIR, 200 runs, cancun, mock 18-decimal token): a chosen paid mint that seals and reveals 8 full epochs of 25 random tokens costs 3,274,698 gas versus 220,823 for a plain chosen mint, about 380k per full epoch (README 10 quotes 650-690k per epoch on EDR/Osaka rules). The bound holds and fits a 36M block, so no liveness defect. The cost allocation is asymmetric: a random minter may always pass an empty reveals array and never pays for settlement, so a griefer who fills epochs with random mints (200 paid mints for the full 3.3M, or one random mint per epoch so that every chosen mint signed before a 25-block boundary and included after it without the new secret reverts with REVEAL_PENDING and must be re-sent) pushes the cost onto chosen-skin minters. Not economical as an attack; a cost and UX note. Reported by economics, flow and math; merged. Suggested: have the page call settle() separately when more than one epoch is ready so the minter is not surprised by a 10x gas estimate; document the 25-block inclusion window for chosen mints; re-measure under the target network's gas schedule before mainnet.","line":427,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"test_gasSettle8x25 in test/scratch/Judge.t.sol: 25 random paid mints from distinct wallets in each of epochs 0..7 with reveals = [], roll to epochSeedBlock(7)+1, then a chosen mintPaid(choice 0, reveals = [x1..x8]). Logged: 'gas chosen mint settling 8x25: 3274698', afterwards pendingRandom == 0 and nextRevealIndex == 8; a following plain chosen mint logs 220823. A chosen mint with reveals = [] in the same state reverts REVEAL_PENDING.","severity":"info","snippet":"    function _settleReady(bytes32[] calldata reveals) private returns (bool progressed) {","title":"Gas bound measured: a chosen-skin mint that settles 8 epochs x 25 tokens costs about 3.27M gas, and that cost always falls on the first chosen-skin minter after a backlog (random minters never pay)"},{"citation":"resolved","description":"Each draw is weighted by remaining capacity (skinCaps[c] - skinCount[c]) at reveal time. Chosen mints consume only skins 0-2, so every chosen mint raises the probability of the random-only skins (pink, orange, gold) for all pending random tokens. The REVEAL_PENDING freeze stops this only once the seed block is mined; before that any minter may shift the weights. Because the seed is unknown before the freeze this cannot target a specific outcome, so README property 4 holds in its strict sense (answer to brief question 1, 'chosen-skin mints around a reveal'), but the odds a random minter faces are not fixed at mint time and can be moved by other participants. Reported by math. Fix (design choice): document the weighting on the mint page, or draw from fixed cap proportions and fall back to remaining capacity only when the drawn skin is sold out.","line":481,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"With production caps, before any chosen mint P(gold) for a random token = 48/1288 = 3.73%. If all 768 green, 168 ice and 168 purple have been minted as chosen skins (allowed; SKIN_SOLD_OUT fires only at the cap), the remaining pool is 184 for 184 random tokens: P(gold) = 48/184 = 26.1%. Follows directly from lines 481-487; no exploit.","severity":"info","snippet":"            for (uint256 c; c < SKIN_COUNT; ++c) { count[c] = skinCount[c]; remaining[c] = skinCaps[c] - count[c]; total += remaining[c]; }","title":"Random-skin odds are path dependent: chosen-skin mints before an epoch's seed block move every pending random token's probabilities (expected-value steering, not outcome steering)"},{"citation":"resolved","description":"The scan cursor `used` only moves forward. With two ready epochs and reveals = [x2, x1], the first scan skips x2 (its hash is x1, not the current commit x0) and consumes x1; the second epoch needs x2 but the cursor is already past it, so _settleReady returns and a chosen mint reverts although it carried every needed secret. The signer returns secrets in order, so this is a robustness note for third-party callers of settle() who build the list from events or several API responses. Reported by math. Fix: restart the scan from index 0 for each epoch (reveals has at most 8 entries) or match the whole array against the current commit.","line":435,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"test_outOfOrderRevealsLoseSecret in test/scratch/Judge.t.sol: one random token in epoch 0 and one in epoch 1, both seed blocks mined, chosen mintPaid with reveals = [x2, x1]. Expected: both epochs sealed (both secrets present). Actual: the call reverts with REVEAL_PENDING and nextRevealIndex stays 0.","severity":"info","snippet":"                while (used < reveals.length && keccak256(abi.encode(reveals[used])) != commit) ++used;","title":"reveals[] matching is order-sensitive: the right secrets in the wrong order lose the later secret and a chosen mint reverts with REVEAL_PENDING"}],"hash":"9d11a56700669e2550572c120f8315aa2cd2fbfee19f087b5d6acd020cba44b5","nodeId":"e365ed3d-67a4-4130-a4db-9b31c6509c86","outcome":"completed","summary":"The review is complete and `.imd-findings.json` is written in the repository root with 14 findings.\n\n**What I did.** I reproduced every specialist claim against the pinned code before keeping it. The three attached proofs were run unchanged: all fail on the current contract for the stated reason, and the two I kept pass once a one-line fix is applied to a patched copy. For the other on-chain claims I wrote one scratch harness, `test/scratch/Judge.t.sol`, which demonstrates each behaviour. The off-chain claims were verified by reading the reader, book and signer code.\n\n**Merged findings, by severity**\n\n- **Medium, owner re-roll.** The owner alone can re-arm the same ready epoch through `setRevealCommit` without limit and keep the draw it likes. Three specialists reported it; merged, with the economics proof attached.\n- **Medium, GOLD book.** The reader rejects any receipt whose outer target is not the Genesis contract or that holds more than one mint, so a paid mint from a Safe, a 4337 bundle or a 7702 batch leaves the GOLD hold stuck forever. No admin command can release it.\n- **Low, nine ready epochs.** Chosen-skin minting is dead until someone sends a separate `settle()`, which the documented flow never does. Proof attached.\n- **Low, zero-confirmation secret release** plus chosen mints still allowed inside the seed block. Three specialists reported it. I rated it low because it needs the next slot's proposer to reorg and hold a pending random token.\n- **Low, expired seed block.** The secret alone decides, the signer knows those outcomes at deployment, and on any chain without the EIP-2935 contract it happens after 256 blocks. Four entries merged.\n- **Low, re-used commitment.** `setRevealCommit` accepts an already opened value such as the original anchor.\n- **Low, unbounded re-arm loop.** Measured at about 28.7k gas per pending epoch, so recovery stops fitting in a block past roughly 1,250 silent epochs.\n- **Info.** Per-sender transient marker, Appearance signature reusable across routes, domain version mismatch in the book, chain length below the supply ceiling, the 3.27M gas measurement for an 8 by 25 settle, path-dependent odds, and order-sensitive reveal lists.\n\n**Dropped or downgraded.** Nothing was dropped as unreproducible. The economics gas finding and the flow griefing finding were merged into one info entry because they describe the same cost allocation. The math specialist's medium rating for the reorg finding was lowered to low for the reason above.\n\n**Coverage note.** I also checked supply and per-skin caps, signature binding, the exact-burn paid path and reentrancy against the code and found no defect beyond those listed. The scratch harness and a Foundry project in `/tmp` were used for checking only; nothing in the repository outside `.imd-findings.json` and `test/scratch/` was created or changed.","treeHash":null,"usage":{"cachedInputTokens":1319992,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":31486,"runtime":"claude","turns":17,"wallClockMs":442974}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c722c2e9ac9aa084","findings":[{"citation":"resolved","description":"The commit-reveal argument rests on one ordering claim (AUDIT_README section 5 and the reveal-chain.mjs header): whoever produces the seed block cannot know the secret, and whoever knows the secret cannot change that block. The signer breaks the first half at the boundary. readyReveals() hands out the secret for epoch e as soon as provider.getBlockNumber() == epochSeedBlock(e), i.e. when the seed block is the head block with zero confirmations. On chain, readiness and the chosen-skin freeze use the strict comparison block.number > epochSeedBlock(epoch) (EmberEvoGenesisCandidate.sol:316), so transactions included in the seed block itself may still change skin counts (verified: a chosen mint at block.number == seedBlock succeeds, test_seed_block_boundary in test/scratch/EmberMath.t.sol). If head block S is reorged out after x[k] is public (API response, or a pending settle/mint carrying it in the mempool), the proposer of the replacement block S' knows x[k], can evaluate keccak256(abi.encode(hash(S'), x[k], e, this)) for every candidate block it builds, can grind S' (tx ordering, extra data, included txs) toward a preferred draw, and can include its own chosen-skin mints in S' to move the remaining[] weights, because at height S the freeze is not active. Single-slot reorgs by the next proposer are a known, occasionally observed mainnet event; a proposer holding a pending random token in epoch e has a direct payoff (gold is a 48/1288 draw). This is exactly the capability README 8.2 says the seed-block producer does not have.","line":39,"path":"Submission1/offchain/signer/reveal-chain.mjs","reproduction":"State: epoch e has pending random tokens; S = epochSeedBlock(e). 1) Block S is produced; the signer sees latest == S and GET /v1/reveals returns x[k] (reveal-chain.mjs:39 only breaks when seedBlock > latest). 2) A settle/mint carrying x[k] is broadcast. 3) The proposer of slot S+1 reorgs S (ex-ante one-block reorg) and builds S' at height S containing its own chosen-skin mints (allowed: nextRevealEpoch() is not seedKnown while block.number == S) and tries variants of S' until keccak256(abi.encode(blockhash(S'), x[k], e, this)) yields the wanted skin for its token id. 4) Anyone seals e at S'+1 with x[k]. Expected (README property 4 / 8.2): the seed-block producer cannot know the secret. Actual: it knows it and can both grind the hash and move the weights. Fix: in the signer, release a secret only once the seed block has K confirmations (e.g. latest >= seedBlock + 2, or the safe head); in the contract, start the freeze in the seed block itself (block.number >= epochSeedBlock(e) in the REVEAL_PENDING check) so no chosen mint can land in the block whose hash seeds the draw, and consider readiness at seedBlock + K to match the signer.","severity":"medium","snippet":"  if(Number(seedBlock)>latest)break;         // seed block not mined yet: its secret stays with the signer","title":"Reveal secret is released against a seed block with zero confirmations: a one-block reorg lets the replacement proposer grind the seed hash with a known secret and add chosen-skin mints in the same bl"},{"citation":"resolved","description":"_settleReady settles at most 8 epochs per call and the signer serves at most 8 secrets (MAX_REVEALS). A random mint never needs settling and always proceeds with an empty reveals list, so the backlog of ready-but-unsealed epochs grows whenever random mints happen and chosen mints do not. Once 9 epochs are ready, _approval() for a chosen skin finds the 9th epoch still seedKnown and reverts regardless of what the caller supplied. The README flow (section 5 and 8.3: the page fetches fresh secrets and retries) cannot recover: the retry fetches the same 8 secrets and reverts again. Only an explicit settle(reveals), repeated until fewer than 9 epochs are pending, restores chosen minting. Amplifier: an unprivileged minter can create the state deliberately by minting one random token per 25-block epoch during a quiet period (9 mints over about 45 minutes); after that every chosen-skin mint is dead until someone runs settle, while random mints keep working.","line":429,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"Verified in test/scratch/EmberMath.t.sol test_nine_ready_epochs_block_chosen: deploy with the production caps, mint one random token in each of 9 consecutive epochs (blocks startBlock+0, +25, ..., +200), roll to startBlock+227 so all 9 seed blocks are mined, then submit a chosen-skin mintPaid(choice 0) with reveals = [x1..x9]. Expected: the mint settles what it can and succeeds, or the page can recover by retrying. Actual: the first 8 epochs are sealed and revealed, then _approval reverts with REVEAL_PENDING; the whole transaction reverts, so the state is unchanged and the next attempt fails identically. Fix: raise or parametrise the per-call bound, have the signer return more than 8 secrets, and make the page call settle() first when more than 8 epochs are ready (nextRevealIndex vs randomEpochCount with epochSeedBlock < latest).","severity":"low","snippet":"        for (uint256 guard; guard < 8; ++guard) {","title":"Nine or more ready epochs make every chosen-skin mint revert with REVEAL_PENDING even when it carries all the secrets; recovery needs an out-of-band settle() the documented flow never sends"},{"citation":"resolved","description":"Every secret that sealed an epoch is public: it is in calldata and in the EpochSealed event, and the old anchor x[0] is in RevealCommitSet. setRevealCommit only rejects zero and the current value. If the owner re-runs the setup tooling with the previous REVEAL_SEED (deploy-testnet.mjs and new-reveal-seed.mjs give no on-chain guard against this), or sets the commitment to any earlier chain value, the re-armed epoch needs a preimage everyone already has. The signer is then no longer the gate: the re-arm block's proposer knows the secret in advance and can grind the hash exactly as the medium finding describes, and any user can seal the epoch the moment the re-arm block is mined. The contract has the information to refuse this cheaply.","line":214,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"Verified in test/scratch/EmberMath.t.sol test_setRevealCommit_accepts_old_chain_value: mint one random token, settle it with x1 (x1 is now public), mint another random token in a later epoch, wait for its seed block, pause, call setRevealCommit(x0) with the original anchor. Expected: REVEAL_COMMIT_INVALID (the commitment was already opened). Actual: accepted, the pending epoch is re-armed to block.number+1, and sealEpoch(e, x1) succeeds two blocks later with the public x1. Fix: record every commitment value the contract has held or opened (mapping(bytes32 => bool) seenCommit, set in setRevealCommit and in _seal for the consumed secret) and require !seenCommit[commit] in setRevealCommit.","severity":"low","snippet":"        require(commit != bytes32(0) && commit != revealCommit, \"REVEAL_COMMIT_INVALID\");","title":"setRevealCommit accepts an already-opened commitment, so a re-armed epoch can be sealed by anyone with a secret that is already public"},{"citation":"resolved","description":"The signer derives exactly 2048 secrets and the comment claims this is >= maxSupply. The contract allows configureSupply up to SUPPLY_CEILING = 2222 before minting opens, and each epoch that holds at least one random token consumes one secret, so more than 2048 random epochs is reachable (one random token per 25-block epoch over about 51k blocks). After the 2048th seal, revealChain.next(commit) returns null for the last chain value, readyReveals throws REVEAL_CHAIN_MISMATCH (HTTP 503), no secret can be produced, the next ready epoch stays unsealed, and every chosen-skin mint reverts with REVEAL_PENDING while random mints keep growing the backlog. Recovery requires pausing and setRevealCommit with a fresh seed, which re-rolls the stuck epoch (the owner+signer power of README 8.2). With the deployed caps (1,288) this is unreachable, but nothing on chain or in the tooling enforces the relation.","line":10,"path":"Submission1/offchain/signer/reveal-chain.mjs","reproduction":"State: owner calls configureSupply([1702,168,168,68,68,48]) (sum 2222, accepted by _setSupply) before opening; minting runs with at least one random mint in 2049 distinct epochs. Expected (reveal-chain.mjs header): the chain covers every epoch. Actual: at the 2049th ready epoch the signer returns 503 and nobody can seal; chosen mints revert with REVEAL_PENDING until setRevealCommit. Fix: have setup tooling assert REVEAL_CHAIN_LENGTH >= maxSupply (or derive the chain length from maxSupply after lockSupply), or store the chain length on chain next to the commitment and check it in configureSupply.","severity":"low","snippet":"export const REVEAL_CHAIN_LENGTH=2048;   // >= maxSupply: at most one epoch per random token","title":"Reveal chain length 2048 is below SUPPLY_CEILING 2222: a supply above 2048 can exhaust the committed secrets and stall every chosen-skin mint until the owner replaces the chain"},{"citation":"resolved","description":"Measured on a scratch Foundry harness (solc 0.8.38, viaIR, 200 runs, cancun): a chosen paid mint that seals and reveals 8 full epochs costs 3,295,524 gas; one full epoch costs 749,386 (the README quotes 650-690k). The bound holds and fits in a 36M block, so this is not a liveness defect, but the cost allocation is asymmetric: a random minter can always pass an empty reveals array and never pays for settlement, while the first chosen minter after a backlog pays for up to 200 reveals (about 2.5M gas more than its own mint). Re-measure against the target network's gas schedule before mainnet.","line":441,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"test/scratch/EmberMath.t.sol test_gas_settle8x25: mint 25 random tokens in each of 8 epochs from distinct wallets, roll two blocks past the 8th seed block, then a chosen mintPaid(choice 0, reveals=[x1..x8]) from a fresh wallet. Logged gas: 3,295,524; afterwards pendingRandom == 0 and nextRevealIndex == 8. Expected per README 8.4: bounded. Actual: bounded at about 3.3M. No contract fix required; consider having the page call settle() separately when more than one epoch is pending so the minter is not surprised by a 4x gas estimate.","severity":"info","snippet":"            _revealRange(e, 0);","title":"Measured gas bound: a chosen mint that settles 8 epochs x 25 tokens costs about 3.30M gas, and only chosen minters ever pay it"},{"citation":"resolved","description":"Each draw is weighted by remaining capacity (skinCaps[c] - skinCount[c]) at reveal time. Chosen mints only consume skins 0-2, so every chosen mint raises the probability of the random-only skins (pink, orange, gold) for all pending random tokens. The REVEAL_PENDING freeze stops this only once the seed block is mined; before that any minter may shift the weights. Because the seed is unknown before the freeze this cannot target a specific outcome, so README property 4 holds in its strict sense, but the odds a random minter faces are not fixed at mint and can be moved by other participants or by the minter (buy chosen greens, then mint randoms).","line":481,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"With production caps: before any chosen mint, P(gold) for a random token = 48/1288 = 3.73% and P(pink) = 68/1288 = 5.28%. If all 768 green, 168 ice and 168 purple have been minted as chosen skins (allowed; SKIN_SOLD_OUT only fires at the cap), the remaining pool is 68+68+48 = 184 for 184 random tokens: P(gold) = 48/184 = 26.1%; with only the greens gone P(gold) = 48/520 = 9.23%. Expected by a reader of 'random over all six skins': fixed odds. Actual: odds depend on prior chosen mints. Fix (design choice): document the weighting on the mint page, or draw from fixed cap proportions and fall back to remaining capacity only when the drawn skin is sold out.","severity":"info","snippet":"            for (uint256 c; c < SKIN_COUNT; ++c) { count[c] = skinCount[c]; remaining[c] = skinCaps[c] - count[c]; total += remaining[c]; }","title":"Random-skin odds are path dependent: chosen-skin mints before the freeze move every pending random token's probabilities (expected-value steering, not outcome steering)"},{"citation":"resolved","description":"A staticcall to an address with no code succeeds with empty return data, so on a chain without the Prague history contract (the Hardhat EDR Cancun chain the test suite uses, or any L2/testnet lacking it) the function returns h = 0 for every seed block older than 256 blocks, and _seal proceeds with keccak256(abi.encode(0, secret, e, this)), an outcome the signer knows in advance for every epoch. On Sepolia and mainnet the contract exists, so the 8,191-block window described in the README applies, but none of the 78 tests exercise it (the README states tests run on Cancun). Boundary check 'no code at receiver': the calling code assumes the history contract answers or reverts, while an empty account answers success.","line":454,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"State: seal an epoch whose seed block is 300 blocks old on a chain without code at 0x0000F90827F1C53a10cb7A02335B175320002935 (e.g. the project's own EDR test chain). Expected per the comment: the hash from the history contract. Actual: ok == true, r.length == 0, h == 0, EpochSealed emitted with blockHash == 0 and the secret alone decides. Fix: require BLOCK_HASH_HISTORY.code.length != 0 in the constructor (mainnet and Sepolia both have it), or treat an empty return as a hard failure for blocks inside the 8,191 window, and add a test with the history contract etched at that address.","severity":"info","snippet":"        (bool ok, bytes memory r) = BLOCK_HASH_HISTORY.staticcall(abi.encode(n));","title":"_blockHash silently returns zero on any chain where the EIP-2935 history contract is absent, so the secret alone decides after 256 blocks instead of 8,191; this path is untested"},{"citation":"resolved","description":"The scan cursor `used` only moves forward. With two ready epochs and reveals = [x2, x1], the first scan skips x2 (its hash is x1, not the current commit x0) and consumes x1; the second epoch then needs x2 but the cursor is already past it, so the function returns and a chosen mint reverts with REVEAL_PENDING although it carried every needed secret. The signer returns secrets in order, so this is a robustness note rather than an exploit; it matters for third-party callers of settle() who build the list from EpochSealed events or from several API responses.","line":435,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"Verified in test/scratch/EmberMath.t.sol test_out_of_order_reveals_array_loses_secret: two epochs with one random token each, both seed blocks mined, chosen mintPaid with reveals = [x2, x1]. Expected: both epochs sealed (both secrets are present). Actual: epoch 1 sealed, epoch 2 not, revert REVEAL_PENDING. Fix: restart the scan from index 0 for each epoch (reveals has at most 8 entries) or match the whole array against the current commit.","severity":"info","snippet":"                while (used < reveals.length && keccak256(abi.encode(reveals[used])) != commit) ++used;","title":"reveals[] matching is order-sensitive: a list with the right secrets in the wrong order loses the later secret and the chosen mint reverts"}],"hash":"af2e8d6edb35a0ac991058ef5ae8a8438f7bc5dfd403b4ae660200bfd68d861a","nodeId":"11b1dd3e-be5a-4bdf-b364-6d238bc1c507","outcome":"completed","summary":"Review complete. The findings are in `.imd-findings.json` (8 entries), the repository tree is untouched, and every claim marked \"verified\" was reproduced with a Foundry harness in `test/scratch/EmberMath.t.sol` against the contract compiled with solc 0.8.38, viaIR, cancun.\n\n**Outcome.** No critical or high defect in the on-chain math. Supply and per-skin caps hold (sum of remaining capacity always covers pending randoms, so `NO_SKIN_LEFT` is unreachable), the weighted draw always lands on a skin with capacity, signatures are bound to wallet, choice, URI, deadline, signer epoch, chain and contract, and the paid route's exact-burn and once-only order checks are sound. The one medium finding is in the trust model at the seam between the signer and the contract's block boundary.\n\nFindings, by severity:\n\n- **Medium, signer `reveal-chain.mjs:39`.** The secret is released when the seed block is the head block with zero confirmations, while the contract's freeze only starts strictly after the seed block. A one-block reorg by the next proposer therefore gives that proposer the secret, the ability to grind the seed hash, and room to include chosen-skin mints in the replacement block. This is the capability the README says the seed-block producer cannot have. Fix: release after K confirmations and start the freeze in the seed block itself.\n- **Low, `_settleReady` 8-epoch cap (line 429).** With 9 or more ready epochs, a chosen mint reverts with REVEAL_PENDING even when carrying every secret, and the documented retry loop cannot recover without an out-of-band `settle()`. An unprivileged minter can create this state with one random mint per epoch during a quiet period.\n- **Low, `setRevealCommit` (line 214).** Accepts a previously opened commitment, so a re-armed epoch becomes sealable with a secret that is already public.\n- **Low, chain length.** 2048 secrets versus a supply ceiling of 2222: an allowed supply configuration can exhaust the chain and stall chosen mints.\n- **Info.** Measured gas bound of 3.30M for an 8×25 settlement paid only by chosen minters; path-dependent random odds (gold rises from 3.73% to 9.23% once greens are chosen out); silent zero hash when the EIP-2935 contract is absent, untested on the project's Cancun test chain; order-sensitive `reveals[]` matching.\n\n**Coverage and limits.** The assigned Math Precision, Boundary and Numerical Gap passes were applied to the full primary contract and to the signer's reveal logic. The GOLD ledger under `offchain/gold-book` was not reviewed in depth. No finding reached the high bar, so no proof tests are attached. The harness tests pass on the current code because they demonstrate behaviour rather than fail on a defect.","treeHash":null,"usage":{"cachedInputTokens":1826645,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":62665,"runtime":"claude","turns":27,"wallClockMs":888664}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7e50667f872c7f5e","findings":[{"citation":"resolved","description":"setRevealCommit (lines 212-225) accepts any new commitment while paused and re-arms every unsealed ready epoch to block.number + 1. Nothing limits how often an epoch can be re-armed, and the new chain does not have to come from the approval signer: the owner can generate its own hash chain, so after the first replacement only the owner knows the secrets and nobody else can seal. The owner then waits one block, reads blockhash(rearm block) and its own secret, replicates _seal/_revealRange off chain, and if the draw is not the one it wants calls setRevealCommit again: the epoch is re-armed once more and the previous seed is discarded. This is a pure owner power, but it contradicts the trust model the package states: section 6 point 4 says nobody can steer a random skin except as described in 8.2, and 8.2 says 'Owner + signer together can re-roll a ready epoch'. The signer is not needed, and the number of re-rolls is unbounded (expected about 27 tries for the 48/1,288 molten-gold skin). On mainnet the owner is a multisig; this is a material trust assumption that the audit package understates. The events RevealCommitSet / EpochRearmed make it visible but do not prevent it.","line":222,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"State: one random token (#1) minted in epoch 0 (deploy block 1000, seed block 1025); block 1026 (epoch ready, unsealed; the signer's x1 is public off chain but nobody has sealed). Owner: setMintPaused(true); setRevealCommit(y0) where y0 = keccak(y1) for an owner-generated y1 -> EpochRearmed(0, 1027). At block 1028 the owner computes seed = keccak(abi.encode(blockhash(1027), y1, 0, address(this))) and r = uint(keccak(seed, 1)) % 1288; if the resulting skin is not 5 (molten gold) the owner calls setRevealCommit(z0) -> EpochRearmed(0, 1029) and repeats. Expected per README 8.2: a re-roll requires the signer's cooperation and is a one-off; actual: the owner's 2nd, 3rd ... Nth setRevealCommit are all accepted (test test_secondRearmOfSameUnsealedEpochIsRefused fails: 'next call did not revert as expected') and test_ownerAloneRerollsReadyEpochUntilGold reaches molten gold for token #1 after 29 re-rolls, then sealEpoch(0, chosen secret) + revealEpoch(0, 0) gives skinOf(1) == (5, true). Fix (minimal, keeps the lost-chain recovery): allow at most one re-arm per epoch (require(epochRearmBlock[e] == 0) before re-arming, or revert setRevealCommit while any ready epoch already carries a re-arm block), and/or require the new commitment to be accepted by the approval signer with an EIP-712 signature (like activateAppearanceSigner) so that the documented 'owner + signer' model actually holds; update README 8.2 either way.","severity":"medium","snippet":"epochRearmBlock[e] = block.number + 1; ++rearmed; emit EpochRearmed(e, block.number + 1);","title":"Owner alone can re-roll a ready random epoch an unbounded number of times with setRevealCommit (README 8.2 says owner + signer are both needed)"},{"citation":"resolved","description":"_blockHash (lines 451-456) returns zero once the seed block is older than blockhash (256) and the EIP-2935 window (8,191 blocks), and on a chain where the history contract is absent (local chains, some L2s) already after 256 blocks. _seal then mixes h = 0 with the committed secret. The secret chain is fixed before minting opens and fully known to the signer service, so every seed on the expiry path is computable by the signer at any time: seed_k = keccak(0, x[k], e, this). README 8.2 describes this as 'one extra option for the signer'. It is more than that: the signer can tell a colluding minter, before the minter even sends a random mint, which token id in which epoch will draw molten gold on the expiry path (the draw is keccak(seed, id) % remaining, and remaining is frozen by REVEAL_PENDING while the epoch waits). The cost is withholding the secret for ~27 h during which chosen mints revert, which is visible but not prevented. Also, whoever holds a published secret while nobody seals for the whole window gets a free choice between the pre-expiry and post-expiry outcome of that epoch.","line":464,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"State: one random token (#1) in epoch 0, seed block 1025, no seal. Roll to block 1025 + 8192 (no EIP-2935 contract at 0x...2935 in the test chain, so h = 0 from the first fallback). sealEpoch(0, x1). Expected (property 4 / commit-reveal intent): the outcome depends on a block hash nobody knew when the chain was committed; actual: epochSeed(0) == keccak256(abi.encode(bytes32(0), x1, 0, address(this))), a value the signer could compute at deployment (scratch test GasBoundTest.test_expiredSeedBlockIsSecretOnly passes on the current code). Fix: when h == 0 in _seal do not seal; instead re-arm the epoch (epochRearmBlock[e] = block.number + 1, emit EpochRearmed) and revert or return, so an expired epoch always takes a hash that was unknown when its secret was fixed; the wait is bounded because anyone can call settle/sealEpoch two blocks later. Alternatively require h != 0 and accept the owner re-arm path as the only recovery.","severity":"low","snippet":"bytes32 seed = keccak256(abi.encode(h, secret, e, address(this)));","title":"Expired seed block silently degrades the seed to secret-only: the signer knows every such seed in advance, and a colluding minter can pick token ids for a chosen rare skin"},{"citation":"resolved","description":"Every chosen-skin mint must first settle all ready epochs (line 337 reverts with REVEAL_PENDING otherwise). A griefer who sends random mints with an empty reveals list never settles anything, so the first chosen-skin mint after a run of unsettled epochs pays for the whole backlog: 8 epochs x 25 tokens inside one mint was measured at 3,283,190 gas in a scratch test (README 8.4 only says 'in practice at most one or two epochs'). Filling 8 epochs costs the griefer 200 paid mints, so this is expensive; the cheap variant is one random mint per epoch: then every 25 blocks a new epoch becomes ready and any chosen-skin mint that was signed before the boundary but included after it (without the new secret) reverts with REVEAL_PENDING and must be re-signed and re-sent (README 8.3 describes the retry but not that a third party can trigger it on every epoch boundary for about one paid mint per 5 minutes). Neither variant breaks an invariant: the bound holds and settle() is public, so this is a cost and UX issue, not a safety one.","line":429,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"State: 8 consecutive epochs (blocks 1000..1199) each with 25 paid random mints carrying reveals = []; block 1201. (1) mintPaid(choice 0, reveals = []) -> revert REVEAL_PENDING. (2) mintPaid(choice 0, reveals = [x1..x8]) succeeds and consumes 3,283,190 gas (scratch GasBoundTest.test_chosenMintSettles8EpochsOf25), versus ~269k for a chosen mint with nothing to settle. Expected: the cost of revealing random tokens is borne by random minters or by whoever calls settle; actual: it lands on the next chosen-skin minter. Fix options: settle at most 1-2 epochs per mint and require settle() for the rest (keeps the invariant: the chosen mint still reverts while an epoch is pending), or let the mint page call settle() in a separate transaction before a chosen mint when more than one epoch is ready; document the 25-block inclusion window for chosen mints.","severity":"low","snippet":"for (uint256 guard; guard < 8; ++guard) {","title":"REVEAL_PENDING griefing: a random-minting griefer makes one chosen-skin minter pay for up to 200 reveals (3.28M gas measured) and forces every chosen mint into a 25-block inclusion window"},{"citation":"resolved","description":"The Appearance struct binds wallet, look, choice, uriHash, deadline, signerEpoch, chain and contract, but has no nonce and the digest is not marked used. Today this is harmless: a second mintFree by the same wallet is stopped by freeClaimed, and a second mintPaid needs a fresh PaidAuthorization (orderId consumed once). But the same Appearance signature is accepted by both mintFree and mintPaid, and an Appearance issued for a paid order can be reused with every later PaidAuthorization of that wallet for the same look until its deadline. If either gate is relaxed later (several free claims per wallet, or an Appearance-only paid path) the missing nonce becomes a replay.","line":342,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"State: wallet W in the snapshot; signer issues Appearance A = (W, lookKey L, choice 0, uriHash U, deadline D, epoch 1) for a paid order O1 and PaidAuthorization for O1. W calls mintFree(L, 0, uri, D, A, proof, []) -> succeeds (the paid order's GOLD stays held until D and is then released). Then W uses the same A with mintPaid(..., O1) -> succeeds as well: two tokens from one approval. Expected per README property 3 ('a signature cannot be replayed ... for another choice / look / URI'): one approval, one mint; actual: one Appearance, two mints, bounded only by the other gates. Fix: add a route byte (free/paid) and a per-wallet nonce to Appearance, or mark the digest used in mintFree.","severity":"info","snippet":"bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(APPEARANCE_TYPEHASH, msg.sender, lookKey, choice, keccak256(bytes(uri)), deadline, signerEpoch)));","title":"Appearance signature has no nonce and is never consumed: one signature serves both routes and any number of paid orders of the same wallet"},{"citation":"resolved","description":"The marker (lines 375-381) limits one paid mint per caller per transaction and is deliberately not cleared. Because the slot is derived from msg.sender, a contract that deploys or owns N minimal proxies can perform N paid mints in one transaction, one per proxy, provided the signer issued one PaidAuthorization per proxy wallet (order.wallet == msg.sender). So the marker only limits what the signer already limits by issuing orders; it adds no on-chain cap on a wallet's total. For smart-contract wallets (4337, 7702) a batch that contains two mintPaid calls reverts entirely with ONE_PAID_MINT_PER_TRANSACTION, which is the intended behaviour but should be stated for wallet integrations. Answers README 8.7; no exploit.","line":376,"path":"Submission1/contracts/EmberEvoGenesisCandidate.sol","reproduction":"State: contract F owns proxies P1, P2 holding EMBEO and approvals; signer issued orders O1 (wallet P1) and O2 (wallet P2). F.run() calls P1.mintPaid(O1) then P2.mintPaid(O2) in one transaction: both succeed (two paid tokens in one transaction). A 4337 wallet S with orders O3, O4 (both wallet S) batching mintPaid(O3) + mintPaid(O4): the second reverts and the batch reverts. Fix: if the intent is one paid token per transaction regardless of caller, key the slot on tx.origin as well, or drop the marker and rely on order issuance.","severity":"info","snippet":"bytes32 slot = keccak256(abi.encode(PAID_TX_SLOT_PREFIX, msg.sender));","title":"Transient one-paid-mint-per-transaction marker is keyed on msg.sender only: a contract can fan out through sub-wallets, and a smart wallet batching two paid mints reverts the whole batch"},{"citation":"resolved","description":"genesisAuthDocument stores domain {name, version '5', chainId, verifyingContract} in every hold's auth document. The contract constructor uses EIP712(\"EmberEvoGenesisCandidate\", \"6\") and the signer's DOMAIN_VERSION is '6'. The stored value is not used to verify anything (the consent uses the 'Ember World GOLD' v1 domain and settlement uses chain facts), so there is no security impact, but anyone auditing a stored hold against the chain would see a wrong domain.","line":13,"path":"Submission1/offchain/gold-book/server/genesis-gold.ts","reproduction":"Reserve any order on the Sepolia book and read its auth document: domain.version == '5'; the verifying contract 0x8ce6...2efa answers eip712Domain() with version '6'. Fix: set GENESIS_R5_DOMAIN.version to '6' (or read it from the deployment registry).","severity":"info","snippet":"export const GENESIS_R5_DOMAIN={name:'EmberEvoGenesisCandidate',version:'5'} as const;","title":"GOLD book records the contract EIP-712 domain as version '5' while the deployed contract and signer use version '6'"}],"hash":"c2cda8e3a8522984a04461c130772093202ffdf5ebfd0e31829dd74347a8c4c8","nodeId":"fb0b1219-283c-4d7f-a245-dfbb9fb58ccb","outcome":"completed","summary":"All snippets match their cited lines, the working tree is clean apart from the findings file, and the review is complete.\n\n**Outcome.** The findings file holds six entries: one medium, two low, three informational. No critical or high defect was found in the primary contract. Supply and per-skin caps, the commit-reveal ordering, signature binding, exact-burn accounting, order consumption and reentrancy all held up against the concrete inputs I tried.\n\n**Findings written to `.imd-findings.json`:**\n\n- **Medium, owner-only unbounded re-roll.** The README says a re-roll needs owner plus signer. It does not. The owner can pause, replace the commitment with a chain only it knows, read the re-armed block hash, and replace again until the draw it wants appears. The scratch test reached molten gold for token 1 after 29 re-rolls, and the \"second re-arm refused\" assertion fails on the current code. A Foundry proof is attached. Fix: one re-arm per epoch, or require the signer's EIP-712 acceptance of a replacement commitment.\n- **Low, expiry path is secret-only.** After the EIP-2935 window the seed depends only on the committed secret, which the signer has known since deployment. With a colluding minter that becomes deterministic skin selection. Fix: re-arm instead of sealing when the hash is unreachable.\n- **Low, REVEAL_PENDING griefing.** A chosen-skin mint that settles 8 epochs of 25 unsettled random tokens measured at about 3.28M gas. One random mint per epoch also forces every chosen mint into a 25-block inclusion window. Within bounds, but the cost lands on the wrong party.\n- **Info.** Appearance signatures carry no nonce and are never consumed, so one approval serves both routes. The transient per-transaction marker is per caller, so sub-wallets bypass it. The GOLD book records domain version 5 while the contract uses 6.\n\n**Coverage.** Primary contract read in full against the Execution Trace, Periphery and First Principles passes and the five numbered task areas. Signer worker, reveal chain, paid and free approval, and the GOLD book reserve, reader and service modules were read for the trust model. Not exercised: the Hardhat suite in `tests/` (no runtime for it here), fee-on-transfer and ERC-777 tokens beyond code reading, and a live EIP-2935 history contract. Scratch tests ran in a Foundry project outside the repository, with OpenZeppelin 5.4.0 and solc 0.8.38 via-IR, matching the package's build settings. Nothing in the repository was modified apart from the findings file.","treeHash":null,"usage":{"cachedInputTokens":1216759,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":53701,"runtime":"claude","turns":21,"wallClockMs":741519}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8c9c4986ca881107","findings":[],"hash":"e7eb3e070ffa9ebbd46cc643d750f6dd77193601f557432c14136af2d9af6f07","nodeId":"11b1dd3e-be5a-4bdf-b364-6d238bc1c507","outcome":"failed","summary":"Failed to authenticate: OAuth session expired and could not be refreshed","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":null,"outputTokens":0,"runtime":"claude","turns":1,"wallClockMs":921}}],"verification":[]}