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) — PodiumPoolsToppedUp → idx_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.notifyPodiumFunded → PodiumFunded(podiumId, amount, pool) |
| 4× seed podium pools | 30% (7.5% each) | PodiumVaults.notifySeedFunded → SeedFunded(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
PodiumVaultsandAdminSellVaultinindex_addresses()foreth_getLogsfiltering. - Decoder (
decoder.rs) handlesTimeArenaevents +PodiumPoolsToppedUp; does not decodePodiumFunded,SeedFunded, orAdminVaultFunded. - 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
FeeTransparencyshows 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
- Transparency / AUDIT: Operators and participants should verify onchain 40/30/30 routing without manual log scraping.
- Reconciliation: Sum of funding events for a
tx_hashshould equalBuy.doub_paid(CRED buys: zero funding events). - Guardrails: Agent skill requires decoding these events when
TimeArenais in registry — currently gap vs implemented donate-pools path. - 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.rsrollback list. - Production: non-zero
TimeArena,PodiumVaults,AdminSellVaultin registry whenINDEXER_PRODUCTION+ ingestion (#156 (closed)). - Do not count
PodiumPoolsToppedUptoward buy routing totals (separate table/API already exists). - Event source: decode logs from
PodiumVaults/AdminSellVaultcontract addresses (events are emitted there, not onTimeArena). - Only
msg.sender == arenacan emit funding notifications onchain — still validatepodiumId < 4and amounts in tests. - HTTP 500 bodies redacted (#157 (closed)).
- Schema version: bump
x-schema-version/ document inindexer/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 |
Recommended direction
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 sametx_hashtoidx_arena_buyin 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_paidfrom pairedBuyin 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_buyrow withpaid_with_cred = false, sum funding rows in sametx_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,AdminVaultFundedfrom registry vault addresses. - Reorg rollback clears new table(s).
-
integration_stage2covers ingest + at least one HTTP route. -
GETtotals match manual sum of fixture rows. - Per-tx breakdown API returns 9 lines (typical) for standard buy fixture tx.
- CRED buy fixture:
idx_arena_buyexists, 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 testgreen; 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 logson test tx matches DB row count and amounts. - Documented in
docs/indexer/design.mdwith 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).