# Audit report

> Review Submission16/ only (read Submission16/AUDIT_REQUEST.md first): World source 38a21a3, live as Worker eb8f9da9. It answers audit 351b0573 on Submission15 (9 Low, 4 Info); findings 2, 9 and 13 were deliberately left (reasons in section 2). No server code changed (the uploaded Worker module is byte-identical to Submission15's); no Solidity changed.
>
> 1. src/world/economyWallet.ts finishReceipt: a v2 record under another nonce is now accepted only for the wallet's own returned hash, under a HIGHER nonce, with this call's exact calldata or exact intended effect, or reverted. Can this confirm or release an intent for a transaction that is not this intent's own (another tab, a later same-calldata transaction, a wallet answering for another request)?
> 2. src/world/GoldPurchase.tsx, WorldGoldPanel.tsx, economyWallet.ts send(): the GOLD panel now offers the staking pane's paste-hash form and hashless acknowledgement, and send() clears the previous hash. Can either clear or release an intent that broadcast (other than the player's own false acknowledgement)? Does clearing the hash break the approve-then-pay flow?
> 3. src/world/pendingGoldCredit.ts, WorldGoldPanel.tsx: 60-minute after-expiry margin; one note per (order, hash, phase). Any "+N crediting" for a payment that cannot be credited, ended while a real payment is on its way, or beyond the 2 h cap?
> 4. scripts/deploy.mjs: node_modules re-checked after build and upload; CLOUDFLARE_ENV / WRANGLER_ENV / WRANGLER_BUILD_* / preloading NODE_OPTIONS refused; VITE_ case rule; link entries. Any false refusal on a clean Windows or Linux deploy, or anything uploaded that the record does not name?
>
> Report severity, reproduction and fix. Not in scope: 3D/rendering, media, Celestao, AIMarket.

| | |
|---|---|
| Repository | https://github.com/tungweb3/imd-ember-world-review.git |
| Commit | `f2f6340013d4a7babdea6fcfbe6ab0d7292b780d` |
| Job | `1ac5269f-0336-4191-8c47-9086a3b095ee` |
| Judged | 2026-10-27 15:21 UTC |
| Findings | 6 low · 3 info |

Four agents audited the code as it is at `f2f6340`, 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. Low: finishReceipt: the wallet's own hash mined under a LOWER nonce with this call's exact calldata is TRANSACTION_MISMATCH for ever, re-creating the finding-3 lock from the other side of the comparator

`Submission16/source/src/world/economyWallet.ts:247`

```
  const renumbered=own&&nonce(record.nonce)&&nonce(tx.nonce)&&BigInt(record.nonce)>BigInt(tx.nonce)&&(same||effect||r.status==='0x0');
```

The finding-3 exception accepts the wallet's own returned hash under another nonce only when record.nonce > tx.nonce, on the comment's premise that "renumbering only moves up". The stored nonce is eth_getTransactionCount(address,'pending') read before the prompt (line 194), which with MetaMask comes from its own nonce tracker and counts transactions it still lists as submitted. Two documented ways put the transaction signed for this very prompt below that count: (a) Settings > Advanced > "Customize transaction nonce" shows an editable nonce on the confirmation of the transaction the page requested, and the documented use is to type the nonce of a stuck pending transaction (lower) so this one replaces it; (b) a locally pending transaction marked dropped (evicted low-fee transaction, or "Clear activity tab data") frees its nonce, and MetaMask numbers the next send from the freed, lower nonce. In both cases eth_sendTransaction returns the hash of exactly this request (own===true), it mines from this sender with this call's exact to/input/value (same===true, or the exact Paid effect when routed) and status 0x1, yet renumbered is false and line 248 throws TRANSACTION_MISMATCH. Nothing resolves it: checkReceipt() repeats the throw, clearUnknownAfterWalletCheck() returns at once (line 277: a version-2 record with a hash is never acknowledgeable), the GOLD panel hides the acknowledgement because wallet.hash is set, and the paste form needs a transaction at the stored nonce, which this send never used. prepare()/payGold() throw PENDING_TRANSACTION for every later GOLD purchase, stake and withdraw of that address in that browser, although the payment ran and the server credits it. The only exit is to wait for some unrelated transaction to consume the stored nonce, paste its hash after finality, and have the intent judged "replaced" ("The original payment did not complete"), which also drops the "+N crediting" note via WorldGoldPanel's failed effect. The direction of the nonce adds no protection: what identifies this call is own (the hash the wallet returned for this persisted intent) plus same/effect (a one-time orderId in the calldata or the Paid event) and inspect() already refused to send when paid(orderId) was true. Four specialists reported this independently; merged here. Fix (keeps the design): drop the direction test for the wallet's own hash, e.g. `const renumbered=own&&nonce(record.nonce)&&nonce(tx.nonce)&&(same||effect||r.status==='0x0');` and keep every pasted-hash rule; update the suite's "an older identical payment" cases (economy-wallet-recovery.test.mjs lines 304-311) accordingly. If the stale-hash worry is kept for the lower direction, at least let the panel offer the hashed-intent acknowledgement after a mismatch so the lock has an exit.

**Reproduction**

Fixture of Submission16/source/tests/economy-wallet-recovery.test.mjs (eth_getTransactionCount answers '0x8', eth_sendTransaction returns HASH). prepare(payPlan()); confirm() -> stored intent {nonce:'0x8', hash:HASH}. candidate({hash:HASH,to:sent.to,input:sent.data,nonce:'0x7'}) (status 0x1, mined, finalized); checkReceipt(). Expected (same inputs with nonce '0x9' give phase 'confirmed', store 0): phase 'confirmed', store 0. Actual: {phase:'unknown',error:'TRANSACTION_MISMATCH',store:1}; a second checkReceipt() identical; clearUnknownAfterWalletCheck() changes nothing (hash non-null, version 2); prepare({kind:'stake',shares:'1'}) -> error PENDING_TRANSACTION, store 1. Pasting an unrelated transaction at nonce 0x8 ({hash:OTHER,to:A,input:'0x',nonce:'0x8'}, finalized) then yields phase 'replaced', error TRANSACTION_REPLACED, store 0. The submission's own test case 'an older identical payment' (nonce '0x5') asserts the TRANSACTION_MISMATCH outcome.

### 2. Low: finishReceipt: an own transaction the wallet renumbered that is then cancelled or sped up can never be released, because every pasted hash must carry the STORED nonce the send never used

`Submission16/source/src/world/economyWallet.ts:248`

```
  if(tx.version===2&&record.nonce!==tx.nonce&&!renumbered)throw new EconomyClientError('TRANSACTION_MISMATCH');
```

The renumbered exception applies only to the wallet's own returned hash (renumbered requires own). When MetaMask numbers this send one up (nonce 0x9 while the intent stored 0x8, the exact case finding 3 describes), and that transaction then never mines because the player cancels or speeds it up in the wallet, the replacement carries the renumbered nonce 0x9. checkReceipt(replacement) computes own=false, so renumbered=false, record.nonce ('0x9') !== tx.nonce ('0x8') and line 248 throws TRANSACTION_MISMATCH on every attempt, for a speed-up with the identical calldata and for a cancellation alike. "Check original transaction" polls a hash that never gets a receipt; clearUnknownAfterWalletCheck returns early (the v2 record has a hash); the address stays locked in the GOLD panel and the staking pane until localStorage is cleared. The information needed is already seen and discarded: while the original is pending, line 232 returns false on the wallet's own copy of HASH, whose nonce field reports the renumbered 0x9. Fix: when the wallet's copy of the OWN hash (record.hash===tx.hash, record.from===tx.from, valid record.nonce !== tx.nonce) is seen at line 232, persist the learned nonce into the stored intent under the intentId guard (savePending) and use it for the pasted-hash nonce check; or equivalently accept a pasted finalized hash at nonce N when the own hash's wallet record reports nonce N. Keep every other pasted-hash rule (sender, chain, finality, canonical block).

**Reproduction**

Fixture of economy-wallet-recovery.test.mjs. prepare(payPlan()); confirm() -> stored {nonce:'0x8', hash:HASH}. (1) Wallet copy of the original, unmined and renumbered: candidate({hash:HASH,to:sent.to,input:sent.data,nonce:'0x9',patch:{chainId:undefined,blockHash:null,blockNumber:null}}) with eth_getTransactionReceipt answering null; checkReceipt() -> phase 'unknown', PENDING_TRANSACTION, stored nonce still '0x8'. (2) Player speeds it up: candidate({hash:OTHER,to:sent.to,input:sent.data,nonce:'0x9'}) (same calldata, status 0x1, finalized block 0x20 canonical); checkReceipt(OTHER). Expected: the same-nonce-replacement treatment that nonce '0x8' gets (control: {hash:OTHER,...,nonce:'0x8'} -> phase 'confirmed', store 0). Actual: {phase:'unknown',error:'TRANSACTION_MISMATCH',store:1}, stored hash HASH, stored nonce '0x8'. A cancellation {hash:OTHER,to:A,input:'0x',nonce:'0x9'} -> identical; clearUnknownAfterWalletCheck() -> no change; prepare -> PENDING_TRANSACTION.

### 4. Low: finishReceipt: a pasted same-nonce speed-up of a smart-account-routed payment is judged "replaced" and the GOLD panel drops the "+N crediting" note although the receipt carries this call's exact Paid

`Submission16/source/src/world/economyWallet.ts:241`

```
  const effect=own&&r.status==='0x1'&&intendedEffect(tx,(receipt as {logs?:unknown}).logs,this.config);
```

effect is computed only for the wallet's own returned hash (own&&...). The GOLD panel now offers the paste form (question 2) and the owner's only real wallet path is MetaMask with smart-account routing (AUDIT_REQUEST section 5). A speed-up of such a routed payment keeps the nonce and the routed to/input (the delegation call) under a new hash, which the player pastes as the form asks. For that hash own=false and same=false, effect is skipped, so done=false: after the finality wait the receipt is judged phase 'replaced' / TRANSACTION_REPLACED and the record is released; WorldGoldPanel's failed effect (paymentFailed -> 'replaced', line 92) then drops the order's note by order id and by sentHash. The player reads "The original payment did not complete as it was sent", the top bar loses "+N crediting", while the EMBEO was paid and the ledger credits the order at finality: the "ended while a real payment is on its way" case of question 3, reached through the new paste form. The shortcut the comment protects is the finality skip, but the finality block (lines 253-258) already runs for every non-own hash regardless of done, and a Paid event for this orderId, this sender and this amount at this sender's intent nonce cannot belong to an unrelated intent. The test at economy-wallet-recovery.test.mjs lines 100-103 enshrines the non-confirmation. Fix: compute the effect for every receipt (`const effect=r.status==='0x1'&&intendedEffect(tx,(receipt as {logs?:unknown}).logs,this.config);`), keep renumbered requiring own, and let finality gate the non-own case as it already does; update that test to expect 'confirmed' after finality and TRANSACTION_NOT_FINALIZED before it.

**Reproduction**

Fixture of economy-wallet-recovery.test.mjs. prepare(payPlan()); confirm() -> intent {nonce:'0x8', to:PAY, data:pay(OID,1000), hash:HASH}. candidate(routed([effect.paid(OID,1000n)],{hash:OTHER})): OTHER at nonce 0x8, to DELEGATION, routed input, status 0x1, exactly one Paid(OID, A, token) log with treasury+1000 data, finalized block 0x20 canonical. checkReceipt(OTHER). Expected: phase 'confirmed', step 'execute', note kept and marked mined. Actual: {phase:'replaced',error:'TRANSACTION_REPLACED',hash:OTHER,sentHash:HASH,store:0}; with finality not yet reached: TRANSACTION_NOT_FINALIZED. Control: the same speed-up sent directly ({hash:OTHER,to:PAY,input:pay calldata,nonce:'0x8'}) -> phase 'confirmed'. In the panel, phase 'replaced' with step 'execute' makes paymentFailed true, so dropPendingCredit(order) and dropPendingCreditByHash(HASH) run and pendingGold becomes 0 for a paid order.

### 5. Low: WorldGoldPanel: a late "+N crediting" note created from a pasted replacement hash is never marked mined (the note is keyed by wallet.hash, marking uses sentHash), so the after-expiry rule drops it in

`Submission16/source/src/world/WorldGoldPanel.tsx:94`

```
 useEffect(()=>{if(data&&mined&&sent)markPendingCreditMined(data.wallet,sent);},[data,mined,sent]);
```

The tracking effect (lines 85-87) creates the note with payingHash = paymentHash(wallet) = wallet.hash, which after checkReceipt(pastedHash) judged a same-nonce speed-up is the pasted hash H2 (finishReceipt sets hash:candidate, sentHash:tx.hash). The mined effect (line 94) marks by sent = wallet.sentHash ?? wallet.hash = the original hash H1. The two agree only while the original note {order,H1} still exists (trackPendingCredit dedups by order, and mark(H1) hits it). Once the H1 note is gone, dropped by the 60-minute after-expiry rule, the (order,hash,phase) key added for finding 6 creates a fresh note {hash:H2, mined:undefined}; markPendingCreditMined(H1) finds nothing; settlePendingCredits in the same commit (line 96) sees !mined && status 'pending' && now > expires_ms+60 min and removes it at once, and the once-key prevents re-creation. The top bar and purchase page show no "+N crediting" for a payment the wallet saw mined with its effect, contradicting the design (pendingGoldCredit.ts lines 9-10, 49-50: a mined payment keeps its note until the 2 h cap) and the claim in AUDIT_REQUEST section 1, finding 6. The own-hash late confirmation is unaffected (hash and sentHash coincide), which isolates the key mismatch as the cause. Display only, no entitlement. Fix: mark both hashes mined (markPendingCreditMined for wallet.hash as well as sent), or track and mark by the same hash (use sent in the tracking effect and its once key), or mark by order id.

**Reproduction**

Replay of the panel's effect order (track -> mark -> settle) against the shipped pendingGoldCredit.ts under a Map-backed localStorage. Order O expires at E, status 'pending', gold '1000'. (1) E-1 min, wallet {phase:'pending',step:'execute',hash:H1}: one note. (2) E+61 min, wallet unknown: settle drops the unmined note (by design), 0 notes. (3) E+62 min, wallet after checkReceipt(H2) judged same-calldata: {phase:'confirmed',step:'execute',hash:H2,sentHash:H1}: trackPendingCredit(addr,O,H2,'1000') adds {hash:H2}; markPendingCreditMined(addr,H1) changes nothing; settlePendingCredits(addr,[O]) removes it. Result: notes [], pendingGold 0n. Expected: one note, mined, pendingGold 1000n until the ledger credits or the 2 h cap. Control with the own hash (step 3 with hash:H1,sentHash:H1): note {hash:H1,mined:true}, pendingGold 1000n. Variant: H2 confirmed at E+10 min while the H1 note still exists -> the H1 note is marked mined and survives E+61 (the mismatch only bites after the original note is gone).

### 7. Low: deploy.mjs: the NODE_OPTIONS preload rule is bypassed by double-quoted flags and misses --snapshot-blob and --openssl-config, so code still runs inside npm test, tsc, vite build and wrangler deploy wi

`Submission16/source/scripts/deploy.mjs:63`

```
const PRELOAD=/(^|\s)(--import|--require|-r|--loader|--experimental-loader)(=|\s|$)/;
```

Finding 8 asked that a NODE_OPTIONS which preloads code be refused on a real deploy, because every step inherits process.env (runStep line 110-111, run line 175). PRELOAD matches the five flags only when preceded by the start of the string or whitespace and followed by '=', whitespace or the end. Two gaps: (1) Node's NODE_OPTIONS parser strips double quotes before matching options, so '"--require" /x.cjs', '"--import" ./x.mjs' and '--max-old-space-size=4096 "-r" ./x.cjs' are accepted by Node and preload the file, yet buildEnvNames returns [] for them; only the quoting defeats the check. (2) Node accepts --snapshot-blob=<file> in NODE_OPTIONS (verified: a missing file gives "Cannot open ...", whereas --env-file is "not allowed in NODE_OPTIONS"); a blob built with --build-snapshot carries code registered through v8.startupSnapshot.addDeserializeCallback that runs at startup of every process started with it, before the entry script. --openssl-config=<file> is likewise accepted and loads an OpenSSL configuration that can load provider/engine modules. None of these is named, so a real deploy proceeds, the preloaded code runs inside vite build and wrangler deploy (it can rewrite dist/ or the Worker bundle between hashing and upload), and the record's buildEnv says [] while its hashes name output no commit explains. Not a false refusal; a missed refusal in both directions. Fix: normalise before matching (e.g. PRELOAD.test(String(env[k]).replace(/"/g,' '))) or match (^|[\s"])(--import|--require|-r|--loader|--experimental-loader|--snapshot-blob|--openssl-config)(["=\s]|$); better, refuse any NODE_OPTIONS that is not on a short allow-list (--max-old-space-size, --enable-source-maps) and list the flag names in manifest.buildEnv on a dry run; add the quoted and snapshot forms to the environment unit test.

**Reproduction**

import {buildEnvNames} from 'Submission16/source/scripts/deploy.mjs'. buildEnvNames({NODE_OPTIONS:'"--require" /tmp/pre.cjs'},'linux') -> [] (expected ['NODE_OPTIONS']); same for '"--import" ./x.mjs', '--max-old-space-size=4096 "-r" ./x.cjs', '--snapshot-blob=/tmp/snap.blob' and '--openssl-config=/tmp/x.cnf'; control '--require /tmp/pre.cjs' -> ['NODE_OPTIONS']. Node v24.21.0: printf 'console.error("PRELOADED")' > /tmp/pre.cjs; NODE_OPTIONS='"--require" /tmp/pre.cjs' node -e 'console.log("ok")' prints PRELOADED then ok (also for the "-r" and "--import" forms). Snapshot: snap-entry.js registers require('node:v8').startupSnapshot.addDeserializeCallback(()=>writeFileSync('/tmp/PRELOADED', 'ran inside '+process.argv.join(' '))); node --snapshot-blob snap.blob --build-snapshot snap-entry.js (blob 6,397,229 bytes); NODE_OPTIONS=--snapshot-blob=snap.blob node -e "console.log('build step ok')" prints 'build step ok' and /tmp/PRELOADED contains 'ran inside /usr/bin/node'. A deploy started from such a shell passes buildEnvRefusal and writes "buildEnv": [].

### 8. Low: deploy.mjs buildEnvNames: CLOUDFLARE_API_BASE_URL, CLOUDFLARE_ACCOUNT_ID, their CF_ aliases and WRANGLER_CI_OVERRIDE_NAME choose where (host, account, Worker name) the bundle is uploaded, yet are neit

`Submission16/source/scripts/deploy.mjs:65`

```
  return Object.keys(env).filter(k=>env[k]!==undefined&&env[k]!==''&&(new RegExp('^(CLOUDFLARE_ENV|WRANGLER_ENV|WRANGLER_BUILD_.+)$',i).test(k)||new RegExp('^NODE_OPTIONS$',i).test(k)&&PRELOAD.test(String(env[k])))).sort();};
```

Finding 8's fix refuses CLOUDFLARE_ENV by name because it "picks another Worker environment for the upload". Wrangler 4.143.0 reads three more variables that pick the upload target by name only: CLOUDFLARE_API_BASE_URL (deprecated alias CF_API_BASE_URL; getCloudflareApiBaseUrlFromEnv overrides https://api.cloudflare.com/client/v4 for every API request of the run, including the Worker upload and the response the script parses "Current Version ID" from), CLOUDFLARE_ACCOUNT_ID (alias CF_ACCOUNT_ID; overrides the account the Worker is deployed into) and WRANGLER_CI_OVERRIDE_NAME (mergeSharedConfigArgs: "Overriding using the CI provided Worker name", name2 = ciOverrideName, so the bundle is uploaded as another Worker). None is matched by buildEnvNames, so a real deploy proceeds, manifest.buildEnv is [], and the record names a commit, config hashes, dist and Worker hashes and a version id that may belong to another host, account or Worker; the record carries no account identifier by design (REDACTIONS.md), so nothing in it contradicts the misdirected upload. This answers question 4's "anything uploaded that the record does not name": the destination. No false-refusal risk on a normal operator shell that uses wrangler's own login, exactly like CLOUDFLARE_ENV. The deploy unit test (tests/deploy.test.mjs line 81) deliberately expects CLOUDFLARE_ACCOUNT_ID not to be listed, so for CI use the fix should list it in the manifest rather than refuse. Fix: add CLOUDFLARE_API_BASE_URL, CF_API_BASE_URL and WRANGLER_CI_OVERRIDE_NAME to the refused set, by name only as today; record CLOUDFLARE_ACCOUNT_ID / CF_ACCOUNT_ID (names) in manifest.buildEnv so the record names the target.

**Reproduction**

import {buildEnvNames,buildEnvRefusal} from 'Submission16/source/scripts/deploy.mjs'. buildEnvNames({CLOUDFLARE_API_BASE_URL:'https://api.example.invalid/client/v4'},'linux') -> [], buildEnvRefusal([],false) -> null; likewise for {CF_API_BASE_URL:'https://x'}, {CLOUDFLARE_ACCOUNT_ID:'abc'} and {WRANGLER_CI_OVERRIDE_NAME:'other-worker'}; control {CLOUDFLARE_ENV:'staging'} -> ['CLOUDFLARE_ENV'] and a refusal. Hence on a clean tree with npm ci done, CLOUDFLARE_API_BASE_URL=https://api.example.invalid/client/v4 node scripts/deploy.mjs (or CLOUDFLARE_ACCOUNT_ID=<other account>, or WRANGLER_CI_OVERRIDE_NAME=<other name>) passes every refusal at line 185, runs the suite, tsc and vite, and wrangler uploads to the overriding host/account/name; the record shows buildEnv [] and a version id taken from that output. Expected: refused like CLOUDFLARE_ENV on a real deploy, listed in manifest.buildEnv on a dry run. Wrangler source checked in the published wrangler-4.143.0 package (wrangler-dist/cli.js: getCloudflareApiBaseUrlFromEnv with deprecatedName CF_API_BASE_URL; getCIOverrideName used in resolveWorkerName and mergeSharedConfigArgs).

### 3. Info: finishReceipt: the wallet's own hash under a higher nonce with status 0x1 but neither the exact calldata nor the exact effect is locked for good instead of judged "replaced" after finality as at the i

`Submission16/source/src/world/economyWallet.ts:247`

```
  const renumbered=own&&nonce(record.nonce)&&nonce(tx.nonce)&&BigInt(record.nonce)>BigInt(tx.nonce)&&(same||effect||r.status==='0x0');
```

At the intent's nonce the own hash has three outcomes: same/effect -> confirmed; reverted -> error; anything else -> wait for finality, then 'replaced' and release (lines 253-266). The renumbered exception covers only the first two (same||effect||r.status==='0x0'), so the third outcome under a higher nonce falls through to the TRANSACTION_MISMATCH throw on every check. A higher nonce for the wallet's own answer to this prompt cannot be "an older transaction", so the receipt is this intent's slot either way; the only difference from the same-nonce path is a permanent lock without acknowledgement (v2 record with hash) instead of the 'replaced' verdict. Example: a smart-account batch that ran but emitted two Paid events for this sender (I-8 rule) or a delegation that succeeded without the intended call. Fix: let the nonce check pass for own&&nonce(record.nonce)&&BigInt(record.nonce)>BigInt(tx.nonce) regardless of same/effect/status, and leave done and the finality block to decide confirmed, error or replaced.

**Reproduction**

Fixture of economy-wallet-recovery.test.mjs. prepare(payPlan()); confirm() (stored nonce '0x8', HASH). candidate(routed([],{nonce:N})) i.e. receipt for HASH: status 0x1, from A, to DELEGATION, logs [], finalized block 0x20 canonical. With N='0x8': {phase:'replaced',error:'TRANSACTION_REPLACED',store:0}. With N='0x9': {phase:'unknown',error:'TRANSACTION_MISMATCH',store:1} on every checkReceipt(); clearUnknownAfterWalletCheck() is a no-op; prepare -> PENDING_TRANSACTION. Expected for N='0x9': the same 'replaced' verdict and released intent as for N='0x8'.

### 6. Info: WorldGoldPanel: a payment whose hash the wallet never returned gets no "+N crediting" note when it is later confirmed through the paste form after a reload, because the tracking effect needs the plan

`Submission16/source/src/world/WorldGoldPanel.tsx:85`

```
 useEffect(()=>{const o=payingOrder?data?.orders.find(o=>o.id===payingOrder):null;if(!data||!o||o.kind!=='topup'||!payingHash||o.status==='paid')return;
```

The note is created only when wallet.plan is a pay plan (payingOrder) and paymentHash(wallet) is set. A payment prompt that broadcast but answered nothing usable (INVALID_TRANSACTION_HASH, the page closed during the prompt) leaves a v2 record with hash null and no note; after a reload restorePending adopts it without a plan (adoptPending sets plan:null), so payingOrder is null. The new paste form is exactly the recovery for this state: the player pastes the wallet's hash, checkReceipt judges it 'confirmed' with step 'execute', but the effect at line 85 returns before trackPendingCredit, pendingGold stays 0, and purchaseStage without a plan falls back to 'review' or 'expired' instead of 'awaiting-credit', so neither the HUD nor the panel shows the crediting line the panel copy promises ("Meanwhile the top bar shows "+amount crediting""). The opposite of a false note, so Info. Fix: when a restored transaction is confirmed with step 'execute' and no plan, create the note from the order the receipt proves: decode the orderId from the record's pay calldata (word 0; available in the controller's transaction before it is cleared, or exposed on the state) and call trackPendingCredit(data.wallet, orderId, sent, o.gold) in the mined effect when no note exists.

**Reproduction**

State: localStorage v2 record {hash:null, step:'execute', to:paymentContract, data:pay(orderId,amount), nonce:'0x8'} for the wallet; page reloaded; order still 'pending'. restorePending() -> state {phase:'unknown', plan:null, hash:null, step:'execute'}; payingOrder = wallet.plan?.kind==='pay' ? ... : null = null. Player pastes the wallet's hash H (nonce 0x8, exact calldata, status 0x1, final): checkReceipt(H) -> phase 'confirmed', step 'execute', hash H, sentHash null. Expected per the panel copy: pendingCredit(wallet, orderId) non-null, HUD "+N crediting" until the ledger credits. Actual: the effect at line 85 computes o=null (payingOrder null) and returns; trackPendingCredit never runs; pendingGold(wallet)=0; markPendingCreditMined(wallet,H) marks nothing; purchaseStage(order,{plan:null,phase:'confirmed',step:'execute'}) is not 'awaiting-credit' (same=false), so the panel shows 'review' or 'expired'.

### 9. Info: deploy.mjs viteEnvNames: on Windows a lower-case vite_* shell variable is refused although Vite's loadEnv (case-sensitive startsWith on the enumerated key) never exposes it: the finding-11 false refus

`Submission16/source/scripts/deploy.mjs:58`

```
export const viteEnvNames=(env,platform=process.platform)=>Object.keys(env).filter(k=>(platform==='win32'?/^VITE_/i:/^VITE_/).test(k)).sort();
```

Finding 11 made the VITE_ prefix match case-sensitive except on win32, reasoning that Windows variable names "have none". That holds for direct lookups (process.env.X), but Vite does not look VITE_ variables up by name: packages/vite/src/node/env.ts loadEnv runs "for (const key in process.env) { if (prefixes.some((prefix) => key.startsWith(prefix))) env[key] = process.env[key] }", a case-sensitive comparison on the enumerated key. Node on Windows enumerates process.env with the names as they were created (only lookup is case-insensitive), so a variable created as vite_debug is enumerated as 'vite_debug', fails startsWith('VITE_') and never reaches import.meta.env. The win32 branch therefore refuses a clean deploy for a variable the build ignores, the same false refusal finding 11 removed on Linux; the test at tests/deploy.test.mjs line 77 pins this behaviour. The win32 case-insensitive branch in buildEnvNames is correct (wrangler and Node read CLOUDFLARE_ENV / WRANGLER_BUILD_* / NODE_OPTIONS by direct lookup). Impact is limited to an operator who keeps a lower-case vite_ name in a Windows shell. Not verified on a Windows machine; the Vite source and the viteEnvNames output were. Fix: use the case-sensitive /^VITE_/ on every platform (one regex, no platform branch), matching Vite's own rule.

**Reproduction**

import {viteEnvNames,viteEnvRefusal} from 'Submission16/source/scripts/deploy.mjs'. viteEnvNames({vite_debug:'1',PATH:'x'},'win32') -> ['vite_debug'] and viteEnvRefusal(['vite_debug'],false) -> the "VITE_* environment variables would change the build ... (vite_debug); unset them" refusal, so a real Windows deploy with `set vite_debug=1` exits 1 at the first gate (line 185). Expected: [] and no refusal, as on Linux (viteEnvNames({vite_debug:'1',PATH:'x'},'linux') -> []), because vite build on that machine tests key.startsWith('VITE_') on the enumerated key 'vite_debug', which is false.

---

Judge's submission `9610aebabca5329c2d893000bf5d6eee8fd4353868cef8bc21829e2afff4fe50`, 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.
