Contracts: per-podium timer params (extension, initial, hard-reset bands)

Context

GitLab #247 (closed) landed independent podiumDeadline[4] / podiumEpoch[4] storage and rollPodiumEpoch, but all four categories still share one set of timer parameters at runtime:

  • Global timerExtensionSec, initialTimerSec, timerCapSec
  • Global hard-reset constants TIMER_RESET_BELOW_REMAINING_SEC (780) / TIMER_RESET_TO_REMAINING_SEC (900)

The product spec already defines per-podium values in docs/product/time-arena.md (see “Onchain implementation note” — currently marked as not enforced). This issue closes that gap immediately so onchain behavior matches the spec table.

Parent epic: #238 (closed).


Product target (authoritative)

Podium Cat Initial Extension / buy Hard-reset if remaining below Reset to
Last Buy 0 24h (86_400) +120s 13m (780) 15m (900)
Time Booster 1 12h (43_200) +60s 4m (240) 5m (300)
Defended Streak 2 18h (64_800) +90s 8.5m (510) 10m (600)
WarBow 3 48h (172_800) +300s 55m (3300) 1h (3600)

Timer cap: default 96h (4 × 86_400) for Last Buy today. Recommended default for this issue: timerCapSec[cat] = 4 × initialTimerSec[cat] (preserves ratio; WarBow cap = 192h). Document final choice in PARAMETERS.md.

Unchanged: lastBuyEpoch / CHARM / CRED accrual still roll only on Last Buy (cat 0) hard reset during _finishBuy, not on other podium rolls (#247 (closed)).


Relevant files

Contracts (primary)

File Why
contracts/src/arena/TimeArena.sol Replace shared timer scalars/constants; per-category extend in _finishBuy / _extendOtherPodiumTimers; per-category roll reset in rollPodiumEpoch; fix scoring hooks that currently use Last Buy timer deltas
contracts/src/libraries/TimeMath.sol No API change expected (extendDeadlineOrResetBelowThreshold already parameterized)
New: contracts/src/arena/libraries/ArenaPodiumTimerConfig.sol (recommended) Canonical default table + validation helper (keeps TimeArena readable)
contracts/script/UUPSDeployLib.sol Pass per-category timer arrays into initialize
contracts/script/DeployProduction.s.sol Production defaults + env overrides per category
contracts/script/DeployDev.s.sol Anvil/E2E defaults (may keep compressed timers for CI — document if so)
contracts/test/TimeArena.t.sol Update / replace tests assuming uniform +120s / 24h initial
contracts/PARAMETERS.md Per-podium table replaces single-row timer section

Docs & invariants

File Why
docs/product/time-arena.md Remove or rewrite “Onchain implementation note” once enforced
docs/product/arena-v2.md Timer table currently says “Same params” for cats 1–3 — update to spec values
docs/testing/invariants-and-business-logic.md New invariants for per-category extension / initial divergence
.cursor/skills/yieldomega-guardrails/SKILL.md Guardrail bullet for per-podium params
skills/play-time-arena-doub/SKILL.md Play-agent timer reference

Derived layers (follow-on in same issue or sub-issues — list for visibility)

File Why
frontend/src/lib/abis.ts ABI for new getters (podiumTimerExtensionSec(uint8), etc.)
frontend/src/pages/arena/useArenaSaleSession.ts Buy preview currently uses single timerExtensionSec
frontend/src/lib/timeArenaBuyPreview.ts Hardcoded 120 default
frontend/src/pages/arena/ArenaTimerChips.tsx Already reads four deadlines; may need per-chip extension labels
indexer/src/chain_timer.rs Optional: expose per-category params on GET /v1/arena/timers for UI preview
bots/timearena/ Bot buy timing heuristics if they assume uniform extension

1. Storage & initialization (UUPS-safe)

Append per-category arrays (do not repurpose existing scalar slots on upgraded proxies):

uint256[4] public podiumTimerExtensionSec;
uint256[4] public podiumInitialTimerSec;
uint256[4] public podiumTimerCapSec;
uint256[4] public podiumResetBelowRemainingSec;
uint256[4] public podiumResetToRemainingSec;
  • initialize: accept arrays (or struct[4]) validated by ArenaPodiumTimerConfig.validate(...).
  • startArena: set podiumDeadline[i] = block.timestamp + podiumInitialTimerSec[i] (categories no longer share the same initial deadline).
  • rollPodiumEpoch(cat): reset with podiumInitialTimerSec[cat], not global initialTimerSec.
  • Deprecate / stop writing global timerExtensionSec & initialTimerSec for new logic; keep getters shimmed to cat 0 for one release if needed for ABI compat.

2. Buy path — per-category TimeMath

Refactor _finishBuy:

  1. Last Buy (cat 0): existing flow; hardReset on cat 0 only drives lastBuyEpoch.
  2. Cats 1–3: replace _extendOtherPodiumTimers loop body to use podium*Sec[c] for each category independently (each may hard-reset on its own remaining band).

Return per-category (actualSecondsAdded[c], hardReset[c]) for downstream scoring.

3. Fix scoring hooks (currently Last Buy–coupled)

Hook Today Should use
Time Booster totalEffectiveTimerSecAdded Last Buy actualSecondsAdded Cat 1 seconds added
Defended Streak _processDefendedStreak Last Buy remainingBefore / secondsAdded Cat 2 remaining / seconds added
WarBow _applyBuyWarBowBp clutch (remainingBefore < 30) Last Buy remaining Cat 3 remaining
WarBow timer-reset bonus BP Last Buy hardReset Cat 3 hard reset

4. Deploy defaults

Wire product table into DeployProduction / DeployDev (env overrides e.g. ARENA_PODIUM_1_TIMER_EXTENSION_SEC). Anvil may use compressed timers only behind DeployDev guard with explicit comment — production must use spec table.

5. Upgrade path

If a TimeArena proxy is already live with shared params: add reinitializePodiumTimers(...) (owner-only, once) or document fresh redeploy only per Arena v2 “no backwards compatibility” policy (#238 (closed)). Pick one approach in implementation PR and document in PARAMETERS.md.


Acceptance criteria

  • Each of the four podium categories has independent extension, initial, cap, resetBelow, resetTo values onchain matching the product table (or documented dev-compressed Anvil subset).
  • startArena seeds different initial podiumDeadline[i] per category.
  • A single buy extends each category by its own extension / hard-reset rules (not shared +120s).
  • rollPodiumEpoch(cat) resets deadline to that category’s initial timer.
  • lastBuyEpoch still increments only on Last Buy (cat 0) hard reset; unaffected by other categories’ hard resets.
  • Time Booster, Defended Streak, and WarBow scoring hooks use their category’s timer deltas / remaining / hard-reset flags.
  • Deploy scripts (DeployProduction, DeployDev) and PARAMETERS.md updated.
  • Product docs no longer claim shared params are intentional (time-arena.md implementation note removed/updated; arena-v2.md table aligned).

Verification checklist

Forge

  • FOUNDRY_PROFILE=ci forge test --match-contract TimeArenaTest
  • New: test_start_arena_initial_deadlines_differ_by_category
  • Update: test_multi_podium_deadline_extend — assert cat-specific deltas (+120 / +60 / +90 / +300), not uniform +120
  • New: test_time_booster_hard_reset_band_240_to_300 (warp near cat 1 deadline)
  • New: test_warbow_bp_bonus_uses_warbow_hard_reset_not_last_buy (Last Buy extend without WarBow hard reset → no WarBow reset bonus)
  • New: test_defended_streak_uses_streak_timer_not_last_buy
  • Existing #247 (closed) divergence tests still pass (may need adjusted warp targets)

Integration / UI (manual or automated)

  • Anvil deploy + GET /v1/arena/timers shows four different initial deadlines after startArena
  • /arena timer chips show distinct countdowns at arena start
  • Buy preview / projected effects document which timer(s) a buy affects (at minimum Last Buy + note for podiums)

Docs / agents

  • Invariants table lists per-podium timer IDs with test names
  • skills/play-time-arena-doub/SKILL.md references per-podium table
  • Guardrails skill updated

Out of scope (unless blocking)

  • Governance setter to change timer params post-deploy (owner setPodiumTimerConfig — nice-to-have, not required for “immediate” spec alignment)
  • Indexer historical backfill of timer config (head reads sufficient for v1)