# Audit report

> Audit BOTH Solidity and TypeScript/D1/client at exact containing commit ca2ed8a1fd509e9d03989924b2ef1c77e4ab5037 (World source e8afd02f650fb8b3f9696b8e02004c226105889a). Read AUDIT_OBJECTIVE.en.txt, REQUIREMENTS.md, SCOPE.md and AUDIT_FINDINGS.md. Prior jobe70ee73c/public8f89b6d4 did not review these changes. Source/local tests only; no signing, payment, deployment or automatic fixes.
>
> Re-audit B01/03: INVALID_POSITION_WITHDRAWAL recovery accepts unissued/partial old UTC days using EARNED journals plus adjustments, never balance/carry; keep snapshots,positive-only deltas,old evidence,atomic retirement/new history closure and future settlement. Empty first-batch bootstrap requires a full namespace inventory and indexer-verified rejected-batch predecessor, not any shadow height; preserve pending quotes/challenges,lineage and staged prefix. B02: exact pending payments are deferred until actual owner/lineage fulfillment; counters and private pending-fulfillment view cannot claim completion. Retain physical dedup,wrong-payment privacy and immutable cases. CLI must refuse unresolved fulfillment. Actual EVM rejects duplicate exact payer/order/amount,so duplicate-log inputs are defensive; separately reproduce reachable single pending payment. B04: per-config halt,wallet-global GOLD,one fixed production config. Keep old negative controls and verify 32/33wallets,64/65positions,direct/staged,rollback/replay,second recovery,all scanner orders.
>
> Rules remain chain1 canonical EMBEO0x2b82cdeb8477415d541799abb0cc48044c8ea5f3,18decimals,100000EMBEO/whole share, separate deposits,1wallet-bound nontransferable/nontradeable GOLD per share retained throughout complete UTC[00:00,next00:00), after finalized indexing. Midday deposit first qualifies next full day. Withdraw anytime; only withdrawn shares lose that UTC day, survivors/prior GOLD stay; no inherited deposit eligibility, conversion or cashout. Principal is separate from immutable treasury0x1C651928150DADDDA9C2C040a9D4901d862f8eC4. Editable names and old SIWE grant no money authority.
>
> Previous fixes still require review: unsafe reward config repins cannot reissue historical GOLD; matching signed completed GOLD retries after expiry return paid once with all original authority/time bounds, while expired NEW spending fails; exact payer/order/amount payment replay key and paid(address,bytes32,uint256) getter; wrong manual transfers remain irreversible without entitlement/refund; expired pending quote returns ORDER_EXPIRED with explicit new-request renewal, immutable old/late paid history retained.
>
> Review historical GOLD recovery,reconciliation,migration0013 and the local-only tools with RECONCILIATION.md; prior reports do not cover this revision.
>
> Review the independent event-derived UTC entitlement model, comparison against EARNED journals plus prior adjustments (never spendable balance), positive-only corrections, overissuance refusal, exact retained old event proofs, matching finalized boundary or verified first-batch predecessor; settled rebuilt days and credited-so-far old journals only for the specified halt, all-financial-table SQL snapshot guard, atomic receipt/binding/lineage/import/day closure/adjustments and exact-hash replay. Preserve every original journal and immutable quote; only valid finalized events may complete pending quotes. Exercise concurrent spend/indexer changes, rollback/retry, second historical rebuild and another legitimate vault. Normal settlement must resume after the recovered horizon and never reissue historical days. Retired configs must fail final-write/indexer/page guards; independent onchain principal withdrawal remains unchanged.
>
> The controlled rebuilt input must originate from the existing finalized receipt-verifying indexer, not a player event upload. Local copies are not independent chain consensus proof. The internal module and offline SQLite tool add no HTTP route, privileged wallet, arbitrary credit parameter, RPC credential, remote D1 command, refund or automatic deployment. Review read-only input hashes and fresh side-by-side output, the private migration batch and the explicit reviewed-plan boundary. Actual production batch execution and config switch require separate review/authorization. Same chain/token/vault/paymentContract/treasury only; earlier deploymentBlock only. Payment-contract replacement is rejected, not silently migrated. Engineering caps:5000events,366days,16ancestors,64statements,512KiB per bound snapshot/value,4MiB snapshots total. Larger plans fail before writes; local throughput does not close production capacity.
>
> Review immutable payment facts/classification and chain/paymentContract/tx/log dedup in the SAME event/cursor/fulfillment transaction. Incomplete staging has no case/economic effect. Wrong amount/payer/config/currency, unknown orders and extra payments give no extra entitlement. A correct later exact payment fulfills once and stays linked to the original exception, not falsely marked refunded. Legacy evidence is rebuilt256/page with bounded operator-copy tooling. Disposition only appends a note or acknowledged-no-entitlement; it cannot move money, change an order, claim a refund or confer treasury control. Players see only their own latest50cases; another user's quote/result is withheld and GOLD prices are not displayed as EMBEO. Review strict client types and actual pane correction/case labels, unknown-send protections and original auth authority.
>
> Retain full original scope: custody/reentrancy/exact token deltas and code/supply/immutable binding; official pinned compiler and artifact projection; contiguous finalized headers/receipts/logs, staging/cursor atomicity, UTC bigint pages and halt; same-origin/session/final-write authority, canonical GOLD personal_sign and atomic debit/journal/fulfillment; opaque immutable orders and late same-slot EMBEO; account/chain/provider/session guards at every await, exact approval and separate confirmation, persisted nonce intent/WebLocks, no unknown-send resend, finalized replacement proof, strict JSON types and clock lowerbounds. See full objective for every negative control and stated trust limit.
>
> Report severity, exact paths/lines, prerequisites, reproducible adversarial/negative controls and scope gaps. The explicit eighteen-file financial runner uses pinned Node24/native EDR, not the old Node22 partial reproduction. Distinguish the complete private World gate from this selected partial public source. Both-language coverage, accepted exact-source report, real wallet extension nonce/replacement, actual Workers CPU/D1/large receipt capacity, provider completeness, mainnet token plus deployed new-contract runtime/immutables, catalogue and production migration/config/postflight remain independent release gates. No new source publication or live deployment is implied by preparing this packet.

| | |
|---|---|
| Repository | https://github.com/tungweb3/imd-ember-world-review.git |
| Commit | `ca2ed8a1fd509e9d03989924b2ef1c77e4ab5037` |
| Job | `658db620-bc5e-4dfd-ace6-7eae69968112` |
| Judged | 2026-10-17 15:32 UTC |
| Findings | 1 medium · 4 low · 1 info |

Four agents audited the code as it is at `ca2ed8a`, 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: Retained first-batch staged prefix permanently blocks every later recovery of the same vault (RECOVERY_NOT_CLEAN)

`world-source/server/economy-recovery.ts:100`

```
  const predecessor=bootstrap&&p.config_id===from.id?b:s;
```

B-03 deliberately keeps the committed staged prefix rows (economy_pending_blocks + economy_staged_events) of a first-batch halted config C1 as forensic evidence and no code path ever deletes them (RECONCILIATION.md forbids it; the shipped B03 tests assert pendingEconomyBlock(db,c) is unchanged after recovery). retainedPending() accepts such a row only if p.parent_hash equals the predecessor's last_hash. During the bootstrap recovery C1->C2 the predecessor is correctly the rebuilt shadow boundary b, but only while p.config_id===from.id. In any later recovery C2->C3, C1 is merely an ancestor, so the predecessor falls back to s = C1's own economy_chain_state row, which for a first-batch halt is the empty sentinel (last_hash=''). The real parent hash never equals '', retainedPending returns false, and line 105 fails the whole plan with RECOVERY_NOT_CLEAN. The comment at lines 94-97 ('Later recoveries may retain the same forensic prefix under a reviewed ancestor') and the objective's 'second historical rebuild' requirement are therefore broken specifically for the prefix path the feature creates: once a vault has gone through bootstrap-with-prefix, a second INVALID_POSITION_WITHDRAWAL halt or a later discovery that deploymentBlock must move earlier again has no reviewed recovery route, and the descendant config's GOLD settlement/spending stays halted. Fail-closed (no money moves). The shipped second-recovery test (Audit3 B01) covers only the non-bootstrap wallet33 fixture and the B03 prefix test never attempts a second recovery. Fix: when a retained pending row belongs to an ancestor whose chain_state is the bootstrap sentinel, compare parent_hash against the boundary recorded by that ancestor's recovery (economy_recoveries.report_json boundary.hash where from_config=ancestor and boundary.mode='bootstrap-predecessor'), not against the sentinel's empty hash. Merged from three specialists (audit_flow, audit_math, audit_economics) reporting the same mechanism.

**Reproduction**

Node 24, tests/d1-sqlite.mjs openD1 and the helpers of tests/economy-audit3-recovery.test.mjs. (1) c=cfg({deploymentBlock:'100'}), next=cfg({deploymentBlock:'99'}); create EMBEO order under c at T+1; stage block 100 offset 0 of totalLogs 2 with withdraw(position 1) -> committed prefix; stage offset 1 (payment) -> rejects ECONOMY_HALTED; chain_state is {lastBlock:'99',lastHash:'',throughMs:0,haltedReason:'INVALID_POSITION_WITHDRAWAL'}. (2) shadow: ingest [stake(1,99,T,2)] blocks 99..99 under next; prepareEconomyRecovery(db,shadow,c,next,{nowMs:T+DAY+1001}) -> boundary.mode 'bootstrap-predecessor', totalDelta '0'; apply. pendingEconomyBlock(db,c) still returns {number:'100',parentHash:H(99),nextOffset:1,totalLogs:2}. (3) Under next: stage block 100 fully (order paid), ingest empty block 101 through T+DAY+2000, settle to completion; GOLD sum 2. (4) third=openD1(), earlier=cfg({deploymentBlock:'98'}); ingest [stake(70,98,T), next's block-99 events] 98..99 through T, next's block-100 events 100..100 through T+DAY+1000, empty 101 through T+DAY+2000; settle to completion. (5) prepareEconomyRecovery(db,third,next,earlier,{nowMs:T+DAY+2002}). Expected: a plan with totalDelta '1' (the extra share-day for position 70). Actual: throws RECOVERY_NOT_CLEAN. Control: identical steps with step 1 using the direct (non-prefix) first batch -> 'OK delta 1'. Observed in my probe run: 'P1 direct: OK delta 1' / 'P1 prefix: FAIL RECOVERY_NOT_CLEAN'.

### 2. Low: Documented 5000-event recovery cap is unreachable: the 512 KiB per-table snapshot bound refuses plans at about 500 finalized events

`world-source/server/economy-recovery.ts:25`

```
 const value=await db.prepare(query).first<{value:string}>();if(!value||encode(value.value)>RECOVERY_LIMITS.boundValueBytes)return fail('RECOVERY_REQUIRES_LARGER_REVIEWED_PLAN');
```

RECOVERY_LIMITS (line 12), AUDIT_FINDINGS.md line 45, SCOPE.md and world-source/docs/economy/RECONCILIATION.md advertise 5000 rebuilt events as the single-run engineering cap. The cap that actually binds is boundValueBytes=524288 applied by snapshot() to the JSON of EVERY whole table in both databases, including economy_chain_events whose rows carry the full normalized payload (about 1,047 bytes per one-share stake row once CAST and JSON escaping are included). The events table therefore exceeds the bound at roughly 501 events, so the replay() events>5000 check (line 38) and the 366-day check can never be the operative limit. The same bound also applies to wallet-global tables (economy_gold_journal, economy_orders, economy_gold_challenges) across all vaults, so the effective ceiling for a production ledger is a few thousand journal rows in total. The refusal is safe (it happens before any write), but the documented capacity operators will plan against is wrong by an order of magnitude, a halted vault with more than about 500 finalized stake/withdraw/payment events cannot be recovered by this tool at all, and the runbook offers no batching alternative. Fix: either document the real byte-derived capacity in RECOVERY_LIMITS and the runbook, or guard each table with a digest (e.g. compare a SHA-256 of json_group_array inside the guard statement) and import economy_chain_events in bounded chunks so the 5000-event limit becomes real while keeping the atomic all-table guard. Merged from audit_flow and audit_math.

**Reproduction**

Node 24 with tests/d1-sqlite.mjs: from=cfg({deploymentBlock:'100'}), to=cfg({deploymentBlock:'99'}); ingest N one-share stake events (distinct wallets, block 100, timestamp T) into db under `from` via ingestEconomyEvents(fromBlock '100', toBlock '101', through T+DAY), settle to completion, then ingest withdraw of unknown position 1 at block 102 -> ECONOMY_HALTED (INVALID_POSITION_WITHDRAWAL); ingest the same N events plus stake(1,99,T,2) into shadow under `to` (99..101) and settle to completion; prepareEconomyRecovery(db,shadow,from,to,{nowMs:T+DAY+1001}). Measured with the exact snapshot() query: N=200 -> OK (shadow economy_chain_events JSON 209,693 bytes); N=400 -> OK (418,893 bytes); N=500 -> OK (523,493 bytes, 795 bytes under the bound); N=600 -> throws RECOVERY_REQUIRES_LARGER_REVIEWED_PLAN (628,093 bytes > 524,288; only economy_chain_events is over the bound). Expected per RECOVERY_LIMITS/AUDIT_FINDINGS/RECONCILIATION.md: plans up to 5000 events within the other caps are accepted. Actual: any plan above about 501 events is refused.

### 3. Low: Any retired ancestor config of ANOTHER vault makes this vault's recovery fail RECOVERY_AMBIGUOUS_HISTORY (day_progress guard ignores lineage)

`world-source/server/economy-recovery.ts:113`

```
 for(const r of at(old,'economy_day_progress'))if(!ancestors.has(r.config_id!)&&!at(old,'economy_reward_bindings').some(b=>b.config_id===r.config_id))return fail('RECOVERY_AMBIGUOUS_HISTORY');
```

The ambiguity guard accepts a foreign economy_day_progress row only if its config still holds a reward binding. A config retired by its own recovery loses the binding (line 165 moves it to the successor) but keeps its day_progress rows forever (old pages/progress must be preserved). Such rows belong to a different staking domain and have an economy_config_lineage row proving they were reviewed, yet the guard ignores lineage, unlike the equivalent settlement guard in economy-ledger.ts (line 122) which explicitly excludes `economy_config_lineage l WHERE l.ancestor_id=p.config_id`. Result: once any vault sharing the database has been recovered, every other vault can never be recovered (fail-closed refusal, no money moved). Production currently serves one fixed config, so this is operational only for the multi-config case the objective asks to exercise ('another legitimate vault'). Fix: mirror the ledger guard and also skip rows whose config_id appears as ancestor_id in economy_config_lineage.

**Reproduction**

Node 24, one shared openD1(), helpers of tests/economy-audit3-recovery.test.mjs. Vault B: Bc=cfg({stakingContract:'0x44..44'}), Bn=cfg({stakingContract:'0x44..44',deploymentBlock:'99'}); ingest [stake(2,100,T)] 100..101 through T+DAY, settleEconomy at T+DAY, ingest [withdraw()] at 102 -> ECONOMY_HALTED; shadow Bs with [stake(1,99,T,2),stake(2,100,T)] 99..101 settled to completion; prepare/apply recovery Bc->Bn succeeds; ingest withdraw under Bn. Vault A (default staking 0x11..11): same halt on its own namespace and its own shadow As. prepareEconomyRecovery(db,As,Ac,An,{nowMs:T+DAY+1002}). Expected: a valid plan (A's history is unambiguous; Bc is a reviewed retired ancestor in a different staking domain). Actual: throws RECOVERY_AMBIGUOUS_HISTORY from line 113 (Bc has day_progress rows, is not in A's ancestors and no longer has a binding). Observed in my probe run: 'P4: FAIL RECOVERY_AMBIGUOUS_HISTORY'.

### 4. Low: A pending staged block of ANY other config (other vault mid-staging, or another halted vault's retained prefix) fails this vault's recovery with RECOVERY_NOT_CLEAN

`world-source/server/economy-recovery.ts:105`

```
 if(states.some(r=>r.config_id===to.id)||at(old,'economy_pending_blocks').some(p=>!retainedPending(p))||at(shadow,'economy_pending_blocks').length||at(shadow,'economy_chain_state').length!==1||at(shadow,'economy_recoveries').length||at(shadow,'economy_orders').length)return fail('RECOVERY_NOT_CLEAN');
```

The cleanliness check requires EVERY row of economy_pending_blocks to satisfy retainedPending(), and retainedPending() (line 101) requires ancestors.has(p.config_id) and halted_reason===INVALID_POSITION_WITHDRAWAL. A pending block belonging to an unrelated staking namespace can never be in this vault's ancestor set, so the plan is refused. Two cases: (a) transient: a healthy other vault is in the middle of a busy-block staging (folded 128 logs per sync call per economy-rpc.ts), so vault A's recovery window depends on vault B's indexing cadence; (b) permanent: another vault halted on a staged fold with INVALID_POSITION_WITHDRAWAL keeps its prefix forever (never deleted, not even by its own recovery), so vault A can never be recovered. Fail-closed, no funds move, but it defeats the 'another legitimate vault' independence the objective requires. Fix: only apply retainedPending() to pending rows whose config_id is in `ancestors` or whose staking domain equals domain(from); rows of other staking domains should be ignored by the plan, exactly as line 113 already ignores bound foreign configs. Merged from audit_economics (low) and audit_permissions (info); same mechanism, both variants reproduce.

**Reproduction**

Node 24, one shared openD1(), helpers of tests/economy-audit3-recovery.test.mjs. Vault A: Ac=cfg(), An=cfg({deploymentBlock:'99'}); ingest [stake(2,100,T)] 100..101 through T+DAY, settle at T+DAY, ingest [withdraw()] at 102 -> ECONOMY_HALTED; shadow As with [stake(1,99,T,2),stake(2,100,T)] 99..101 settled. other=cfg({stakingContract:'0x44..44'}); ingest(db,other,[],'100','101',T+DAY); stageEconomyBlock(db,other,{number:'102',hash:H(102),parentHash:H(101),throughMs:T+DAY+1000,totalLogs:3,digest:'d'*64,offset:0,events:[two stakes at block 102]}) -> {complete:false,staged:2}. prepareEconomyRecovery(db,As,Ac,An,{nowMs:T+DAY+1001}). Expected: a plan for vault A with totalDelta '2'. Actual: throws RECOVERY_NOT_CLEAN. Control: identical fixture without the other vault's staged block -> plan OK with totalDelta '2'. Observed in my probe run: 'P5: FAIL RECOVERY_NOT_CLEAN' / 'P5 control: OK delta 2'. Variant (b): halt the other vault on a staged fold (stage page 0 of 2 with an unknown-position withdraw, then the final page) and the same refusal is permanent.

### 5. Low: One accepted EMPTY ingestion by a stray same-vault config permanently and irreversibly blocks the only historical-GOLD recovery path

`world-source/server/economy-recovery.ts:112`

```
 for(const r of registry)if(r.staking_contract===from.stakingContract&&r.token===from.token&&!ancestors.has(r.config_id!))return fail('RECOVERY_AMBIGUOUS_HISTORY');
```

Every ingestion and staging batch unconditionally executes economyConfigStatement (economy-ledger.ts and economy-staging.ts) and inserts an economy_chain_state row for whatever config it was called with, even for an accepted EMPTY batch (events=[]) carrying no economic history. Migration 0013 makes economy_config_registry rows undeletable (trigger economy_registry_no_delete) and economy_ingestions holds a foreign key to economy_chain_state, so neither row can ever be removed. prepareEconomyRecovery then treats ANY registry row with the same staking_contract+token that is not in the lineage of `from` as ambiguous history and fails before planning. The guard is meant to block ambiguous legacy reward history (REQUIREMENTS.md item 18), but it also fires on a config id that has no events, positions, journals, day_progress or bindings at all. A single mis-pinned deploymentBlock sync (a staging worker, a preflight run, a typo in a local copy) against the database makes the INVALID_POSITION_WITHDRAWAL recovery permanently impossible for that vault; no CLI command, migration or trigger exception can clear it, and the per-config halt of the real config persists. Availability/trust-gap defect in the remediation path, not a fund-loss path. Fix: when deciding ambiguity, ignore registry rows whose config id has no economic footprint (no events, positions, day_progress, gold_days, journal reference prefix, bindings, lineage) and whose chain_state is at its sentinel or only has empty accepted ingestions; keep rejecting any stray config that actually holds events, progress or journals; or register a config only once it commits at least one event/order.

**Reproduction**

Node 24 with tests/d1-sqlite.mjs: A=cfg({deploymentBlock:'100'}), B=cfg({deploymentBlock:'99'}). (1) ingest [stake(2,100,T)] under A 100..101 through T+DAY; settleEconomy(db,A,{nowMs:T+DAY}); (2) ingest withdraw of unknown position 1 at block 102 -> ECONOMY_HALTED (INVALID_POSITION_WITHDRAWAL). (3) shadow for B with stake(1,99,T,2)+stake(2,100,T) through block 101, settled fully. (4) Before planning: ingestEconomyEvents(db,cfg({deploymentBlock:'150'}),{fromBlock:'150',toBlock:'150',toBlockHash:H(150),throughTimestamp:T+DAY+5000,events:[]}) -> returns {replayed:false,eventCount:0} (accepted). (5) prepareEconomyRecovery(db,shadow,A,B,{nowMs:T+DAY+5001}). Expected: plan with adjustments for the missed 2-share lot (the stray config has no economic history). Actual: throws RECOVERY_AMBIGUOUS_HISTORY. (6) DELETE FROM economy_config_registry WHERE config_id=<stray> -> ECONOMY_CONFIG_CONFLICT; DELETE FROM economy_chain_state WHERE config_id=<stray> -> FOREIGN KEY constraint failed. Observed in my probe run: 'P6: FAIL RECOVERY_AMBIGUOUS_HISTORY', 'registry delete -> ECONOMY_CONFIG_CONFLICT', 'chain_state delete -> FOREIGN KEY constraint failed'.

### 6. Info: Payment reconciliation pane shows the blanket 'these payments grant no additional service' heading above pending-fulfillment rows

`world-source/src/world/EconomyPane.tsx:99`

```
These payments grant no additional service. No automatic refund or resend occurs. Check the order and wallet history first.
```

The introductory paragraph of the reconciliation list is rendered unconditionally whenever paymentCases is non-empty. Since this revision the list also carries the B-02 derived 'pending-fulfillment' rows (an exact finalized payment for the player's own pending quote that WILL be delivered once the owner/lineage indexer ingests it). For a player whose only row is pending-fulfillment the pane simultaneously says the payment 'grants no additional service' (heading) and 'Finalized payment received; service delivery pending' (row). The row label is correct; the heading is false for that row and could push a player toward re-sending EMBEO for a quote that is about to be fulfilled, which the design otherwise prevents (no unknown-send resend). No money moves and no server state is affected; wording/UX only. Fix: show the no-service/no-refund statement only when at least one row has an immutable exception classification (unknown-order, wrong-payer, wrong-config, wrong-currency, wrong-amount, extra-payment), or reword it to distinguish pending delivery from exceptions.

**Reproduction**

Run the shipped pane harness (tests/economy-pane.test.mjs fixture, language 'en'): resolve the config request with the enabled config, then resolve the snapshot with paymentCases=[{id:'pending-payment',transactionHash:'0x'+'f'.repeat(64),orderId:'0x'+'3'.repeat(64),actualAmount:String(10n**18n),expectedAmount:String(10n**18n),classification:'pending-fulfillment',createdMs:NOW,correctPaymentEvent:null,disposition:'unreviewed'}]. Observed rendered text (f.text) matches BOTH /These payments grant no additional service/ and /Finalized payment received; service delivery pending/ (probe appended to a copy of the harness printed 'heading present= true pending row present= true'). Expected: the no-service/no-refund heading is absent when every row is pending-fulfillment.

---

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