# Audit report

> Audit BOTH Solidity and TypeScript/D1/client at exact containing commit 7611e2bfd519a9491eaa4be3595ae854636ba0f9 (World source fe920ae748155d7d7ed9aaa98ebb6f9fb2c3a81f). Read AUDIT_OBJECTIVE.en.txt, REQUIREMENTS.md, SCOPE.md, AUDIT_FINDINGS.md and world-source/docs/economy/AUDIT4_REMEDIATION.md. Previous job658db620/publicca2ed8a1/producte8afd02f reported 1 Medium,4 Low,1 Info and does not cover these new bytes. Source/local tests only; no signing,payment,deployment,publication or automatic fixes.
>
> Re-audit C01: retained bootstrap staged prefixes must survive C1->C2->C3 recovery. Resolve an ancestor sentinel using its immutable bootstrap-predecessor receipt, not its empty last_hash. Verify canonical registry, source/target/domain, finalized boundary/time and transitive lineage. Preserve prefix and journals, positive shortfall once, replay/rollback/future settlement. Corrupt hash/domain/from/missing receipt must fail with zero writes. Explicit canonical source input may identify its own pre-0013 namespace without a registry row; never infer an unrelated unregistered namespace. A compatibility test caught this during local review.
>
> C03/C04: another vault's reviewed retired progress and healthy/halted pending prefix must not block this vault. Recognize foreign retired data only through coherent receipts,lineage and current binding; a retired label is insufficient. Limit pending semantic checks to the relevant staking domain but retain the ALL-financial-table atomic snapshot guard. Foreign concurrent changes must still invalidate stale plans and roll back everything. Do not delete evidence or bypass related pending proofs.
>
> C05: an accepted empty same-vault sync can remain preserved without blocking recovery. Require a complete absence of economic footprint: events,positions/checkpoints,days/progress/pages,journal references,binding,pending/staged,orders/payment cases,recoveries/lineage. Pending unpaid orders also count. Check existing FK relationships for challenges,fulfillment,case actions and adjustments; do not infer emptiness from events alone. Any real/ambiguous economic history still fails. Keep original registry,ingestions and chain state immutable.
>
> C02: deterministic 128-row snapshots replace whole-table 512KiB values. Inside ONE atomic batch compare all table counts AND every complete page value; test append/delete/update/reorder across pages and concurrent spend/indexer changes. UTF8-bound bulk chunks preserve GLOBAL block/log ordering before splitting. All limits apply simultaneously:5000events,366days,16ancestors,64statements,512KiB/page or bound value,4MiB both snapshots,100guard parameters. 5000 is an upper cap, not guaranteed recovery capacity. Global journals/orders/cases count too. Read-only preflight uses the identical complete planner, reports actual table/page bytes,guard params,statements or a specific capacity failure. CLI exit0 ready/2 blocked,unchanged input hashes,no writable DB copy/no migration batch. Validate 600-existing/601-rebuilt events,exact512KiB/+1,total4MiB and realistic global growth; no deletion,raised constants or non-atomic multi-batch workaround. Actual production CPU/D1 scale remains a separate unproved gate; oversize recovery needs another reviewed atomic plan.
>
> C06: actual bilingual pane distinguishes pending-fulfillment from exceptions for pending-only,exception-only,mixed lists. Correct finalized payment says delivery pending/do not pay again; no blanket no-service heading or exception disposition for pending rows. Preserve no automatic refund/resend,unknown-send protections and real delivery semantics.
>
> Retain original A/B scope and adversarial controls. Chain1 canonical EMBEO0x2b82cdeb8477415d541799abb0cc48044c8ea5f3,18decimals,100000EMBEO/whole share,separate deposits,1 integer wallet-bound nontransferable/nontradeable GOLD per share held throughout complete UTC[00:00,next00:00),after finalized indexing. Midday deposits start next full day. Withdraw anytime; only withdrawn shares forfeit that UTC day,earlier GOLD/survivors retained. No trading pair,conversion or cashout. Principal separate from immutable treasury0x1C651928150DADDDA9C2C040a9D4901d862f8eC4. Names/oldSIWE grant no money authority.
>
> Review custody/reentrancy/exact token deltas,official pinned compiler/artifact projection,token/code/supply/immutables; complete finalized headers/receipts/logs,cursor/staging atomicity,UTC bigint paged settlement and durable halt. Recovery derives entitlement independently from events and subtracts EARNED journals plus prior adjustments,never spendable balance/carry; positive-only corrections,overissuance refusal,exact old proofs,controlled indexer-rebuilt input,atomic receipt/binding/lineage/import/history closure,retired-config final-write guards. Preserve pending immutable quotes and exact late same-slot EMBEO. No player event upload,privileged credit/refund route,remote D1 command or automatic config switch. Same chain/token/vault/paymentContract/treasury,earlier deploymentBlock only.
>
> Payment facts,physical chain/paymentContract/tx/log dedup,cases,cursor and fulfillment commit together. Exact pending payments remain deferred until owner/lineage fulfillment; private pending view cannot claim delivery. Wrong amount/payer/config/currency,unknown/extra payments grant no extra service; a later correct payment fulfills once and stays linked. Disposition is append-only metadata,no money/refund power. CLI refuses unresolved owner fulfillment. Duplicate exact logs are defensive input; actual EmbeoPayments rejects repeated payer/order/amount.
>
> Retain origin/session/final-SQL authorization,canonical GOLD personal_sign binding all authority/time fields,atomic debit/journal/challenge/fulfillment,completed signed retries after expiry paid once while expired NEW spending fails,clock lowerbounds,opaque immutable orders/explicit quote renewal. At every await recheck account/chain/provider/session/halt; exact approval and separate confirmation,persisted nonce intent/WebLocks,no unknown-send resend,finalized replacement proof,strict JSON and private error/case views.
>
> Report severity,paths/lines,prerequisites,reproducible positive/negative controls and gaps. Node24 native EDR/SQLite runner has19 selected financial files; partial public source is separate from full private World. Accepted exact-source review,physical wallet extension nonce/replacement,actual Workers CPU/D1/large receipt/recovery capacity,RPC completeness,mainnet token plus deployed new-contract runtime/immutables,catalogue and backup/migration/config/postflight remain independent release gates. Nothing is authorized to go live by this audit.

| | |
|---|---|
| Repository | https://github.com/tungweb3/imd-ember-world-review.git |
| Commit | `7611e2bfd519a9491eaa4be3595ae854636ba0f9` |
| Job | `df50e1ba-1df3-4cfc-8d6d-5180523699d8` |
| Judged | 2026-10-17 21:20 UTC |
| Findings | 3 low |

Four agents audited the code as it is at `7611e2b`, 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: C-05 emptySync accepts same-vault sync metadata that contradicts the rebuilt finalized chain (same height/different hash, or empty range covering rebuilt events)

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

```
  /^0x[0-9a-f]{64}$/.test(state.last_hash!)&&integer(state.last_block)>=BigInt(c.deploymentBlock)&&clock(state.through_ms)>=0&&ingestions.length>0&&ingestions.every(r=>r.accepted==='1');
```

The C-05 tolerance for an accepted empty same-vault sync only checks that the stray config's economy_chain_state row has a well-formed last_hash, last_block >= its deploymentBlock, a non-negative through_ms and only accepted ingestions. It never cross-checks that finalized claim against the rebuilt boundary the plan imports. Two contradictory cases are therefore treated as 'provably empty': (A) the stray asserts block 101 finalized with hash H(777777) while the rebuilt asserts block 101 with hash H(101) (two finalized identities for one height of one vault); (B) the stray asserts an accepted EMPTY range 98..105 while the rebuilt carries stake events at blocks 99 and 100 inside that range. Both are ambiguous finalized evidence about the same chainId:token:vault domain, which the objective says must still fail ('Any real/ambiguous economic history still fails'; requirement 12 requires any header at an announced finalized height to match the complete finalized identity). The planner proceeds, writes binding/lineage/adjustments and leaves the contradictory row in place instead of failing closed with RECOVERY_AMBIGUOUS_HISTORY. Impact is bounded: no GOLD is mis-issued by this alone and the stray cannot settle afterwards (ECONOMY_CONFIG_CONFLICT), but the 'provably empty' gate accepts evidence that disproves the input being imported, so a forked or corrupt indexer run is hidden from operator review. Fix: in emptySync (or at the call site on line 163) require coherence with the rebuilt boundary b: fail when state.last_block===b.last_block && state.last_hash!==b.last_hash; fail when integer(state.last_block)>integer(b.last_block) unless explicitly documented as an unverifiable-beyond-horizon exception; and fail when any rebuilt economy_chain_events row has block_number between BigInt(c.deploymentBlock) and integer(state.last_block). The existing positive fixture (stray at deploymentBlock 150 beyond the boundary) would need that exception or a last_block<=b.last_block constraint. Reproduced by the judge against commit 7611e2b with Node 24 and tests/d1-sqlite.mjs; merged from the audit_economics specialist finding (reproduction matched exactly).

**Reproduction**

Node 24, world-source/, helpers identical to tests/economy-audit4-recovery.test.mjs (cfg/stake/bad/ingest/complete). (1) c=cfg({deploymentBlock:'100'}), next=cfg({deploymentBlock:'99'}), stray=cfg({deploymentBlock:'98'}), same chainId/token/stakingContract/paymentContract/treasury. (2) ingest(db,c,[stake(2,100,T)],100,101,T+DAY); complete(db,c); ingest(db,c,[bad()]) rejects ECONOMY_HALTED. Rebuilt: ingest(shadow,next,[stake(1,99,T,2),stake(2,100,T)],99,101,T+DAY); complete(shadow,next) -> shadow economy_chain_state last_block 101, last_hash H(101), events at blocks 99,100. (3A) ingestEconomyEvents(db,stray,{fromBlock:'98',toBlock:'101',toBlockHash:H(777777),throughTimestamp:T+DAY,events:[]}) -> stray state last_block 101, last_hash 0x...bde31. (4) prepareEconomyRecovery(db,shadow,c,next,{nowMs:T+DAY+1001}) then applyEconomyRecovery with the plan hash. Expected: reject RECOVERY_AMBIGUOUS_HISTORY, zero writes. Actual (observed): plan accepted totalDelta '2', batch applied, SUM(economy_gold_accounts.balance)=3, stray row retained unchanged. (3B) instead ingest(db,stray,[],98,105,T+DAY+9000) (accepted empty 98..105 while rebuilt has stakes at 99,100). Expected: reject. Actual (observed): accepted and applied, sum 3. Positive controls: the shipped C-05 negatives (event/order/progress/pending) still reject; after apply, settleEconomy(db,stray,...) throws ECONOMY_CONFIG_CONFLICT, bounding impact.

### 2. Low: Unregistered same-vault config with real stake events is invisible to the RECOVERY_AMBIGUOUS_HISTORY gate (registry-scoped iteration; migration 0013 does not backfill)

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

```
 for(const c of known.values())if(domain(c)===domain(from)&&!ancestors.has(c.id)&&!emptySync(old,c))return fail('RECOVERY_AMBIGUOUS_HISTORY');
```

The same-domain ambiguity check iterates only configs present in economy_config_registry ('known'). Migration 0013 creates the registry without backfilling rows for configs that existed before it, and the registry is only populated when a config next writes through economyConfigStatement. A pre-0013 same-vault config that ingested real stake events and was then abandoned therefore has economy_chain_state, economy_ingestions, economy_chain_events, economy_positions and economy_position_balances rows but no registry row, and line 163 never evaluates it. Day-progress rows of unknown configs are caught at lines 164-167, so settled (journaled) history is still blocked and no double issuance was found; but unsettled real events for the same vault pass as clean. In the reproduction the stray's accepted event is a stake at block 98, which is BELOW the rebuilt deploymentBlock 99: prior accepted evidence proves the rebuilt input is incomplete for this vault, yet the plan is accepted and applied, leaving position 80 permanently uncredited and the stray's evidence unreviewed. With the registry row present the identical state is rejected (RECOVERY_AMBIGUOUS_HISTORY), so the outcome depends only on whether a registry row happens to exist. The objective says explicit source input may identify its own pre-0013 namespace and the planner must not INFER an unrelated unregistered namespace; it does not say unregistered rows carrying real economic footprint may be ignored ('Any real/ambiguous economic history still fails'). Fix (preserving the no-inference rule): before line 163, fail with RECOVERY_AMBIGUOUS_HISTORY when any row of economy_chain_events/economy_positions/economy_position_balances/economy_orders/economy_payment_cases (and utc-reward journal references) names a config_id that is neither in known nor in ancestors, since its domain cannot be proven to differ from domain(from); a pre-0013 export must then be registered (or reviewed) explicitly before recovery. Related to the C-05 coherence finding but a distinct mechanism (registry scoping vs. metadata coherence).

**Reproduction**

Node 24, world-source/, same helpers as tests/economy-audit4-recovery.test.mjs. (1) c=cfg({deploymentBlock:'100'}), next=cfg({deploymentBlock:'99'}), stray=cfg({deploymentBlock:'98'}) same vault. (2) ingest(db,c,[stake(2,100,T)],100,101,T+DAY); complete(db,c); ingest(db,c,[bad()]) rejects ECONOMY_HALTED. Rebuilt: ingest(shadow,next,[stake(1,99,T,2),stake(2,100,T)],99,101,T+DAY); complete(shadow,next). (3) ingest(db,stray,[stake(80,98,T)],98,98,T) -> one real stake event row under stray.id. (4) Simulate the pre-0013 export shape exactly as the shipped compatibility test does: db.raw.exec('DROP TRIGGER economy_registry_no_delete'); DELETE FROM economy_config_registry WHERE config_id=stray.id. (5) prepareEconomyRecovery(db,shadow,c,next,{nowMs:T+DAY+1001}) then apply. Expected: RECOVERY_AMBIGUOUS_HISTORY, zero writes (a same-vault config with a real event is economic footprint). Actual (observed): plan ACCEPTED totalDelta '2', applied, SUM(balance)=3, stray event rows left unreviewed. Control (observed): with the registry row left in place the same state is rejected with RECOVERY_AMBIGUOUS_HISTORY.

### 3. Low: 4 MiB combined-snapshot cap makes the documented 5000-event recovery cap unreachable; effective ceiling is about 1,450 single-day stake events

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

```
export const RECOVERY_LIMITS=Object.freeze({events:5000,days:366,ancestors:16,statements:64,boundValueBytes:524288,planBytes:4194304,snapshotPageRows:128,guardParameters:100});
```

Every stake event costs rows on BOTH snapshot sides (original and rebuilt): economy_chain_events (whose payload column duplicates the row), economy_positions and economy_position_balances. planBytes (4,194,304 bytes for both snapshots together, accumulated in snapshot() at line 31 and charged per page) therefore saturates at roughly 1,450-1,500 single-day stake events with no withdrawals, payments, orders or journals, and the events:5000 cap at line 48 can never be the binding limit. Real histories (withdrawals add an event and a balance row on both sides; payments add events and cases; settlement adds journals and accounts) lower the ceiling further. Measured by the judge with the shipped planner and the d1-sqlite adapter (40 rotating wallets, one deposit day, one-day horizon, original halted with INVALID_POSITION_WITHDRAWAL like the Audit4 fixture): 600 events ready at 1,734,875 bytes / 42 guard params / 15 statements; 1,450 ready at 4,144,366 bytes / 63 guard params; 1,500 blocked planBytes actual 4,264,643 on economy_chain_events/rebuilt; 2,500 blocked planBytes 4,194,792 on economy_position_balances/rebuilt. REQUIREMENTS.md and AUDIT4_REMEDIATION.md do say 5000 is 'an upper cap, not guaranteed capacity', but neither states that the byte cap removes about 70% of that range even in the best case, so an operator sizing 'another reviewed atomic plan' from the 5000 figure will design work the planner can never execute. No funds are at risk: the refusal is explicit, read-only and leaves both inputs unchanged (verified by dump comparison). Fix options that preserve the design: document the measured effective ceiling (about 1,400 minimal-footprint stake events, fewer with withdrawals) next to the 5000 figure and in the preflight output; or stop charging rebuilt-side pages against planBytes (they are hashed into the plan identity but never bound into the guard statement, which only binds original pages at line 254), roughly doubling the usable range while keeping the 512 KiB, 100-parameter and 64-statement guards. Raising planBytes blindly is not recommended without D1 evidence. Merged from the audit_math specialist finding; the judge's measurements agree within 0.1%.

**Reproduction**

Node 24, world-source/, openD1() from tests/d1-sqlite.mjs for both databases. Config c (deploymentBlock 100) and next (deploymentBlock 99), same vault/token/payment/treasury. Ingest N stake events (shares '1', amount 1e23, all timestamps T=2026-10-01T00:00Z, 40 rotating wallets, log indices 0..) in chunks of 900 per block starting at block 100 (ingestEconomyEvents caps a batch at 1000 events), then an empty block with throughTimestamp T+DAY; settle to completion; halt the original with an invalid withdrawal (ECONOMY_HALTED). Rebuilt: same plus stake(1,99,T,2) from block 99. Call preflightEconomyRecovery(db,shadow,c,next,{nowMs:T+DAY+1001}). Expected from the documented caps: N=1500 and N=2500 (both below events:5000) are evaluated against the events cap. Actual (observed): N=600 -> ready, snapshotBytes 1,734,875; N=1450 -> ready, snapshotBytes 4,144,366; N=1500 -> blocked {metric:'planBytes',actual:4264643,limit:4194304,table:'economy_chain_events',side:'rebuilt'}; N=2500 -> blocked {metric:'planBytes',actual:4194792,limit:4194304,table:'economy_position_balances',side:'rebuilt'}. Both input dumps unchanged in every run. The events:5000 cap is never the reported failure for any N.

---

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