Indexer: ingest Arena buy vault funding events (PodiumFunded, SeedFunded, AdminVaultFunded)

Summary

Extend Arena v2 indexer coverage to persist per-buy DOUB prize routing events emitted from PodiumVaults and AdminSellVault — completing the guardrail in yieldomega-guardrails and arena-v2.md that is not covered by the donate-pools slice of #262 (closed).

Already shipped (do not redo): #262 (closed) / #261 (closed)PodiumPoolsToppedUpidx_arena_podium_pool_top_up + GET /v1/arena/podium-pool-donations + protocol AUDIT card (TimeCurveProtocolDonatePoolsSection.tsx).

This issue: PodiumFunded, SeedFunded, AdminVaultFunded from each qualifying TimeArena buy (and top-up vault notifications).

Parent epic: #238 (closed).


Current codebase

Onchain (authoritative)

Each DOUB buy (not CRED burn) calls _routeDoubPrizeSplit in TimeArena.sol:

Share Bps (total) Notification
4× active podium pools 40% (10% each) PodiumVaults.notifyPodiumFundedPodiumFunded(podiumId, amount, pool)
4× seed podium pools 30% (7.5% each) PodiumVaults.notifySeedFundedSeedFunded(podiumId, amount, pool)
AdminSellVault 30% (+ wei dust) AdminVaultFunded(amount)

topUpPodiumPools routes 100% to eight vaults and emits PodiumPoolsToppedUp only — already indexed.

Math: ArenaBuyRouting.splitBuyAmount — tested in ArenaPrizeRouting.t.sol.

Indexer (partial)

  • Registry already includes PodiumVaults and AdminSellVault in index_addresses() for eth_getLogs filtering.
  • Decoder (decoder.rs) handles TimeArena events + PodiumPoolsToppedUp; does not decode PodiumFunded, SeedFunded, or AdminVaultFunded.
  • Persist (persist.rs) has no tables for buy-sourced vault funding rows.
  • HTTP: no public aggregate/route for “where did this buy’s DOUB go?” beyond inferring from idx_arena_buy.doub_paid.

Frontend

  • FeeTransparency shows static 40/30/30 copy + vault addresses.
  • No indexer-backed “funding breakdown per buy” or running totals per vault category.
  • Protocol AUDIT may later chart vault inflows — optional consumer of new API (can be follow-up).

Why this is needed

  1. Transparency / AUDIT: Operators and participants should verify onchain 40/30/30 routing without manual log scraping.
  2. Reconciliation: Sum of funding events for a tx_hash should equal Buy.doub_paid (CRED buys: zero funding events).
  3. Guardrails: Agent skill requires decoding these events when TimeArena is in registry — currently gap vs implemented donate-pools path.
  4. Analytics: Enables podium-pool balance history, admin vault inflow time series, and future wallet/profile views without re-indexing from scratch.

Constraints and guardrails

  • One SQL transaction per block (#140 (closed)); idempotent inserts ON CONFLICT (tx_hash, log_index) DO NOTHING.
  • Reorg: register new tables in reorg.rs rollback list.
  • Production: non-zero TimeArena, PodiumVaults, AdminSellVault in registry when INDEXER_PRODUCTION + ingestion (#156 (closed)).
  • Do not count PodiumPoolsToppedUp toward buy routing totals (separate table/API already exists).
  • Event source: decode logs from PodiumVaults / AdminSellVault contract addresses (events are emitted there, not on TimeArena).
  • Only msg.sender == arena can emit funding notifications onchain — still validate podiumId < 4 and amounts in tests.
  • HTTP 500 bodies redacted (#157 (closed)).
  • Schema version: bump x-schema-version / document in indexer/README.md.

Relevant files

Area Files
Contracts (reference) PodiumVaults.sol, AdminSellVault.sol, TimeArena.sol
Migrations indexer/migrations/*_arena_vault_funding_*.sql (new)
Ingest decoder.rs, persist.rs, ingestion.rs
HTTP api_arena.rs or new api_arena_funding.rs
Tests integration_stage2.rs
Docs docs/indexer/design.md, docs/testing/invariants-and-business-logic.md, docs/onchain/fee-routing-and-governance.md (Arena prize routing section)
Registry indexer/address-registry.megaeth-mainnet.json

1. Schema

Option A — single table idx_arena_vault_funding with kind enum (podium_active, podium_seed, admin):

  • Columns: block_number, block_timestamp, tx_hash, log_index, kind, podium_id (nullable), amount_doub_wad, pool_address (nullable), buyer (optional — join via same tx_hash to idx_arena_buy in API layer).

Option B — three tables mirroring event names. Prefer Option A for simpler HTTP aggregates unless strong query isolation needed.

Indexes: (tx_hash), (block_number DESC), (kind, podium_id).

2. Decoder

Add sol! events matching onchain signatures:

event PodiumFunded(uint8 indexed podiumId, uint256 amount, address indexed pool);
event SeedFunded(uint8 indexed podiumId, uint256 amount, address indexed pool);
event AdminVaultFunded(uint256 amount);

Map contract address → decode variant (PodiumVaults vs AdminSellVault).

3. Persist + integration test

  • Insert on decode; include synthetic logs in integration_stage2 “all events” fixture.
  • Assert 9 rows per 1000 DOUB buy (4+4+1) for typical split, or sum equals doub_paid from paired Buy in same tx.

4. HTTP API (minimum viable)

Proposed routes (names negotiable):

Route Purpose
GET /v1/arena/vault-funding/recent?limit=&offset= Latest funding rows (all kinds)
GET /v1/arena/vault-funding/by-tx/{tx_hash} Breakdown for one buy tx
GET /v1/arena/vault-funding/totals Network sums by kind (+ per podium_id for podium kinds)

Empty DB → zeros / [] (not 404), consistent with #262 (closed) donate API.

5. Invariants

Add e.g. INV-INDEXER-262-VAULT-FUNDING (or new ID if #262 (closed) closed):

  • For each idx_arena_buy row with paid_with_cred = false, sum funding rows in same tx_hash = doub_paid.
  • CRED buys: no funding rows in same tx.

6. Frontend (optional in this issue)

  • If timeboxed: indexer-only + docs; frontend card in separate issue.
  • If included: small AUDIT table on protocol page — “Recent prize routing (from buys)”.

Acceptance criteria

  • Decoder persists PodiumFunded, SeedFunded, AdminVaultFunded from registry vault addresses.
  • Reorg rollback clears new table(s).
  • integration_stage2 covers ingest + at least one HTTP route.
  • GET totals match manual sum of fixture rows.
  • Per-tx breakdown API returns 9 lines (typical) for standard buy fixture tx.
  • CRED buy fixture: idx_arena_buy exists, funding sum = 0 for that tx.
  • SCHEMA_VERSION / README updated.
  • Invariant row + design.md section added.
  • No regression to idx_arena_podium_pool_top_up / GET /v1/arena/podium-pool-donations.

Test plan — functional paths

# Path Expected
1 Ingest block with one DOUB Buy 1 buy row + N funding rows (N≥9 before dust consolidation)
2 Sum funding amount per tx Equals doub_paid on buy row
3 topUpPodiumPools tx Donate row only; no funding rows or funding rows only if events also fire (document: top-up should not emit PodiumFunded — only PodiumPoolsToppedUp)
4 GET .../by-tx/{hash} JSON lists kinds, podium_id, amounts
5 GET .../totals Matches SQL SUM(amount) grouped by kind
6 Empty database Totals zero; recent []
7 Duplicate log replay Idempotent — row count unchanged
8 Reorg past block Funding rows for block removed with buy rows

Automated: cd indexer && cargo test; optional YIELDOMEGA_PG_TEST_URL=... integration.


Test plan — attack / abuse vectors

Vector Test Expected
Spoofed funding event Log from non-registry address Ignored (not in filter)
Fake PodiumVaults contract Wrong emitter in registry Production validation rejects misconfigured registry
Inflated amounts in logs Compare to Buy.doub_paid Invariant test fails CI if sum > buy (data quality alarm)
SQL injection via tx_hash param Fuzz HTTP path param Parameterized queries only
Log index collision Same tx_hash + log_index twice ON CONFLICT DO NOTHING
CRED buy confusion paid_with_cred=true No false funding attribution in API copy

Verification criteria

  • cargo test green; integration_stage2 funding section passes on CI Postgres when enabled.
  • Manual: run Anvil buy → query GET /v1/arena/vault-funding/by-tx/... → 40/30/30 visible.
  • cast logs on test tx matches DB row count and amounts.
  • Documented in docs/indexer/design.md with anchor for manual QA.
  • Close comment on #262 (closed) noting donate-pools complete; funding-events tracked here.

Out of scope

  • Onchain changes to event shapes.
  • Replacing idx_arena_buy — buy row remains primary participant record.
  • FeeRouter / TimeCurve CL8Y routing history.
  • Frontend prize charts (unless explicitly added with bandwidth).