# Audit report

> Review Submission10/ only (read Submission10/AUDIT_README.md first; Submission9 to 5 AUDIT_README are the reference for everything unchanged). Not deployed. This is the build intended for Ethereum mainnet and the tools that will deploy it. The contract only loses the constructor guard require(block.chainid != 1); the runtime is identical to Submission9 without the trailing CBOR metadata (24,427 bytes, see Submission10/build/bytecode-comparison.Submission9-to-10.json). The signer and GOLD book are unchanged. New: tools/deploy-mainnet.ps1, tools/deploy-mainnet.mjs, tools/safe-batch.mjs.
>
> 1. Guard removal: does anything in the contract, signer or GOLD book rely on never running on chainId 1? Is any test-only behaviour reachable on mainnet?
> 2. Deployer window: between deployment and the Safe's acceptOwnership, what can the deployer key still do? Can a step be front-run or griefed? Can a partial run leave a contract that looks valid but is wrong?
> 3. Order and permanence: metadata URIs lock in the second transaction, before the metadata host is live. Is the order right, and is any check missing before an irreversible call?
> 4. Read-backs and preflight: is every value the next steps depend on checked? Can a stale load-balanced RPC node give a wrong result or nonce?
> 5. safe-batch.mjs open: does the root match the wallet file the signer serves? Are the time checks right? What happens with contract, zero or malformed addresses in the list?
> 6. Secrets: can the deployer key, RPC URL, signer key or reveal seed reach a log, the deployment record or an unneeded child process? (Windows does not enforce file mode 0o600.)
> 7. World registry entry: does mainnetGoldBookEntry match what offchain/gold-book/server/genesis-gold.ts validates?
>
> Report severity, reproduction and fix. Not in scope: artwork, the Mint web page, World 3D.

| | |
|---|---|
| Repository | https://github.com/tungweb3/ember-evo-genesis-review.git |
| Commit | `e99a826961a3c577fcbd799f8da9c9593b523e6e` |
| Job | `90ae2f92-4728-499d-9a73-d68ca3ea7cbf` |
| Judged | 2026-11-02 00:54 UTC |
| Findings | 4 medium · 3 low · 2 info |

Four agents audited the code as it is at `e99a826`, 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: Mainnet deploy sends whatever bytecode is in the unpublished test-artifacts file; nothing compares it with the reviewed build (build-receipt.json) before the six irreversible transactions or after the

`Submission10/tools/deploy-mainnet.mjs:18`

```
const artifacts=JSON.parse(readFileSync(new URL('../artifacts/test-artifacts.private.json',import.meta.url),'utf8'));
```

deployMainnet builds its ContractFactory (line 49) from artifacts[GENESIS].bytecode read from contract/artifacts/test-artifacts.private.json, a local file that is not published and that the test suite and the fixtures (FixtureToken, DecimalsToken, the hardening harnesses) are compiled into. build/build-receipt.json pins the reviewed build (initcodeSha256 8c42f973..., runtimeSha256 b2c62ca6..., runtimeBytes 24427) and the audit README says the reviewed runtime is the 24,427-byte one, but neither preflight() nor deployMainnet() compares the artifact with those values. After deployment the tool records codeHash and runtimeBytes (lines 75 and 79) but leaves both out of the `expect` comparison (lines 69-71), which covers only ABI-visible state that is identical for any compile of the same source or of an edited source with the same ABI. So a stale or re-compiled artifact (different optimizer/viaIR/evmVersion settings, a local edit never reverted, a Submission9 build with a different metadata hash, or a harness contract under the GENESIS key) is deployed on chainId 1 with readBackMismatch [] and exit code 0; the URIs are locked, USDC is added for good and the price is approved before any read-back runs. mainnetGoldBookEntry (line 87) then pins the deployed codeHash, so the World GOLD book reader (genesis-gold-reader.ts line 105 compares keccak256(eth_getCode) with d.codeHash) would settle paid orders against the unreviewed code, while Etherscan verification against build/standard-input.json fails. Other values the next steps depend on are also missing from `expect`: paymentTokens(USDC).usableAt (should be the add block's timestamp + 2 days), startBlock == deployBlock (the GOLD-book entry's deployBlock), signerEpoch == 1 and the eip712Domain chainId/verifyingContract. Merged from three specialists (economics, permissions, flow). Fix: before factory.deploy, require sha256(artifacts[GENESIS].bytecode without 0x) == build-receipt.json initcodeSha256 (load the receipt from build/) and that the ABI matches; after codeVisible, require sha256(runtime) == runtimeSha256 and add runtimeBytes '24427' plus the fields above to `expect`; make preflight refuse when the artifact hash differs so the owner sees it before typing DEPLOY.

**Reproduction**

Reproduced by reading the code (the artifact file and tools/runtime.mjs are not in the tree, so the Node test cannot be run here). State: contract/artifacts/test-artifacts.private.json holds, under the GENESIS key, bytecode compiled from the same source with viaIR:false, or from a contract edited after the review. Run `node contract/tools/deploy-mainnet.mjs` (or --dry-run). Expected: the tool stops before transaction 1 because sha256(artifact bytecode) != 8c42f9738a32b9b3c88968510d1cf9106d7a83c7e874fffdc29198e67b3dada6, or at least reports a read-back mismatch after step 1. Actual: all six transactions are sent, readBackMismatch is [] (codeHash and runtimeBytes are not in `expect`, lines 69-72), exit code 0, the record's codeHash and followUp.goldBookRegistry.codeHash pin the unreviewed code. The only check that exists, tests/contract/deploy-mainnet.test.mjs:38 `assert.equal(d.runtimeBytes,24427)`, runs against the same local artifact and is not part of the deploy.

### 2. Medium: Transactions 2-6 take their nonce, the acceptance block and the receipt wait from a load-balanced RPC without pinning or retry; a lagging node halts the non-resumable run after the URIs are locked, le

`Submission10/tools/deploy-mainnet.mjs:54`

```
 const commitBlock=await provider.getBlock((await provider.getBlockNumber())-2);
```

codeVisible (lines 44-47) protects only the eth_getCode read after the deployment; the file's own comment on line 43 and README section 3 item 5 say the mainnet RPC is load balanced and a node may lag. Every later step is exposed to that: (a) the deployer is a plain ethers.Wallet, so each of steps 2-6 populates its nonce with a fresh eth_getTransactionCount(deployer,'pending') (ethers 6.15 abstract-signer.js line 66: `pop.nonce = await this.getNonce("pending")`); a node that has not seen the previous receipt returns the nonce just used, and the send fails with NONCE_EXPIRED ('nonce too low') or is accepted into the lagging node's pool and never mined, in which case tx.wait() (no timeout) polls for ever with the deployer key in the PowerShell session's environment. (b) Line 54 asks the head from one call and block head-2 from a second; a node more than two blocks behind returns null and line 55 throws `Cannot read properties of null (reading 'number')`; a node on a one-block-reorged fork yields a blockHash that differs from blockhash(blockNumber) on the canonical chain and setRevealCommit reverts REVEAL_ACCEPTANCE_INVALID. (c) The acceptance is valid only while blockhash(blockNumber) != 0 (about 254 blocks, contract line 318-319); setRevealCommit is sent with fees estimated once and waited on without timeout, so a base-fee spike that keeps it pending past that window makes it revert REVEAL_ACCEPTANCE_EXPIRED and the run stops with TX_FAILED. In every case the failure comes after setRevealURIs(locked) has been mined: the contract exists with the official name, symbol and Safe recipient, URIs locked, some of the reveal commitment / USDC / price approval / pendingOwner missing, owned by the deployer; the record file is written only after step six (line 137) and the README tells the owner not to run the tool again, so the remaining steps have to be done by hand (including a fresh signer acceptance within 256 blocks). The dry run on the in-process chain cannot exercise any of this. Merged from five specialist findings (math x2, economics, permissions, flow). Fix: read nonce = getTransactionCount(owner,'latest') once after codeVisible (retry until it equals deployNonce+1) and pass {nonce: nonce++} explicitly to each of steps 2-6, or wrap the signer in ethers.NonceManager; take the commit block from a single getBlock('latest') object and use number-2 with a retry until getBlock is non-null and its hash is stable over two reads; after each receipt loop until getBlockNumber() >= rc.blockNumber and getTransactionReceipt(hash) is non-null before the next send; use tx.wait(1, timeoutMs) and on timeout re-sign a fresh acceptance and resend with the same nonce and a higher fee; append each completed step to the record file as it lands so a halted run can be resumed.

**Reproduction**

Reproduced against ethers 6.15.0 installed locally: node_modules/ethers/lib.esm/providers/abstract-signer.js line 66 `pop.nonce = await this.getNonce("pending")` runs inside populateTransaction for every sendTransaction, and deploy-mainnet.mjs passes no nonce to any of genesis.setRevealURIs/setRevealCommit/addPaymentToken/approvePaidPrice/transferOwnership (lines 53-59). State: RPC_URL fronts nodes A and B, B one block behind. Step 2 (nonce N+1) is mined and seen by A; step 3's eth_getTransactionCount lands on B and returns N+1. Expected: step 3 is sent with N+2 regardless of which node answers. Actual: A rejects the signed step 3 with 'nonce too low' (ethers NONCE_EXPIRED) and deployMainnet throws after setRevealURIs(locked) was mined, or B accepts it and tx.wait() never resolves. Variant: getBlockNumber() answers H from A, getBlock(H-2) lands on B at height H-3 and returns null; line 55 `commitBlock.number` throws TypeError with the URIs already locked. In both cases no deployment record exists (line 137 runs only after step six) and deploy-mainnet.ps1 line 78 tells the owner not to run the tool again.

### 3. Medium: safe-batch.mjs open: no upper bound on the opening time; an 11-digit unix value or a wrong year passes the tool and the contract, and configureSnapshot is once-only with no setter for freeStartAt

`Submission10/tools/safe-batch.mjs:31`

```
 if(!(start>=now+6*3600&&start>=takenAt))throw Error('the opening time must be at least 6 hours from now');
```

openBatch checks only a lower bound on `start` (at least 6 hours ahead and not before takenAt). The parser `unix()` on line 17 accepts any 9 to 11 digit number (`/^\d{9,11}$/`) and any zoned ISO date, so a unix value with one extra zero (17630000000 = 2528-09-02) or an ISO date with the wrong year passes every check in the tool, and the contract's configureSnapshot (EmberEvoGenesisCandidate.sol line 291) requires only start >= block.timestamp and start <= type(uint40).max (year 36812). configureSnapshot can run once (line 289 SNAPSHOT_ALREADY_FIXED), there is no setter for freeStartAt, mintFree and mintPaid both require block.timestamp >= freeStartAt (lines 588 and 618) and paidAvailability() is 0 before it (line 481). A Safe batch built from a mistyped opening time, once executed, therefore leaves the collection permanently unmintable: setMintPaused(false) succeeds in the same batch, the signer answers SALE_NOT_STARTED, and nothing the Safe can call moves the date; the only remedy is a new contract. The sole guard is the two Safe signers noticing the ISO date in the batch description (line 35) and `warnings[]` covers only the USDC usableAt. The tool is the last place a sanity bound can be applied before this irreversible call. Fix: reject 11-digit unix inputs (10 digits cover until 2286), add an upper bound such as start <= now + 60*86400 unless an explicit --far-opening flag is given, print the ISO opening date and require it to be typed back, and optionally read snapshotConfigured() and owner() from the chain before writing the file.

**Reproduction**

Run with the same regex and arithmetic as unix(): `node -e "const unix=v=>{if(/^\d{9,11}$/.test(String(v)))return Number(v);throw 1};for(const v of ['17630000000','99999999999'])console.log(v,new Date(unix(v)*1000).toISOString(),unix(v)<=2**40-1)"` prints `17630000000 2528-09-02T22:13:20.000Z true` and `99999999999 5138-11-16T09:46:39.000Z true`: both pass line 31 (start >= now+6h) and both fit uint40, so configureSnapshot(root, n, 1762000000, 17630000000) passes SNAPSHOT_TIME and setMintPaused(false) passes OPEN_NOT_READY. Input: `node safe-batch.mjs open <record> <wallets.json> 1762000000 17630000000` (the intended 1763000000 with one extra zero). Expected: the tool refuses or demands confirmation of an opening centuries ahead. Actual: the .safe-open.json is written with no warning; after the Safe executes it freeStartAt = 17630000000 for good, every mintFree/mintPaid reverts SALE_NOT_STARTED until the year 2528 and configureSnapshot reverts SNAPSHOT_ALREADY_FIXED.

### 4. Medium: The RPC URL (API key in the path) is printed in the uncaught ethers error on any non-200 RPC response, and deploy-mainnet.ps1 tells the owner to copy that output to an external assistant

`Submission10/tools/deploy-mainnet.ps1:78`

```
  if ($LASTEXITCODE -ne 0) { throw "Deploy script stopped (exit $LASTEXITCODE). Copy the output above to Claude; do not run this again before checking." }
```

The script treats RPC_URL as a secret (Read-Secret on line 50, kept in the process environment, removed in finally). But deploy-mainnet.mjs has no error handler on the mainnet path: preflight() on line 132 runs outside any try, and the try on lines 135-143 has only a finally. When the RPC answers with a non-200 status (an expired or mistyped Alchemy key gives 401, throttling gives 429, outages 5xx), ethers 6.15 throws SERVER_ERROR whose message and `info` carry `requestUrl`, the full RPC URL including the key path (utils/fetch.js lines 823-837). Node prints the uncaught error, with that message, to the terminal. Line 78 then tells the owner to copy the output to Claude, i.e. to paste the RPC credential into a third-party service; the same output lands in any terminal transcript or screenshot. The preflight run on line 60 prints the same error (without the paste instruction). The deployer private key is not affected (ethers redacts private keys in its argument errors) and the deployment record and console summary hold no secrets. Separately, the child process on line 53 only derives the deployer address but inherits RPC_URL, which it does not need. Merged from two specialists (permissions, economics). Fix: in deploy-mainnet.mjs wrap the mainnet entry point (preflight and deployMainnet) in try/catch and print only e.code, e.shortMessage ?? e.message with every occurrence of process.env.RPC_URL replaced by '<RPC_URL>', e.info?.responseStatus and the last step name, then set process.exitCode = 1; in the ps1 replace the instruction on line 78 with: note the exit code and the last step printed, check the chain, never paste the output anywhere before removing URLs; set $env:RPC_URL only around the two node calls that need it.

**Reproduction**

Reproduced with ethers 6.15.0 installed locally: a local HTTP server answering every request with 401 and body {"error":"Must be authenticated!"}; `new ethers.JsonRpcProvider('http://127.0.0.1:<port>/v2/SECRETKEY123').getNetwork()` (the first call preflight makes, line 98) rejects with message `server response 401 Unauthorized (request={  }, response={  }, error=null, info={ "requestUrl": "http://127.0.0.1:38049/v2/SECRETKEY123", "responseBody": "{\"error\":\"Must be authenticated!\"}", "responseStatus": "401 Unauthorized" }, code=SERVER_ERROR, version=6.15.0)` and e.info.requestUrl equal to the full URL. In deploy-mainnet.mjs that rejection is uncaught (no catch around lines 132 or 136), so Node prints the message verbatim and exits 1; deploy-mainnet.ps1 line 78 then instructs the owner to copy the output above to Claude. Expected: the output names the failure (401, chain unreachable) without the URL.

### 5. Low: Reveal URIs are locked for ever in transaction 2, before the metadata host exists and without any validation of the strings, although nothing needs the lock until the Safe opens minting

`Submission10/tools/deploy-mainnet.mjs:53`

```
 await record('setRevealURIs (locked)',await genesis.setRevealURIs(cfg.pendingURI,cfg.metadataBase,true));
```

setRevealURIs with lock=true is irreversible (contract line 442 `require(!revealURIsLocked, "REVEAL_URIS_LOCKED")`). README section 4 says the metadata and image host is not built yet, and nothing in preflight() or the run checks the two constants: not against the signer's own CONFIG_METADATA_BASE rule (`/^(ipfs|ar|https):\/\/[^\s]{1,200}\/$/`, free-approval.mjs line 53 and paid-approval.mjs line 61), not that pendingURI lies under metadataBase, not that the host answers. The read-back on line 71 compares the chain value with the same constant, so it detects only a transaction that did not land, never a wrong constant. tokenURI derives revealBaseURI + lookKey hex + '/' + skin (contract line 471), so a base without the trailing slash or on a mistyped domain breaks every token URI for good, and the signer Worker refuses to start with CONFIG_METADATA_BASE. The lock is needed only by setMintPaused(false) (contract line 300), which the Safe executes weeks later in the `open` batch, so locking in the deployer window buys nothing and removes the only chance to fix the host name, the path layout or a move to IPFS/Arweave after the host is live. Merged from four specialists (math, economics, permissions, flow). Fix: validate metadataBase with the signer's regex and pendingURI.startsWith(metadataBase) before sending; call setRevealURIs(pending, base, false) at step 2 (expect.revealURIsLocked false) and add ['setRevealURIs',[pending,base,true]] as the first call of openBatch in safe-batch.mjs, after an HTTPS GET of pendingURI and of one derived URI returns JSON; this adds no signing round because the Safe already signs that batch.

**Reproduction**

Input: edit MAINNET.metadataBase (line 29) to 'https://genesis.imdember.com/meta' (trailing slash dropped) or 'https://genesis.imdember.com/metta/' and run the tool. Expected: the tool refuses (regex/consistency check) or leaves the URIs unlocked so the Safe can correct them. Actual: the six transactions go through, readBackMismatch is [] because line 71 compares against the same constant, revealURIsLocked is true, and every later setRevealURIs reverts REVEAL_URIS_LOCKED for the owner and the Safe alike; tokenURI(1) returns 'https://genesis.imdember.com/meta0x<lookKey>/1' (or the wrong host) for ever, and FreeApprovalSigner/PaidApprovalSigner fail CONFIG_METADATA_BASE on the same string. Verified in the code that revealURIsLocked is read only by setMintPaused (line 300) and setRevealURIs (line 442), so no step between 2 and 6 needs the lock.

### 6. Low: Deployer window (trust assumption): until the Safe accepts, the deployer key can fix the once-only free list, open minting, add the last payment token, lock supply/URIs or re-target pendingOwner; safe

`Submission10/tools/deploy-mainnet.mjs:59`

```
 await record('transferOwnership(Safe)',await genesis.transferOwnership(ethers.getAddress(cfg.safe)));
```

Ownable2Step leaves owner() as the deployer until acceptOwnership. README section 4 accepts this and plans to sign acceptOwnership right after the run; the concrete powers are recorded so the window is kept short and verified. After step 3 revealURIsLocked and revealCommit are both set, so the deployer key alone (a hot key pasted into a PowerShell session) can call configureSnapshot(anyRoot, n, takenAt, start) (once only, SNAPSHOT_ALREADY_FIXED afterwards, line 289) and then setMintPaused(false), opening the sale on a free list the project never announced; it can also call addPaymentToken for the one remaining extra slot (MAX_EXTRA_PAYMENT_TOKENS is 2, line 96; USDC took one; never removable), lockSupply, configureSupply, lockTokenURIs, propose a new signer, and transferOwnership(other), which silently replaces the Safe as pendingOwner so the Safe's acceptOwnership reverts OwnableUnauthorizedAccount. Third parties cannot grief any of the six steps: every state-changing function except mintFree, mintPaid and settle is onlyOwner, those three need mintPaused == false, and the contract address derives from the deployer's own nonce. The tooling gap is concrete: safe-batch.mjs builds the accept and open files from the record alone and never reads owner(), pendingOwner(), snapshotConfigured() or mintPaused() from the chain, and deploy-mainnet.ps1 declares the run done without polling owner(). Not a permission bypass; the contract works as designed. Merged from three specialists (math, economics, permissions). Fix: keep the design but verify the hand-over: have the ps1 (or the tool) poll owner()/pendingOwner() and print a loud reminder until owner() == Safe; have safe-batch `accept` refuse when pendingOwner() != Safe and `open` refuse when owner() != Safe or snapshotConfigured() is already true; use a fresh single-purpose deployer key holding only the gas needed and treat it as burned once acceptance lands.

**Reproduction**

State after step 6: owner = deployer, pendingOwner = Safe. From the deployer key send configureSnapshot(0x01..01, 1, block.timestamp-60, block.timestamp) then setMintPaused(false), or transferOwnership(attacker). Expected (design intent): only the Safe sets the free list and the opening. Actual: both succeed (configureSnapshot is onlyOwner with no pendingOwner check, line 288; setMintPaused(false) passes OPEN_NOT_READY because snapshotConfigured, revealURIsLocked and revealCommit are all set after step 3); the Safe's later `open` batch reverts SNAPSHOT_ALREADY_FIXED and the free list can never be corrected, or its acceptOwnership reverts OwnableUnauthorizedAccount. safe-batch.mjs `accept`/`open` (lines 44-48) still write their files from the record without any chain read, so nothing warns the signers.

### 7. Low: safe-batch open writes the wallet list beside the record, but the signer Worker bundles offchain/signer/snapshot-wallets.json; nothing ties the on-chain root to the list the Worker actually serves

`Submission10/tools/safe-batch.mjs:50`

```
  writeFileSync(base+'.snapshot-wallets.json',JSON.stringify([...new Set(wallets.map(w=>ethers.getAddress(w).toLowerCase()))].sort(),null,1));
```

The root and the written file use the same normalization: snapshotTree (free-approval.mjs lines 30-31) applies wallet() (regex, getAddress, rejects the zero address), lowercases, dedupes and sorts; line 50 applies getAddress, lowercases, dedupes and sorts, after openBatch has already run snapshotTree and so rejected zero and malformed entries (WALLET_INVALID), so the two lists are identical. The time checks are right: takenAt <= now; start >= now+6h and start >= takenAt; a batch executed after `start` reverts SNAPSHOT_TIME atomically so the once-only snapshot is not consumed. Contract addresses are accepted with no warning: mintFree authenticates msg.sender, so an exchange deposit address or a token contract on the list holds a free slot nobody can use. The gap is between the file the tool writes and the file the Worker uses: worker-entry.mjs line 15 imports './snapshot-wallets.json', which today holds the two rehearsal wallets, and no step copies the new list there or compares roots before the Safe executes configureSnapshot. The Worker does compare snapshotRoot() with its own tree per request (free-approval.mjs line 87, SNAPSHOT_MISMATCH 503), so the failure is detected only after the opening, when every free approval is refused while paid approvals (which do not check the root) keep selling the shared supply; paidAvailability() reserves nothing for free-list wallets. Fix: have safe-batch write or verify offchain/signer/snapshot-wallets.json itself and print the root the Worker's own snapshotTree computes; add a Worker start-up check that compares snapshotTree(bundled).root with the on-chain snapshotRoot; optionally warn about entries with code (eth_getCode != 0x).

**Reproduction**

Run `safe-batch.mjs open <record> L.json <takenAt> <start>` with list L (root R_L) and deploy the Worker without replacing offchain/signer/snapshot-wallets.json (still the two rehearsal wallets, root R_old). The Safe executes the batch. Expected: a check refuses the open batch or the Worker refuses to start because R_old != R_L. Actual: nothing compares them; after the opening every free approval returns SNAPSHOT_MISMATCH (free-approval.mjs line 87) while paid approvals keep selling, until the Worker is redeployed with the right list. Zero or malformed address in L: snapshotTree throws WALLET_INVALID before any file is written (verified in free-approval.mjs lines 21-25). Contract address in L: accepted, holds an unusable free slot.

### 8. Info: The mainnet contract's NatSpec still says it is a rehearsal build that refuses mainnet; that text will be published through Etherscan verification

`Submission10/contracts/EmberEvoGenesisCandidate.sol:17`

```
/// @notice R6 testnet rehearsal build. Deployable on test networks; mainnet is refused until the final review removes
```

The constructor guard was removed (lines 236-237) but the @notice on lines 17-18 still says mainnet is refused and that price and administration 'still need final review'. The NatSpec is part of build/standard-input.json and will be published verbatim when the mainnet contract is verified, contradicting the deployment and misleading a reader of the verified source. Documentation only; the runtime is unchanged. Guard-removal check (README item 1): no other block.chainid use exists in the contract, EIP712 recomputes the domain separator per chain, BLOCK_HASH_HISTORY (EIP-2935) has code on mainnet (83 bytes at 0x0000F90827F1C53a10cb7A02335B175320002935, read through the task's chain-1 RPC), the signer and GOLD book take chainId from configuration and GENESIS_SLOT_SECONDS has an entry for chain 1, and no test-only path exists in EmberEvoGenesisCandidate.sol. Merged from two specialists (economics, flow). Fix: rewrite the @notice before the final build (this changes only the CBOR metadata hash; regenerate build-receipt.json and the bytecode comparison).

**Reproduction**

Read Submission10/contracts/EmberEvoGenesisCandidate.sol lines 17-18 against the constructor at lines 233-250: the notice says mainnet is refused, the constructor has no chain check. Expected: the notice describes the mainnet build.

### 9. Info: Safe Transaction Builder files carry no meta.checksum and no decoded method, so the import shows a 'changed properties' warning and raw calldata instead of the decoded calls the owners are told to che

`Submission10/tools/safe-batch.mjs:22`

```
  meta:{name,description,txBuilderVersion:'1.16.5',createdFromSafeAddress:d.safe,createdFromOwnerAddress:''},
```

builderFile writes the Transaction Builder JSON without meta.checksum and with contractMethod and contractInputsValues null. The Transaction Builder (safe-global/safe-react-apps, apps/tx-builder/src/lib/checksum.ts, confirmed by reading the file) recomputes keccak256 of the deterministically serialised file with meta.name nulled and compares it with meta.checksum; validateChecksum returns false when the stored checksum is undefined, so the import logs 'This file was modified since it was generated' and shows the checksum warning banner, though the import proceeds. With contractMethod null and the contract not yet verified on Etherscan at accept time, the app shows hex data only, so the file's own instruction (line 2-3, 'check the decoded calls') cannot be followed: two signers approve 0x79ba5097 and a configureSnapshot blob by eye, on the two mainnet steps where a tamper warning is exactly the signal they should otherwise take seriously. README section 6 lists the real import as not exercised. Merged from three specialists (math, permissions, flow). Fix: fill contractMethod with the ABI fragment ({inputs:[{name,type,internalType}],name,payable:false}) and contractInputsValues with the stringified arguments; compute meta.checksum as checksum.ts does (keys sorted recursively, undefined to null, meta.name set to null, keccak256 of the string) and add a test that recomputes it; import one generated file on Sepolia before mainnet.

**Reproduction**

Input: the file written by `safe-batch.mjs accept <record>` (lines 20-25: no meta.checksum, contractMethod:null) dragged into app.safe.global -> Apps -> Transaction Builder. Expected: imports cleanly and shows acceptOwnership() / configureSnapshot(root, eligible, takenAt, start) with arguments. Actual (from the Transaction Builder's checksum.ts: validateChecksum compares a computed keccak string with undefined and returns false): the checksum warning is shown and the transactions appear as raw data to the contract address. Not run against the live app here.

---

Judge's submission `4926aed892915a7e984e4f6f62f4c7edca6d848a4243fe7c36a1e24ffcb01782`, 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.
