Arm podium settlement timers on first qualifying buy (#330) #504

Merged
PlasticDigits merged 1 commit from issue-330-podium-timer-arm-on-first-buy into main 2026-06-19 03:55:24 +00:00
PlasticDigits commented 2026-06-19 03:44:13 +00:00 (Migrated from gitlab.com)

Summary

Implements GitLab #330: per-category podium settlement timers stay unarmed after startArena() and rollPodiumEpoch(cat) until the first qualifying buy in that epoch (Option B — per-category, level-gated per #299).

Contracts: podiumTimerArmed[4], _armPodiumTimer, PodiumTimerArmed event; autoroll and rollPodiumEpoch skip unarmed categories; Last Buy deadline mirrors cat 0.

Indexer: GET /v1/arena/timers adds podium_timer_armed (schema 2.17.0).

Frontend: Play chips, hero carousel, and AUDIT show awaiting first buy when unarmed (indexer-first #301).

Verification checklist

Item Command / step Result
Contracts TimeArenaTest cd contracts && forge test --match-contract TimeArenaTest PASS
Anvil podium timers bash scripts/verify-podium-timers-anvil.sh PASS
Indexer clippy cd indexer && cargo clippy --all-targets -- -D warnings PASS
Indexer tests cd indexer && cargo test PASS
Podium live smoke bash scripts/verify-podium-live-anvil.sh PASS
Frontend typecheck cd frontend && npm run typecheck PASS
Frontend timer tests cd frontend && npm test -- --run src/pages/arena/arenaPodiumTimerDisplay.test.ts src/pages/arena/useArenaProtocolPodiumAudit.test.ts src/pages/arena/ArenaTimerChips.test.tsx PASS
API armed flags curl -s http://127.0.0.1:3100/v1/arena/timers | jq '.podium_timer_armed' (fresh arena, no buys) SKIP — manual on stack
Manual play UI Fresh Anvil stack → / and /arena/protocol SKIP — manual

Third-party verification

  1. bash scripts/bootstrap-cloud-install.sh (or local toolchain per AGENTS.md).
  2. bash scripts/verify-podium-timers-anvil.sh — expect unarmed at start, arm after buy.
  3. Start stack (bash scripts/start-qa-local-full-stack.sh --no-swarm or equivalent); before any buy: curl -s http://127.0.0.1:3100/v1/arena/timers | jq '{podium_timer_armed, podium_deadlines_sec}' → all false / zeros.
  4. Execute one DOUB buy; re-fetch timers → cat 0 armed with live deadline; UI shows countdown on Last Buy.
  5. cd contracts && forge test --match-contract TimeArenaTest — 118 tests pass.
## Summary Implements GitLab #330: per-category podium settlement timers stay **unarmed** after `startArena()` and `rollPodiumEpoch(cat)` until the **first qualifying buy** in that epoch (Option B — per-category, level-gated per #299). **Contracts:** `podiumTimerArmed[4]`, `_armPodiumTimer`, `PodiumTimerArmed` event; autoroll and `rollPodiumEpoch` skip unarmed categories; Last Buy `deadline` mirrors cat 0. **Indexer:** `GET /v1/arena/timers` adds `podium_timer_armed` (schema **2.17.0**). **Frontend:** Play chips, hero carousel, and AUDIT show **awaiting first buy** when unarmed (indexer-first #301). ## Verification checklist | Item | Command / step | Result | |------|----------------|--------| | Contracts TimeArenaTest | `cd contracts && forge test --match-contract TimeArenaTest` | PASS | | Anvil podium timers | `bash scripts/verify-podium-timers-anvil.sh` | PASS | | Indexer clippy | `cd indexer && cargo clippy --all-targets -- -D warnings` | PASS | | Indexer tests | `cd indexer && cargo test` | PASS | | Podium live smoke | `bash scripts/verify-podium-live-anvil.sh` | PASS | | Frontend typecheck | `cd frontend && npm run typecheck` | PASS | | Frontend timer tests | `cd frontend && npm test -- --run src/pages/arena/arenaPodiumTimerDisplay.test.ts src/pages/arena/useArenaProtocolPodiumAudit.test.ts src/pages/arena/ArenaTimerChips.test.tsx` | PASS | | API armed flags | `curl -s http://127.0.0.1:3100/v1/arena/timers \| jq '.podium_timer_armed'` (fresh arena, no buys) | SKIP — manual on stack | | Manual play UI | Fresh Anvil stack → `/` and `/arena/protocol` | SKIP — manual | ## Third-party verification 1. `bash scripts/bootstrap-cloud-install.sh` (or local toolchain per AGENTS.md). 2. `bash scripts/verify-podium-timers-anvil.sh` — expect unarmed at start, arm after buy. 3. Start stack (`bash scripts/start-qa-local-full-stack.sh --no-swarm` or equivalent); before any buy: `curl -s http://127.0.0.1:3100/v1/arena/timers | jq '{podium_timer_armed, podium_deadlines_sec}'` → all `false` / zeros. 4. Execute one DOUB buy; re-fetch timers → cat 0 armed with live deadline; UI shows countdown on Last Buy. 5. `cd contracts && forge test --match-contract TimeArenaTest` — 118 tests pass.
PlasticDigits commented 2026-06-19 03:44:18 +00:00 (Migrated from gitlab.com)

mentioned in issue #330

mentioned in issue #330
PlasticDigits commented 2026-06-19 03:49:38 +00:00 (Migrated from gitlab.com)

HIGH — UUPS storage layout collision on upgrade

Inserting bool[4] podiumTimerArmed between podiumEpoch and charmWeight shifts every downstream storage slot (all mappings from charmWeight onward). Upgrading an existing TimeArena ERC-1967 proxy to this implementation corrupts live state.

Attack path (post-upgrade): Operator performs a normal UUPS upgradeTo → buyCount, charmWeight, WarBow BP, podium leaders, and CRED epoch mappings read/write wrong slots → e.g. buyCount appears zero so FIRST_BUY_CRED_BONUS can be re-triggered; podium settlement can pay wrong/zero winners.

Evidence: Contract is UUPSUpgradeable; repo already documents append-only storage discipline for prior vars (epochFixedCredBonus, _cachedLevel). Pre-change layout places charmWeight at slot 50; this diff inserts podiumTimerArmed at slot 50 and moves charmWeight to slot 51.

Fix: Append podiumTimerArmed at the end of storage (with gap reduction / initializer), never before existing mappings. Fresh proxy deploys are safe; this blocks safe UUPS rollout on any proxy with the current layout.

**HIGH — UUPS storage layout collision on upgrade** Inserting `bool[4] podiumTimerArmed` between `podiumEpoch` and `charmWeight` shifts every downstream storage slot (all mappings from `charmWeight` onward). Upgrading an existing `TimeArena` ERC-1967 proxy to this implementation corrupts live state. **Attack path (post-upgrade):** Operator performs a normal UUPS `upgradeTo` → `buyCount`, `charmWeight`, WarBow BP, podium leaders, and CRED epoch mappings read/write wrong slots → e.g. `buyCount` appears zero so `FIRST_BUY_CRED_BONUS` can be re-triggered; podium settlement can pay wrong/zero winners. **Evidence:** Contract is `UUPSUpgradeable`; repo already documents append-only storage discipline for prior vars (`epochFixedCredBonus`, `_cachedLevel`). Pre-change layout places `charmWeight` at slot 50; this diff inserts `podiumTimerArmed` at slot 50 and moves `charmWeight` to slot 51. **Fix:** Append `podiumTimerArmed` at the end of storage (with gap reduction / initializer), never before existing mappings. Fresh proxy deploys are safe; this blocks safe UUPS rollout on any proxy with the current layout.
PlasticDigits commented 2026-06-19 03:50:00 +00:00 (Migrated from gitlab.com)

Security review — MR !504

Reviewed commit: cc9a8489874bc886c1b4c2cada001801b9641075
Scope: TimeArena podium timer arm model (#330), indexer podium_timer_armed (schema 2.17.0), frontend/indexer display paths, tests and docs.

Outcome: FINDINGS: 1 medium+

Summary

Reviewed contract buy/arm/autoroll/roll paths, TimeMath interaction on first arm, indexer eth_call wiring, and frontend display helpers. Timer-arm logic itself is coherent: unarmed categories skip autoroll/rollPodiumEpoch, _willLastBuyHardReset correctly gates on podiumTimerArmed, and _armPodiumTimer runs before deadline extension (avoiding false hard-reset on deadline==0). Indexer/frontend changes are read-only reflections of chain state; no injection/XSS/auth bypass identified.

One HIGH finding blocks safe UUPS upgrade of an existing TimeArena proxy — see inline thread on contracts/src/arena/TimeArena.sol (insertion of podiumTimerArmed before charmWeight).

Fresh ERC-1967 proxy deploys are unaffected.

Inline threads

Severity Location Topic
HIGH contracts/src/arena/TimeArena.sol:105 UUPS storage slot shift on upgrade
## Security review — MR !504 **Reviewed commit:** `cc9a8489874bc886c1b4c2cada001801b9641075` **Scope:** `TimeArena` podium timer arm model (#330), indexer `podium_timer_armed` (schema 2.17.0), frontend/indexer display paths, tests and docs. **Outcome:** `FINDINGS: 1` medium+ ### Summary Reviewed contract buy/arm/autoroll/roll paths, `TimeMath` interaction on first arm, indexer eth_call wiring, and frontend display helpers. Timer-arm logic itself is coherent: unarmed categories skip autoroll/`rollPodiumEpoch`, `_willLastBuyHardReset` correctly gates on `podiumTimerArmed`, and `_armPodiumTimer` runs before deadline extension (avoiding false hard-reset on `deadline==0`). Indexer/frontend changes are read-only reflections of chain state; no injection/XSS/auth bypass identified. **One HIGH finding** blocks safe UUPS upgrade of an existing `TimeArena` proxy — see inline thread on `contracts/src/arena/TimeArena.sol` (insertion of `podiumTimerArmed` before `charmWeight`). Fresh ERC-1967 proxy deploys are unaffected. ### Inline threads | Severity | Location | Topic | |----------|----------|-------| | HIGH | `contracts/src/arena/TimeArena.sol:105` | UUPS storage slot shift on upgrade |
PlasticDigits commented 2026-06-19 03:55:05 +00:00 (Migrated from gitlab.com)

We will be doing a fresh deploy, so this is not a concern.

We will be doing a fresh deploy, so this is not a concern.
PlasticDigits commented 2026-06-19 03:55:08 +00:00 (Migrated from gitlab.com)

resolved all threads

resolved all threads
PlasticDigits (Migrated from gitlab.com) merged commit 71d56404bc into main 2026-06-19 03:55:25 +00:00
PlasticDigits commented 2026-06-19 03:55:26 +00:00 (Migrated from gitlab.com)

mentioned in commit 71d56404bc

mentioned in commit 71d56404bca180be1a4497e1b968f01075963fb4
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega!504
No description provided.