# Audit report

> Review Submission18/ only (read Submission18/AUDIT_REQUEST.md first): World source 96858bf, live as Worker b08f59ef. It answers audit 1ac5269f on Submission16 (6 Low, 3 Info) in be7d3b0; 96858bf changes page wording only. No server code changed (the uploaded Worker module is byte-identical to Submission16's); no Solidity changed.
>
> 1. src/world/economyWallet.ts finishReceipt: the wallet's own hash under another nonce is accepted under a higher nonce (then judged by the normal rules) or, under a lower nonce, only for a payment with this call's exact calldata or Paid effect. Can this confirm or release an intent for a transaction that is not this intent's own?
> 2. economyWallet.ts learnNonce: the intent learns a higher nonce from the wallet's pending copy of its own hash; a tab with the old nonce takes the stored intent over. Can the learned nonce be wrong, release an intent, or create a new permanent lock?
> 3. finishReceipt: the intended effect is now computed for pasted hashes too (still nonce, sender, finality and canonical-block checked). Can a pasted hash confirm something it should not?
> 4. src/world/WorldGoldPanel.tsx, pendingGoldCredit.ts: both hashes marked mined; paidOrder names the order after a reload. Any "+N crediting" for a payment that cannot be credited, the wrong order, or beyond the 2 h cap?
> 5. scripts/deploy.mjs: NODE_OPTIONS allow-list, more refused target variables, targetEnv. Any false refusal on a clean Windows or Linux deploy (CLOUDFLARE_ACCOUNT_ID is set by the operator), 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 | `26fd4b55b07509cecf33196677f222e41eebdf31` |
| Job | `f6bd50ad-af0b-4191-a0de-0e322c19c667` |
| Judged | 2026-10-27 19:24 UTC |
| Findings | 3 low |

Four agents audited the code as it is at `26fd4b5`, 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: a lower-nonce own hash of a REVERTED pay() with this call's exact calldata releases the intent at once, without finality, although a revert consumes no order id

`Submission18/source/src/world/economyWallet.ts:272`

```
  const lowerPayment=(same||effect)&&tx.step==='execute'&&tx.to===this.config.paymentContract;
```

Merged from audit_permissions, audit_math, audit_flow and audit_economics (four reports of the same mechanism). Question 1. The lower-nonce exception is justified (comment lines 267-270, AUDIT_REQUEST section 1 row 1) by the order id being one-time: a pay(orderId,amount) by this sender can succeed only once (OrderAlreadyPaid). The comment adds that a lower-nonce revert stays refused. That argument covers `effect` (status 0x1 only) but not `same`: `same` (line 253) is to/input/value equality and ignores r.status. So a lower-nonce own hash whose record is a REVERTED pay(OID,amount) with this intent's exact data sets lowerPayment and renumbered (line 273), passes the nonce gate (line 274), sets done via `same` (line 275), and because candidate===tx.hash and done, skips the finality/canonical-block block (line 280). The intent is deleted and the state becomes error/TRANSACTION_REVERTED. A reverted pay leaves the order unpaid, so an earlier failed attempt at the same order with identical calldata is exactly the stale-hash case the lower-direction rule is meant to refuse. The identical lower-nonce reverted STAKE is refused (TRANSACTION_MISMATCH), so the rule is inconsistent between the two. Impact: while this intent's real send (stored nonce) may still be pending, the address lock is dropped, the panel's paymentFailed() removes the +N crediting note, and prepare()/confirm() of the same order are allowed again because inspect()'s paid() is still false. The second payment then reverts OrderAlreadyPaid (gas lost) once the first mines, or pays a fresh order if the player requotes. No GOLD is credited without payment (the contract and the ledger bound it). Precondition: the wallet returns a hash of an older transaction (the wallet-bug class of section 2), so Low. The author's test tests/economy-wallet-recovery.test.mjs:315 ('lower, identical payment, reverted') pins the accepting behaviour, contradicting the code comment and the request text.

**Reproduction**

Harness: the author's fixture from tests/economy-wallet-recovery.test.mjs (lines 1-57), Node 24.21. Steps: prepare(payPlan()); confirm() (stored intent nonce 0x8, hash HASH, data pay(OID,1000)); f.finalized=false (finalized block 0x1f < receipt block 0x20); candidate({hash:HASH,to:PAY,input:encodeEconomyCall('pay',[OID,1000n]),nonce:'0x5',status:'0x0'}); checkReceipt(). Expected (comment lines 268-270, AUDIT_REQUEST row 1, consistent with the 'lower, identical stake' case): phase unknown, error TRANSACTION_MISMATCH, store size 1. Actual (observed): {phase:'error',error:'TRANSACTION_REVERTED',store:0,finalityReads:0}; then prepare(payPlan()) reaches phase 'confirm' and confirm() sends a second eth_sendTransaction with identical pay(OID,1000) data (sent.length 2). Contrast: the same candidate shape for a stake intent (to VAULT, deposit(1), nonce 0x5, status 0x0) gives {phase:'unknown',error:'TRANSACTION_MISMATCH',store:1}. Fix: require success for the lower direction: `const lowerPayment=r.status==='0x1'&&(same||effect)&&tx.step==='execute'&&tx.to===this.config.paymentContract;` and move the 'lower, identical payment, reverted' case (test line 315) into the refused list (TRANSACTION_MISMATCH, store 1). Higher-nonce reverted own hashes still release via BigInt(record.nonce)>BigInt(tx.nonce). Trade-off: a genuinely customised lower nonce whose payment reverted then stays locked, like the other lower-nonce cases the owner already accepts in section 2.

### 2. Low: WorldGoldPanel: a tab left on a replaced/reverted payment of order A deletes another tab's live "+N crediting" note for A on each ledger refresh

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

```
 useEffect(()=>{if(!data||!failed)return;if(payingOrder)dropPendingCredit(data.wallet,payingOrder);if(sent)dropPendingCreditByHash(data.wallet,sent);},[data,payingOrder,failed,sent]);
```

Question 4 (crediting note). The drop effect depends on `data`, which useGold replaces every 12 s while the tab is visible (line 30) and on focus/visibility. `failed` (paymentFailed) stays true for as long as this tab's controller sits in phase replaced, or error/TRANSACTION_REVERTED, with the plan still set; only a new prepare() in the same tab resets it. dropPendingCredit removes by order id, and notes live in localStorage shared by every tab of the wallet. Once a payment of order A is judged replaced/reverted in tab 1, the intent is released, so the player can pay the still-unpaid order A again from tab 2. Tab 2 tracks a note under the new hash H2. On tab 1's next refresh, line 94 runs dropPendingCredit(W,A) and the live note is gone. When tab 2 later reaches confirmed, its once-key (which includes the phase) re-adds the note, and tab 1 drops it again on its next refresh. The player sees no "+N crediting" for a payment that is on its way, for up to ~15 minutes until the ledger credits. Display only: no entitlement or ledger effect. Distinct from the F1 release: here the first payment really was replaced, and it is the second, valid payment's note that is lost.

**Reproduction**

Replay of the two effects against src/world/pendingGoldCredit.ts (unchanged since Submission16) with a shared in-memory localStorage, Node 24, NOW=Date.now(). Tab 1 wallet state {phase:'replaced',step:'execute',error:'TRANSACTION_REPLACED',plan:{kind:'pay',payment:{orderId:A}},hash:H1,sentHash:H1}: paymentFailed()=true, payingOrder=A, sent=H1. Tab 2 sends a new payment of A: paymentHash({phase:'pending',step:'execute',hash:H2})=H2; trackPendingCredit(W,A,H2,'1000',NOW) gives pendingGold=1000n. Tab 1 data refresh (line 94: dropPendingCredit(W,A); dropPendingCreditByHash(W,H1)) gives pendingGold=0n. Tab 2 confirmed re-track gives 1000n; tab 1's next refresh gives 0n again (observed: 1000n, 0n, 1000n, 0n). Expected: the note for live payment H2 stays until the ledger credits A or the 2 h cap. Fix: drop only the judged payment's own note, e.g. give dropPendingCredit a hash argument and remove only the entry whose orderId===payingOrder AND hash===sent (or wallet.hash). Or run the drop once per judged payment through the `tracked` ref, keyed on sent+phase, instead of on every `data` change.

### 3. Low: deploy.mjs: ESBUILD_BINARY_PATH and NAPI_RS_NATIVE_LIBRARY_PATH replace the bundlers that produce worker/index.js and dist/, yet are neither refused nor recorded

`Submission18/source/scripts/deploy.mjs:69`

```
const BUILD_ENV='^(CLOUDFLARE_ENV|WRANGLER_ENV|WRANGLER_BUILD_.+|CLOUDFLARE_API_BASE_URL|CF_API_BASE_URL|CLOUDFLARE_COMPLIANCE_REGION|WRANGLER_API_ENVIRONMENT|WRANGLER_CI_OVERRIDE_NAME)$';
```

Merged from audit_flow and audit_economics. Question 5 (anything uploaded that the record does not name). buildEnvNames refuses shell variables that change what is built (WRANGLER_BUILD_*, CLOUDFLARE_ENV, and NODE_OPTIONS outside a 4-option allow-list) because the commit does not name them (findings 7/8 of 1ac5269f, both Low). Two variables in the same class are missing. ESBUILD_BINARY_PATH: wrangler 4.143.0 bundles the Worker with esbuild 0.28.1, whose lib/main.js:1889 reads `process.env.ESBUILD_BINARY_PATH` and runs that program as the esbuild service. NAPI_RS_NATIVE_LIBRARY_PATH: rolldown, Vite 8's bundler, does `__require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)` in dist/shared/binding-*.mjs lines 93-94 (checked in rolldown 1.2.11, the version in the lockfile available here; the deploy ran vite 8.3.5, whose rolldown was not checked). runStep (line 122) passes `{...process.env}` to every step, so both reach tsc/vite/wrangler. The record then says buildEnv [] and dirty false for a worker/index.js and dist/ produced by a program the commit does not name. The output hashes are still recorded, so the bytes are named but their cause is not, and nothing stops the upload. No false refusal was found for the operator's clean setup. CLOUDFLARE_ACCOUNT_ID is recorded and not refused on linux and win32. NODE_OPTIONS=--max-old-space-size=4096 passes. Observed: buildEnvNames({Path:'x',CLOUDFLARE_ACCOUNT_ID:'a',NODE_OPTIONS:'--max-old-space-size=4096'},'win32') is [] and targetEnvNames(...) is ['CLOUDFLARE_ACCOUNT_ID'].

**Reproduction**

(1) Refusal: node -e "import('./scripts/deploy.mjs').then(m=>console.log(m.buildEnvNames({PATH:'x',ESBUILD_BINARY_PATH:'/tmp/ebs/fake.sh',NAPI_RS_NATIVE_LIBRARY_PATH:'/tmp/y.node'},'linux')))" prints [] (contrast: {NODE_OPTIONS:'--import ./x.mjs'} prints ['NODE_OPTIONS']). Expected ['ESBUILD_BINARY_PATH','NAPI_RS_NATIVE_LIBRARY_PATH']: a real deploy refused, a dry run listing them in manifest.buildEnv. (2) Effect, esbuild@0.28.1 (wrangler 4.143.0's locked esbuild): /tmp/ebs/fake.sh appends "fake esbuild invoked: $0 $*" to a log and execs the real @esbuild/linux-x64 binary. `ESBUILD_BINARY_PATH=/tmp/ebs/fake.sh node -e "require('esbuild').build({entryPoints:['/tmp/ebs/in.js'],bundle:true,write:false})"` prints build ok, and the log holds "fake esbuild invoked: /tmp/ebs/fake.sh --service=0.28.1 --ping". (3) rolldown 1.2.11 (npm pack) dist/shared/binding-BY0qR5iS.mjs:93-94: `if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) try { const overrideBinding = __require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);`. Fix: add ESBUILD_BINARY_PATH and NAPI_RS_NATIVE_LIBRARY_PATH to BUILD_ENV (line 69) and extend the environment unit test by these names. More robust: run each step with an explicit allow-listed environment instead of {...process.env}.

---

Judge's submission `9ac4ca588091e9ece3fd643a3463496ec02fa98ebb349990990245de2b551fc8`, 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.
