Contracts: Manual DOUB podium pool top-up (no platform take)

Context

Arena v2 buy routing (GitLab #249) sends 70% of gross DOUB to prizes (10% → each active podium pool, 7.5% → each next-round seed pool) and 30% to AdminSellVault. Operators and participants also need a permissionless manual top-up path that boosts prize liquidity without the platform take.

Parent epic: #238 (closed). Complements #249 (closed) (shared PodiumVaults / routing helpers).

Relevant files

  • contracts/src/TimeArena.sol — new external entrypoint
  • contracts/src/PodiumVaults.sol (or prize-routing module from #249 (closed))
  • contracts/src/Doubloon.soltransferFrom / allowance
  • docs/product/time-arena.md (spec #240 (closed)) — document donor economics
  • indexer/src/decoder.rs — ingest new event(s)
  • Optional later: protocol / arena UI donate CTA (not blocking for this issue)

Behavior

Add a public function (e.g. topUpPodiumPools(uint256 amountDoubWad)) that:

  1. require(amountDoubWad > 0)
  2. Pulls DOUB from msg.sender via IERC20.transferFrom(msg.sender, …, amountDoubWad) (balance-delta parity per #123 (closed))
  3. Allocates 100% of amountDoubWad across the eight prize vaults using the same per-category ratios as buys, but no AdminSellVault slice:
Destination (per podium category) Share of top-up amount
Active podium pool 10 / 70 (≈ 14.29%)
Next-round seed pool 7.5 / 70 (≈ 10.71%)

Applied independently for Last Buy, Streak, Time Booster, and WarBow (four categories × two vaults). Integer rounding: document remainder policy (recommend: last vault or largest active pool — match #249 (closed)).

Equivalence: A manual top-up of 700 DOUB must land the same per-vault increments as the prize portion of a 1000 DOUB buy (100 active + 75 seed per category).

  1. Emits indexer-friendly events (e.g. PodiumPoolsToppedUp(donor, amountDoubWad) plus per-vault PodiumFunded / SeedFunded if reusing #249 (closed) events)
  2. No minting of CHARM, CRED, or XP; no timer extension; no totalRaised increment unless product explicitly wants gross accounting (default: do not count toward buy stats)

Acceptance criteria

  • topUpPodiumPools(amount) pulls DOUB only from msg.sender (transferFrom); reverts without allowance/balance
  • Zero DOUB from this path goes to AdminSellVault (no 30% platform take)
  • Per-category split matches buy prize ratios (10% : 7.5% active:seed), normalized over 100% of donated amount
  • Reuses shared internal routing with buy path where possible (single source of truth for bps)
  • Distinct event(s) emitted for indexer / wallet stats (donor address + amount)
  • nonReentrant if sharing vault code with buy
  • Spec note in docs/product/time-arena.md: manual top-up is voluntary prize sponsorship only

Verification checklist

  • Forge: approve + topUpPodiumPools(700e18) → each active pool +100e18, each seed +75e18 (four categories)
  • Forge: topUpPodiumPools(1000e18)AdminSellVault balance unchanged
  • Forge: fuzz amounts; sum of vault deltas == donated amount (no dust loss beyond documented remainder rule)
  • Forge: transferFrom failure reverts with no vault mutation
  • Indexer: migration + decode row for new event (can be follow-up linked issue if preferred)