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 |
Recommended changes
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 byArenaPodiumTimerConfig.validate(...).startArena: setpodiumDeadline[i] = block.timestamp + podiumInitialTimerSec[i](categories no longer share the same initial deadline).rollPodiumEpoch(cat): reset withpodiumInitialTimerSec[cat], not globalinitialTimerSec.- Deprecate / stop writing global
timerExtensionSec&initialTimerSecfor new logic; keep getters shimmed to cat 0 for one release if needed for ABI compat.
2. Buy path — per-category TimeMath
Refactor _finishBuy:
- Last Buy (cat 0): existing flow;
hardReseton cat 0 only driveslastBuyEpoch. - Cats 1–3: replace
_extendOtherPodiumTimersloop body to usepodium*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,resetTovalues onchain matching the product table (or documented dev-compressed Anvil subset). -
startArenaseeds different initialpodiumDeadline[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. -
lastBuyEpochstill 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) andPARAMETERS.mdupdated. - Product docs no longer claim shared params are intentional (
time-arena.mdimplementation note removed/updated;arena-v2.mdtable 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/timersshows four different initial deadlines afterstartArena -
/arenatimer 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.mdreferences 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)