# Audit report

> Audit BOTH Solidity and TypeScript/D1/client at the exact immutable containing Git commit 5a9e8f74a4b8f6c9cedf7e661d3c683c5af311c0 (World source 6732aa3f75971a3ecb371dfaf9a7adcff77b7cb2). Read AUDIT_OBJECTIVE.en.txt, REQUIREMENTS.md, SCOPE.md and AUDIT_FINDINGS.md for the complete requirements and previous findings; do not limit this to Solidity or the new files. Prior accepted job592da0ae-6f65-4f4d-9636-b10cb6d9d71b/public143ac7e0 did not review this source. Source review/local tests only: no signing, payments, deployment, service reset or automatic fixes.
>
> 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 sixteen-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 | `5a9e8f74a4b8f6c9cedf7e661d3c683c5af311c0` |
| Job | `17fb18b0-3f81-4545-aabe-fe3316b73e64` |
| Judged | 2026-10-16 21:04 UTC |
| Findings | 2 medium · 2 info |

Four agents audited the code as it is at `5a9e8f7`, 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: Finalized withdrawal of a position staked before the pinned deploymentBlock stalls ingestion forever without a durable halt reason; the same block's exact EMBEO payment never fulfills and no payment c

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

```
  catch(error){if(await bind(db,'SELECT id FROM economy_ingestions WHERE id=?',ingestionId).first())return {replayed:true,eventCount:events.length};throw error;}
```

The migration 0010 trigger economy_event_withdraw (world-source/migrations/0010_world_economy.sql:79-84) RAISEs ECONOMY_INVALID_WITHDRAWAL when a withdraw event names a position for which this config has no economy_positions row. A position staked in a block below config.deploymentBlock is exactly such a row, and the new 0013 earlier-deploymentBlock recovery scope exists because that operator state is anticipated. EmbeoStakingVault.withdraw succeeds on-chain (the contract checks ownership itself), the finalized Withdrawn log is decoded by economy-rpc.ts and passed to ingestEconomyEvents, the whole block batch aborts, and the catch at line 69 rethrows the raw SQLite error. Nothing calls haltEconomyChain, so economy_chain_state.halted_reason stays NULL and every later synchronize() run rejects on the same block: a permanent, silent cursor stall. The design surfaces a changed finalized hash as a durable FINALIZED_REORG halt visible through chain.haltedReason, but this inconsistency is not surfaced at all: the pane only shows an ageing 'Chain data through' value, players keep being quoted EMBEO orders, and an exact-amount Paid event in the stalled block (or any later block) never completes its pending quote and never produces an economy_payment_cases row, so the immutable payment-facts guarantee fails for that payer. stageEconomyBlock's INSERT..SELECT fold (economy-staging.ts:54-55) fires the same trigger and stalls the same way. Prerequisite: operator pinned deploymentBlock above the real first stake, then any such early staker withdraws (fully or partially) after the pin. Fix: in ingestEconomyEvents/stageEconomyBlock detect ECONOMY_INVALID_WITHDRAWAL and call haltEconomyChain(db,config,'UNKNOWN_POSITION_WITHDRAWAL') so the state is durable, visible and recoverable via the 0013 path; or make the trigger tolerant of a withdraw for an unknown position (record the event without touching positions) so the block, cursor, payment fulfillment and payment case still commit. Merged from audit_permissions.

**Reproduction**

Node 24, world-source/ with node_modules installed: openD1() over all migrations, c=defineEconomyConfig({...deploymentBlock:'100'}). ingestEconomyEvents(db,c,{fromBlock:'100',toBlock:'101',toBlockHash:H(101),throughTimestamp:T+DAY,events:[stake(position 2, block 100, T)]}). order=createEconomyOrder(db,c,{currency:'EMBEO',...}). Then twice: ingestEconomyEvents(db,c,{fromBlock:'102',toBlock:'102',toBlockHash:H(102),throughTimestamp:T+DAY+1000,events:[withdraw(position 1, never staked in range, 1 share, block 102, logIndex 0), payment(order.id, exact 1e18, block 102, logIndex 1)]}). Expected: block 102 indexes (position 1 is a legitimate on-chain position) OR economyChainState().haltedReason is a durable code. Actual (observed): both attempts reject with 'Error: ECONOMY_INVALID_WITHDRAWAL'; economyChainState = {lastBlock:'101', haltedReason:null}; economy_orders.status for the order stays 'pending'; economyWalletSnapshot(db,c,W).paymentCases is empty. Probe output: 'attempt 0 error: Error: ECONOMY_INVALID_WITHDRAWAL / attempt 1 error: Error: ECONOMY_INVALID_WITHDRAWAL / state after { lastBlock: 101, haltedReason: null } / order status pending cases 0'.

### 2. Medium: Payment-case classification is per ingesting config but dedup is per payment contract: when another live config sharing the EmbeoPayments contract ingests first, the player's own fulfilling exact paym

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

```
 AND NOT EXISTS(SELECT 1 FROM economy_payment_cases p WHERE p.chain_id=r.chain_id AND p.payment_contract=r.payment_contract AND p.tx_hash=e.tx_hash AND p.log_index=e.log_index)
```

economyPaymentEvidenceStatement writes one immutable economy_payment_cases row per physical Paid log (UNIQUE(chain_id,payment_contract,tx_hash,log_index) in migrations/0013_economy_reconciliation.sql:59, plus the NOT EXISTS guard at line 22), but the classification CASE at lines 16-19 is evaluated from the perspective of whichever config's ingestion commits first. defineEconomyConfig (economy-config.ts:33) permits two live configs that differ only in stakingContract while sharing chain/token/paymentContract/treasury; this is exactly the 'another legitimate vault' shape the objective asks to exercise and that tests/economy-reconciliation.test.mjs:43 uses (`separate`). Both indexers filter eth_getLogs on their own staking plus the shared payment address, so both ingest the same Paid log. If the config that does not own the order (no lineage link) ingests the block first, line 17 classifies the payment 'wrong-config' and inserts the only row. When the owning config ingests, its UPDATE economy_orders fulfills the quote correctly, but the evidence INSERT is skipped by the dedup, so the sole immutable record of a fully successful payment says 'wrong-config'. Triggers economy_payment_case_no_change/no_delete make it permanent; reconcileEconomyPayments uses the same NOT EXISTS and cannot repair it; recordEconomyPaymentDisposition can only append and will accept 'acknowledged-no-entitlement' for a payment that did deliver the service. economyPaymentCases returns it (classification<>'matched') with correctPaymentEvent pointing at the owning config's event id, so the payer's pane lists their own fulfilling payment under payment reconciliation as 'Disposition pending', and the operator case export counts it as an unreviewed exception. The outcome depends on cron/isolate ordering between the two configs' sync flights, so the evidence table is nondeterministic. No funds move and fulfillment stays exactly-once; this is an integrity defect of the new immutable reconciliation evidence. Fix: classify against the physical event rather than the ingesting namespace (treat as 'matched' when the order owned by any config is fulfilled by this tx_hash:log_index), or let only the order-owning config or its lineage write the case for an order it owns, or reject configs that share a paymentContract in defineEconomyConfig. Merged from audit_permissions, audit_flow and audit_economics (three reports of the same mechanism at lines 17/22).

**Reproduction**

Node 24, world-source/: openD1(); X=defineEconomyConfig({stakingContract:0x11..11,paymentContract:0x22..22,deploymentBlock:'100',...}); Y=same but stakingContract 0x44..44. order=createEconomyOrder(db,X,{wallet:W,requestId:'order-under-x-0000001',serviceId:'svc',currency:'EMBEO',nowMs:T+DAY}). e={kind:'payment',transactionHash:0x..0bcc,logIndex:0,blockNumber:'102',blockHash:H(102),timestamp:T+DAY+1000,wallet:W,orderId:order.id,amount:'1000000000000000000',token:EMBEO,receiver:TREASURY}. ingestEconomyEvents(db,Y,{fromBlock:'100',toBlock:'102',toBlockHash:H(102),throughTimestamp:T+DAY+1000,events:[e]}) then ingestEconomyEvents(db,X,{same batch}). Expected: order paid under X and no non-matched payment case for W (or a 'matched' row). Actual (observed): economy_orders.status='paid', paid_event_id='<X.id>:0x..0bcc:0', one fulfillment row, but economy_payment_cases holds exactly one row with config_id=<Y.id>, event_id='<Y.id>:0x..0bcc:0', classification='wrong-config'; economyWalletSnapshot(db,X,W).paymentCases=[{classification:'wrong-config',expectedAmount:'1000000000000000000',actualAmount:'1000000000000000000',correctPaymentEvent:'<X.id>:0x..0bcc:0',disposition:'unreviewed'}]; reconcileEconomyPayments(db,X) returns {remaining:0} and leaves the row 'wrong-config'. Reversing the order (X ingests first) yields one 'matched' row and an empty player case list, demonstrating order dependence.

### 3. Info: Dead ternary in ingestion ordering loop: both arms assign e.logIndex, so previousLog is never reset at a block boundary

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

```
    previousLog=block===previousBlock?e.logIndex:e.logIndex;previousBlock=block;previousTime=e.timestamp;previousHash=e.blockHash;
```

Both arms of the conditional assign the same value, so the expression equals previousLog=e.logIndex. The evident intent was to reset the per-block log cursor when the block number changes. No ordering or money defect follows: the only consumer is the guard on line 45, which compares logIndex against previousLog only under block===previousBlock, so a stale value carried across a block boundary is never read. Readability/intent note only. Fix: write previousLog=e.logIndex, or reset to -1 on a new block to match the intent. From audit_math; confirmed by reading and by exercising the guard.

**Reproduction**

Input: ingestEconomyEvents with events (block 100, logIndex 7) then (block 101, logIndex 0). Expected: accepted. Actual: accepted. Input: (block 100, logIndex 7) then (block 100, logIndex 7). Expected: EVENT_ORDER_OR_RANGE. Actual: EVENT_ORDER_OR_RANGE. The ternary has no observable effect in either version.

### 4. Info: Unreachable upper time bound in recovery replay: time>horizon+DAY can never hold for an event that passed the build() validation

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

```
  if(time<priorTime||time>horizon+DAY||seen.has(identity))return fail('RECOVERY_EVENT_CONFLICT');priorTime=time;seen.add(identity);
```

horizon is Math.floor(clock(a.through_ms)/DAY)*DAY (line 75), so through_ms<horizon+DAY always. Every rebuilt event must satisfy clock(e.at_ms)<=clock(b.through_ms) (line 87) with b.through_ms===a.through_ms (line 73), and every old event is a byte-identical proof of a rebuilt event (lines 93-95). Hence time<=through_ms<horizon+DAY for every event that reaches replay(), and the branch is dead. The bound that matters, events after horizon affecting only the unsettled partial day, is enforced by the day loop (lines 58-61, day<horizon). No overissuance or wrong refusal results. Fix: remove the dead comparison or make it time>through_ms with the horizon passed explicitly. From audit_math; confirmed by reading.

**Reproduction**

State: original DB through_ms=T, rebuilt DB through_ms=T. Any event with at_ms>T is rejected at line 87 (RECOVERY_EVENT_CONFLICT) before replay() runs; any event with at_ms<=T satisfies at_ms<horizon+DAY. Expected: the line 43 upper bound rejects some input. Actual: no input reaches it with time>horizon+DAY. The fixture tests (tests/economy-reconciliation.test.mjs) and the earlier differential fuzz never hit this branch.

---

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