Protocol AUDIT — Donate to pools card (indexer + wallet top-up)
## Context
[GitLab #261](https://gitlab.com/PlasticDigits/yieldomega/-/work_items/261) adds onchain **`topUpPodiumPools`** — permissionless DOUB **`transferFrom(msg.sender)`** into active + next-round prize vaults with **no** platform take. Operators and participants need a **single AUDIT-page card** that (1) submits donations, (2) shows the required no-benefit disclosure, and (3) surfaces **indexer-backed** donation history and totals on the **same card**.
Parent epic: [#238](https://gitlab.com/PlasticDigits/yieldomega/-/work_items/238). **Blocked by / pairs with:** [#261](https://gitlab.com/PlasticDigits/yieldomega/-/work_items/261) (contract + event shape).
Follow the [#231](https://gitlab.com/PlasticDigits/yieldomega/-/issues/231) pattern: one protocol section, one HTTP read-model, shared `data-testid`, invariants in QA issue [#260](https://gitlab.com/PlasticDigits/yieldomega/-/work_items/260).
## Relevant files
**Indexer**
- `indexer/migrations/` — e.g. `idx_arena_podium_pool_top_up` (or `idx_timecurve_podium_pool_top_up` until arena rename)
- `indexer/src/decoder.rs` — decode `PodiumPoolsToppedUp` (or final event name from #261)
- `indexer/src/api.rs` — new route + `SCHEMA_VERSION` bump
- `indexer/tests/integration_stage2.rs`
- `docs/indexer/design.md`
**Frontend**
- `frontend/src/pages/TimeCurveProtocolPage.tsx` (v2: arena protocol / audit route) — mount new section
- New: `TimeCurveProtocolDonatePoolsSection.tsx` (rename to `TimeArenaProtocolDonatePoolsSection.tsx` when #256 lands)
- `frontend/src/lib/indexerApi.ts` — fetch helper + types
- New: `useTimecurveProtocolDonatePools.ts` (hook: indexer read + wagmi write)
- `frontend/src/lib/ensureCl8yTimeCurveAllowance.ts` pattern → **DOUB approve** to `TimeArena` (exact allowance per [#143](https://gitlab.com/PlasticDigits/yieldomega/-/issues/143))
- `docs/frontend/timecurve-views.md` — AUDIT section doc
- `frontend/e2e/timecurve.spec.ts` or arena equivalent
## Indexer acceptance criteria
- [ ] Persist each top-up: `tx_hash`, `log_index`, `block_number`, `block_timestamp`, `donor_address` (lowercase), `amount_doub_wad`
- [ ] **`GET /v1/arena/podium-pool-donations`** (or `/v1/timecurve/podium-pool-donations` pre-rename) returns:
- **`total_donated_doub_wad`** (network-wide sum)
- **`unique_donors_count`**
- **`recent[]`**: latest N donations `{ donor, amount_doub_wad, block_timestamp, tx_hash }` (default N=10, paginated optional)
- **`donor_summary`** when `?donor=0x…` set: that wallet’s **`total_donated_doub_wad`** + count
- [ ] Empty DB → zeros / empty arrays (not 404)
- [ ] **`500`** bodies redacted per [#157](https://gitlab.com/PlasticDigits/yieldomega/-/issues/157)
## Frontend acceptance criteria — single card
**Placement:** protocol / **AUDIT** page (`/timecurve/protocol` today; `/arena/protocol` or equivalent after [#256](https://gitlab.com/PlasticDigits/yieldomega/-/work_items/256)). **`data-testid="timecurve-protocol-donate-pools"`**.
**Disclosure (required, always visible above the donate CTA):**
> Donating to the pools makes YieldOmega prizes more exciting, but does not provide you with any benefit.
Use a **`StatusMessage`** or equivalent warning variant — not dismissible, not fine print.
**Write path (connected wallet):**
- [ ] DOUB amount input + **Donate to pools** CTA
- [ ] **`ChainMismatchWriteBarrier`** + exact DOUB **`approve`** then **`topUpPodiumPools(amount)`** ([#261](https://gitlab.com/PlasticDigits/yieldomega/-/work_items/261))
- [ ] Preflight: balance ≥ amount; clear success / revert copy via `friendlyRevertFromUnknown`
- [ ] After mined tx: refetch indexer summary + wallet DOUB balance
**Read path (same card, below or beside write UI):**
- [ ] **Total donated** (network) from indexer
- [ ] **Your donations** when wallet connected (`donor_summary`)
- [ ] **Recent donations** table/list (donor abbrev + amount + relative time); row click opens wallet profile modal ([#258](https://gitlab.com/PlasticDigits/yieldomega/-/work_items/258)) when available, else explorer link
- [ ] Indexer unset/offline: **`EmptyDataPlaceholder`** / muted copy — **no fake zeros** ([#200](https://gitlab.com/PlasticDigits/yieldomega/-/issues/200), [#96](https://gitlab.com/PlasticDigits/yieldomega/-/issues/96))
- [ ] Manual ↻ refresh + optional poll after successful donate
**Out of scope for this issue:** counting donations toward XP, CRED, CHARM, or platform-usage wallet spend totals unless spec [#240](https://gitlab.com/PlasticDigits/yieldomega/-/work_items/240) explicitly amends.
## Verification checklist
**Indexer**
- [ ] Migration up/down
- [ ] `integration_stage2`: ingest synthetic `PodiumPoolsToppedUp` → HTTP totals + recent rows match
- [ ] `SCHEMA_VERSION` documented in `indexer/README.md`
**Frontend**
- [ ] Vitest: warning string present; empty/offline states
- [ ] Anvil: approve + donate → card totals increment after indexer catches up
- [ ] Manual QA: disclosure visible without connecting wallet; write gated by chain match ([#95](https://gitlab.com/PlasticDigits/yieldomega/-/issues/95))
**Docs / invariants (can land in [#260](https://gitlab.com/PlasticDigits/yieldomega/-/work_items/260) if split)**
- [ ] `INV-INDEXER-*-DONATE-POOLS` + `INV-FRONTEND-*-DONATE-POOLS` in `docs/testing/invariants-and-business-logic.md`
- [ ] `docs/frontend/timecurve-views.md` § donate-pools
- [ ] Manual QA checklist row
issue