# Audit report

> Audit BOTH Solidity and TypeScript/D1/client at exact containing Git commit 8f89b6d47125763f8907f302030687fb5feec01a (World source d4b897e9cc1f3b066517e4abd276d67a95e72768). Read AUDIT_OBJECTIVE.en.txt, REQUIREMENTS.md, SCOPE.md and AUDIT_FINDINGS.md. Prior job17fb18b0-3f81-4545-aabe-fe3316b73e64/public5a9e8f74 reviewed the previous source, not these fixes. Source/local tests only: no signing, payments, deployment or automatic fixes.
>
> Re-audit two Medium fixes: direct/staged finalized invalid withdrawals must atomically roll back facts then durably halt INVALID_POSITION_WITHDRAWAL, blocking new quotes/debits/rewards. Reviewed earlier-start recovery may accept only that halt, preserving valid pending proof prefixes and ancestor history at the same confirmed boundary with completed settlement; FINALIZED_REORG remains refused. Resume exact late payment and future UTC GOLD once. Classify physical chain/token/paymentContract/treasury and payer/order/amount, not the scanner vault namespace. Shared-contract scan order/backfill must not mislabel the same physical successful payment as wrong-config/extra. Missing legacy owner registry must defer explicitly, never guess. Exercise the new regressions and old negative controls. INFO3/4 remain unchanged.
>
> 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.
>
> Historical GOLD recovery and payment reconciliation are now included: migrations0013, server/economy-recovery.ts, server/economy-reconciliation.ts, the local-only scripts/economy-recovery.mjs, the full RECONCILIATION.md runbook and executable regressions. Treat this new scope as needing independent review, not as covered by the old accepted Audit or the previous b46 local tests.
>
> 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, same finalized boundary, settled old/new days, 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 seventeen-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 | `8f89b6d47125763f8907f302030687fb5feec01a` |
| Job | `e70ee73c-8d49-4dae-8538-83588e95baa5` |
| Judged | 2026-10-17 09:58 UTC |
| Findings | 2 medium · 1 low · 1 info |

Four agents audited the code as it is at `8f89b6d`, 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: INVALID_POSITION_WITHDRAWAL halt that lands while an old-config UTC day is unsettled or partially settled can never be recovered: settlement is blocked by the halt and recovery refuses RECOVERY_SETTLE

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

```
 if(previous.firstDay!==null)for(let day=previous.firstDay;day<horizon;day+=DAY)if(!at(old,'economy_gold_days').some(r=>r.config_id===from.id&&r.day_start_ms===String(day)))return fail('RECOVERY_SETTLEMENT_INCOMPLETE');
 if(at(old,'economy_day_progress').some(r=>r.config_id===from.id&&r.completed!=='1'))return fail('RECOVERY_SETTLEMENT_INCOMPLETE');
```

Merged from audit_flow, audit_math and audit_economics (same mechanism, two triggers). build() requires every old-config day in [firstDay, floor(through_ms/DAY)) to have an economy_gold_days row (line 107) and every old economy_day_progress row to be completed (line 108). But once halted_reason is set, settleEconomy throws ECONOMY_HALTED (world-source/server/economy-ledger.ts:105) and the settlement page INSERT also requires halted_reason IS NULL (economy-ledger.ts:157). The two conditions are therefore mutually exclusive after the halt: the old namespace can never finish settlement, and the only sanctioned recovery path for this halt is permanently refused. The rebuilt shadow must match the halted cursor exactly (line 73), so the operator cannot choose an earlier boundary either. Two realistic triggers: (a) a day with more than 32 wallets or 64 positions needs several one-minute synchronize passes (ECONOMY_SETTLEMENT_WALLET_LIMIT/POSITION_LIMIT), and a finalized block carrying a withdrawal of a pre-deploymentBlock position arriving during that window leaves economy_day_progress.completed=0; (b) the ingest batch crosses UTC midnight and the following settleEconomy call in synchronize does not complete (Worker CPU/D1 error) before the next batch halts. Effect: the config stays halted with no code path out (no quotes, GOLD debits, rewards or fulfilment of already-sent EMBEO payments); GOLD for already-finalized, already-indexed days stays unissued for the wallets past the settled page. Principal withdrawal is onchain and unaffected. RECONCILIATION.md line 15 acknowledges the refusal and says a 'separately reviewed plan' must be drafted, but no tooling exists and the runbook forbids clearing the halt or forging settlement markers, so there is no supported remedy. The shipped regressions (haltedFixture) always settle before the halting batch, hiding the case. Relaxing is safe for the stated invariant: adjustments are computed as entitlement minus utc-reward journals actually issued (reference from.id:day:wallet), so unsettled/partial days contribute credited=0 or only the journaled wallets. Fix options: let settleEconomy finish days whose end <= state.throughMs under the INVALID_POSITION_WITHDRAWAL halt only (the cursor never advanced past valid facts), then keep today's completeness checks; or in build() treat old economy_day_progress rows with completed=0 and missing economy_gold_days rows below horizon as 'credited so far' instead of failing.

**Reproduction**

Node 24, world-source copy with real migrations via tests/d1-sqlite.mjs openD1 (viem 2.56.9 resolvable). cfg deploymentBlock '100', T=Date.UTC(2026,9,1). Variant (a): ingestEconomyEvents blocks 100-101 (toBlockHash H(101), throughTimestamp T+DAY) with 33 stake events for 33 distinct wallets (positionId 2..34, logIndex 0..32, timestamp T, 1 share each). settleEconomy(db,c,{nowMs:T+DAY}) -> {settledDays:0}; economy_day_progress has one row completed=0,last_wallet=0x..20 (32 wallets journaled, wallet 33 not). Ingest block 102 (throughTimestamp T+DAY+1000) with a 'withdraw' of positionId '1' (never indexed) -> rejects ECONOMY_HALTED, haltedReason INVALID_POSITION_WITHDRAWAL (expected). Then settleEconomy(db,c,{nowMs:T+DAY+2000}) -> throws ECONOMY_HALTED; wallet 33 gold '0', wallet 1 gold '1'. Shadow DB: cfg deploymentBlock '99', ingest 99-101 with stake(1,99) plus the same 33 stakes, settleEconomy 4 times until economy_gold_days has 1 row. prepareEconomyRecovery(db,shadow,c,next,{nowMs:T+DAY+2001}). Expected: a plan (totalDelta covering position 1 plus wallet 33's day) or any supported path. Actual: throws RECOVERY_SETTLEMENT_INCOMPLETE. Variant (b): ingest 100-101 with one stake(pos 2, T) through T+DAY, do NOT settle, ingest block 102 with withdraw(pos 1) -> ECONOMY_HALTED; settleEconomy -> ECONOMY_HALTED; shadow deploymentBlock 99 with stake(1,99,2 shares)+stake(2,100) settled; prepareEconomyRecovery -> RECOVERY_SETTLEMENT_INCOMPLETE (expected plan totalDelta '2'). Probe test (node --test) used: tests P1a/P1b in the judge's zz-probe.test.mjs printed 'P1a recovery -> RECOVERY_SETTLEMENT_INCOMPLETE' and 'P1b recovery -> RECOVERY_SETTLEMENT_INCOMPLETE'.

### 2. Medium: Shared payment contract: a non-owner scanner that ingests first freezes every exact payment to a still-pending quote as immutable 'matched', so a genuine extra payment (or an unfulfilled payment while

`world-source/server/economy-reconciliation.ts:23`

```
 WHEN o.status='paid' AND NOT EXISTS(SELECT 1 FROM economy_chain_events paid JOIN economy_config_registry pr ON pr.config_id=paid.config_id
  WHERE paid.id=o.paid_event_id AND paid.kind='payment' AND pr.chain_id=r.chain_id AND pr.payment_contract=r.payment_contract AND paid.tx_hash=e.tx_hash AND paid.log_index=e.log_index) THEN 'extra-payment' ELSE 'matched' END,e.at_ms
```

Merged from audit_permissions, audit_flow and audit_economics (same root cause). economyPaymentEvidenceStatement decides matched vs extra-payment from the quote's status at the moment the SCANNING config ingests the event, and the row is then immutable (economy_payment_case_no_change/no_delete) and de-duplicated physically by (chain_id,payment_contract,tx_hash,log_index) for every later scanner. Only the quote-owning config or its lineage can set status='paid' (economyOrderScope in economy-ledger.ts:79 and economy-staging.ts:56). So when a second legitimate vault config sharing the same EmbeoPayments contract scans a block before the owner, every exact payment to a still-pending quote is written as 'matched', including a second payment to the same quote. When the owner later ingests the same block it fulfils the quote with the first event and the evidence INSERT skips both physical keys, so the real extra payment stays 'matched' forever. The same events with the owner scanning first yield 'matched' + 'extra-payment', so the M2 claim 'classification independent of scanner order' does not hold for this branch. Consequences: economyPaymentCases filters classification<>'matched' so the payer never sees the case; recordEconomyPaymentDisposition refuses matched rows (PAYMENT_CASE_NOT_FOUND) so the operator cannot even annotate it; reconcileEconomyPayments reports remaining=0 so backfill never revisits it. Sub-case: if the owner is halted on INVALID_POSITION_WITHDRAWAL (exactly the window the M1 fix creates) or retired without lineage, a single exact payment is recorded 'matched' while the order stays pending with zero fulfilments, and both configs report {remaining:0,deferred:0}, which the runbook calls a completion condition. No money moves and no extra entitlement arises; this is an evidence-accuracy defect in the fixed mechanism. Fix: a scanner whose economyOrderScope does not cover the quote must not finalize the matched/extra-payment decision (restrict the evidence INSERT to o.id IS NULL OR quote in scope, leaving out-of-scope exact payments to the owner's fulfilment batch/backfill, as the missing-registry case already defers), or require 'matched' to mean o.status='paid' AND o.paid_event_id resolves to the same physical chain/contract/tx/log and otherwise record a distinct visible classification.

**Reproduction**

Node 24, world-source copy with real migrations. owner=cfg(stakingContract 0x11..11, paymentContract 0x22..22), other=cfg(stakingContract 0x44..44, same paymentContract). Both ingest empty 100-101 (through T+DAY). quote=createEconomyOrder(db,owner,{wallet W,requestId 'audit-shared-contract-002',serviceId 'audit-local',currency 'EMBEO',nowMs T+DAY+10}) (amount 1e18). p1={kind payment, tx H(3000), logIndex 1, block 102, orderId quote.id, wallet W, amount 1e18, token ECONOMY_TOKEN, receiver ECONOMY_TREASURY, timestamp T+DAY+1000}; p2 same with tx H(3001), logIndex 2. ingestEconomyEvents(db,other,{102..102,[p1,p2]}) then ingestEconomyEvents(db,owner,{102..102,[p1,p2]}). Expected (and observed with owner first): economy_payment_cases p1='matched', p2='extra-payment'; economyPaymentCases(db,W) lists p2. Actual with other first: both rows classification='matched'; order paid by p1, fulfilments 1; economyPaymentCases(db,W) === []; recordEconomyPaymentDisposition({eventId:`${other.id}:${H(3001)}:2`,action:'note',...}) throws PAYMENT_CASE_NOT_FOUND; reconcileEconomyPayments(owner) and (other) both {remaining:0,deferred:0}. Sub-case: quote 'audit-shared-contract-001', only p1, ingested by other only (owner halted/not scanning): economy_payment_cases = [{classification:'matched',config_id:other.id}], economy_orders.status='pending', paid_event_id null, fulfilments 0, reconcile both {remaining:0,deferred:0}, player paymentCases length 0. Probe tests P3a/P3b in the judge's zz-probe.test.mjs printed exactly these values. The shipped regression (economy-audit2-medium.test.mjs 'other first') only asserts after both configs scanned one single payment, so it does not observe either state.

### 3. Low: Invalid withdrawal in a late-start config's very first finalized batch halts at the empty sentinel boundary (last_hash='', through_ms=0) that earlier-start recovery can never match

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

```
 if(!a||!b||(a.halted_reason&&a.halted_reason!==INVALID_POSITION_WITHDRAWAL)||b.halted_reason||a.last_block!==b.last_block||a.last_hash!==b.last_hash||a.through_ms!==b.through_ms||!/^0x[0-9a-f]{64}$/.test(a.last_hash??''))return fail('RECOVERY_FINALIZED_BOUNDARY_MISMATCH');
```

From audit_math; reproduced. When the first ingestEconomyEvents batch (or first staged block) for a config contains a withdrawal of a position deposited before deploymentBlock, the whole batch rolls back including the economy_chain_state seed row, and haltEconomyChain (economy-ledger.ts:20) creates the row with sentinel values last_block=deploymentBlock-1, last_hash='' and through_ms=0. build() requires a.last_hash to be a 32-byte hash and the shadow to have identical last_block/last_hash/through_ms; a rebuilt indexer always stores a real hash and timestamp, so no shadow can satisfy it and prepareEconomyRecovery always throws RECOVERY_FINALIZED_BOUNDARY_MISMATCH. This is precisely the scenario the recovery was built for (deploymentBlock chosen after the vault went live). Impact is limited because the halted namespace holds no events, journals, bindings or day_progress, so an operator can start a fresh config at the earlier block without a recovery (settleEconomy's binding insert and requireRunning do not object). What is lost: EMBEO/GOLD quotes created under the halted config before its first ingest (createEconomyOrder succeeds with the default unhalted state) remain pending and are not in the new config's economyOrderScope, so a payment for them is only ever recorded as evidence, never fulfilled, and RECONCILIATION.md line 15 documents only 'draft a separate reviewed plan'. Fix: when the halted namespace has no confirmed block (last_hash='') and no events/journals, accept a shadow whose boundary is any finalized block, or have the halt record the real (deploymentBlock-1) block identity so the ordinary boundary check can match.

**Reproduction**

Node 24, real migrations. c=cfg(deploymentBlock '100'). q=createEconomyOrder(db,c,{wallet W,requestId 'audit-prehalt-quote-0001',EMBEO}) succeeds (no chain_state row yet). ingestEconomyEvents(db,c,{fromBlock '100',toBlock '100',toBlockHash H(100),throughTimestamp T+1000,events:[withdraw positionId '1' at block 100]}) -> ECONOMY_HALTED. economyChainState(db,c) === {lastBlock:'99',lastHash:'',throughMs:0,haltedReason:'INVALID_POSITION_WITHDRAWAL'}. For each shadow: next=cfg(deploymentBlock '99'); ingest 99..99 [stake(1,99,T)] through T, or 99..100 [stake(1,99,T),withdraw] through T+1000; settleEconomy(shadow,next,{nowMs:through}). prepareEconomyRecovery(db,shadow,c,next,{nowMs:T+5000}). Expected: a plan. Actual: RECOVERY_FINALIZED_BOUNDARY_MISMATCH in both cases; q stays status 'pending' under the halted config id. Probe P2 in the judge's zz-probe.test.mjs printed these values.

### 4. Info: INVALID_POSITION_WITHDRAWAL halt is scoped to one config id; a second vault config sharing the wallet-global GOLD balance keeps quotes, challenges and GOLD debits open for the same wallet

`world-source/server/economy-ledger.ts:37`

```
async function requireRunning(db:D1Database,config:EconomyConfig){if((await economyChainState(db,config)).haltedReason)throw new EconomyConflict('ECONOMY_HALTED');if(await bind(db,'SELECT ancestor_id FROM economy_config_lineage WHERE ancestor_id=? LIMIT 1',config.id).first())throw new EconomyConflict('ECONOMY_CONFIG_REPLACED');}
```

From audit_permissions; reproduced. requireRunning and every SQL-level halted_reason guard (order insert economy-ledger.ts:240, challenge insert :221, GOLD debit :274, settlement page :157) are keyed on economy_chain_state.config_id, while economy_gold_accounts/economy_gold_journal are wallet-global. If the deployment runs a second legitimate vault config (different stakingContract, same token/treasury/paymentContract, as the objective's 'another legitimate vault' scenario allows), a halt on config A does not stop the same wallet from quoting and debiting under config B GOLD that was earned under A. This is a trust-boundary note rather than a bypass: INVALID_POSITION_WITHDRAWAL can only under-issue (missed earlier stakes), so no over-issued GOLD is at risk, and the Worker serves one reviewed config at a time. The objective and RECONCILIATION.md line 13 describe the halt as stopping 'new quotes, GOLD writes and settlement' without the per-config qualifier; the runbook should state the halt scope is the config id and that multi-config operation needs a cross-config halt policy before it is enabled.

**Reproduction**

Node 24, real migrations. A=cfg(stakingContract 0x11..11), B=cfg(stakingContract 0x44..44, same paymentContract). ingest A 100-101 with stake(pos 2, block 100, T) through T+DAY; settleEconomy(db,A,{nowMs:T+DAY}) -> wallet W gold '1'. ingest B empty 100-101. ingest A block 102 with withdraw(pos 1, unknown) -> ECONOMY_HALTED, economyChainState(A).haltedReason === 'INVALID_POSITION_WITHDRAWAL'. createEconomyOrder(db,A,{W,'audit-halted-gold-A-0001','audit-local','GOLD'}) -> rejects ECONOMY_HALTED (expected). createEconomyOrder(db,B,{W,'audit-halted-gold-B-0001','audit-local','GOLD'}) -> Expected under a global reading of the halt: ECONOMY_HALTED. Actual: returns {status:'pending',currency:'GOLD'} and the challenge/pay path under B is not blocked by A's halt. Probe P4 in the judge's zz-probe.test.mjs printed 'P4 order under B pending GOLD'.

---

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