WarBow steal/guard/revenge prices derived from DOUB/CHARM TWAP anchor (guard = anchor/2, steal = revenge = anchor/5) #367

Closed
opened 2026-07-07 12:54:34 +00:00 by PlasticDigits · 5 comments
PlasticDigits commented 2026-07-07 12:54:34 +00:00 (Migrated from gitlab.com)

Summary

Reprice WarBow guard, revenge, and steal from fixed DOUB constants to values derived from the DOUB/CHARM TWAP anchor, recomputed every time the anchor is updated:

  • Guard = 1/2 of the epoch DOUB/CHARM anchor (e.g. anchor 20_000e18 DOUB per CHARM → guard costs 10,000 DOUB).
  • Revenge = 1/5 of the epoch DOUB/CHARM anchor (e.g. anchor 20_000e18 → revenge costs 4,000 DOUB).
  • Steal = 1/5 of the epoch DOUB/CHARM anchor — same ratio as revenge (e.g. anchor 20_000e18 → steal costs 4,000 DOUB).

Only the steal-limit bypass (WARBOW_STEAL_LIMIT_BYPASS_DOUB = 50_000e18) is out of scope and stays fixed.

Current codebase

  • contracts/src/arena/TimeArena.sol declares the prices as compile-time constants (lines ~56–58): WARBOW_STEAL_DOUB = 1000e18, WARBOW_REVENGE_DOUB = 1000e18, WARBOW_GUARD_DOUB = 10_000e18. warbowSteal() (~line 541), warbowRevenge() (~line 578), and warbowActivateGuard() (~line 596) pull these exact amounts via _pullDoubExact and route 100% to podium vaults via _routeWarbowDoubSpend (#310).
  • The DOUB/CHARM price anchor lives in charmPriceWad / epochCharmAnchorWad (+ epochAnchorTimestamp). It is (re)set by:
    • initialize(...) / setCharmPriceWad (pre-start only),
    • setEpochCharmAnchorWad (owner break-glass, #305),
    • _startNextLastBuyEpoch() → _sampleCharmAnchor() → _setEpochCharmAnchor() on every Last Buy podium roll — Anvil Kumbaya spot when charmAnchorKumbayaRouter is configured, Kumbaya V3 TWAP via ArenaCharmPriceTwap.compute(megaethMainnetConfig()) on chain 4326 (#303, #352), otherwise carry-forward.
    • Within an epoch effectiveCharmPriceWad() grows the anchor ~10%/day (#305); the anchor itself only changes at re-anchor events.
  • Frontend reads WARBOW_STEAL_DOUB() / WARBOW_GUARD_DOUB() / WARBOW_REVENGE_DOUB() via RPC when the indexer is off, and hardcodes WARBOW_STEAL_DOUB_WAD = 1000e18 / WARBOW_GUARD_DOUB_WAD = 10_000e18 / WARBOW_REVENGE_DOUB_WAD = 1000e18 in frontend/src/lib/arenaWarbowConstants.ts when VITE_INDEXER_URL is set (indexerOn), per INV-FRONTEND-301-INDEXER-FIRST-DISPLAY (useArenaWarbowHero.ts lines ~130–137). These hardcoded values become wrong once prices are dynamic.
  • The indexer already exposes charm_price_wad, epoch_charm_anchor_wad, and epoch_anchor_timestamp_sec on GET /v1/arena/timers (indexer/src/chain_timer.rs, indexer/src/api_arena.rs) and persists LastBuyEpochCharmAnchored (decoder.rs / persist.rs).
  • Docs/params pin the fixed values: contracts/PARAMETERS.md (WarBow table), docs/product/time-arena.md (§ guard 10000e18 / revenge 1000e18), docs/product/arena-v2.md, docs/frontend/design.md, skills/play-time-arena-warbow/SKILL.md, and invariant INV-TIME-ARENA-WARBOW-DOUB in docs/testing/invariants-and-business-logic.md.

Why this is needed

Steal, guard, and revenge costs are economic levers relative to the price of a CHARM buy. With fixed 1,000 / 10,000 / 1,000 DOUB costs and a market-driven DOUB/CHARM anchor (production initializes from live Kumbaya TWAP, #303), the relative cost of aggression, protection, and retaliation drifts arbitrarily: if DOUB/CHARM is 20k, guard at 10k DOUB is only half a buy and steal at 1k is 5% of a buy; if DOUB/CHARM falls to 2k, guard costs five buys. Tying guard to 1/2 and steal/revenge to 1/5 of the anchor keeps WarBow PvP economics proportional to buy pricing across market conditions and across re-anchor events, without manual owner retuning.

Constraints / guardrails

  • Onchain authority (yieldomega-guardrails §3): pricing must be computed in TimeArena; indexer and frontend only display derived values.
  • UUPS storage safety: TimeArena is upgradeable — append-only storage; do not reorder existing slots. Prefer deriving prices in view functions from the existing epochCharmAnchorWad rather than adding new storage.
  • ABI compatibility: WARBOW_STEAL_DOUB() / WARBOW_GUARD_DOUB() / WARBOW_REVENGE_DOUB() selectors are consumed by frontend/src/lib/abis.ts, frontend/public/abis/TimeArena.json, bots/timearena/src/timearena_bot/abis/timearena.json, and cost pills (skill doc). Keep the same function names/selectors (a public view function has the same ABI as a public constant getter) or provide a migration for every consumer in the same MR.
  • Anchor semantics: prices derive from the epoch anchor (epochCharmAnchorWad, the "initial TWAP" of the epoch), not from the growing effectiveCharmPriceWad(). Steal/guard/revenge costs change only at re-anchor events (epoch roll, owner break-glass, pre-start set) and are stable within an epoch.
  • Routing invariant unchanged (#310 / INV-TIME-ARENA-WARBOW-DOUB): 100% of steal/guard/revenge DOUB (including the fixed bypass) still routes to podium vaults via _routeWarbowDoubSpend, totalDoubRaised still increments by gross, no DOUB stranded on TimeArena. Update the invariant text, not the behavior.
  • Steal-limit bypass stays fixed: WARBOW_STEAL_LIMIT_BYPASS_DOUB = 50_000e18 remains a constant; only the base steal spend becomes dynamic. The bypass is pulled in addition to the dynamic steal price when the daily cap is exceeded, exactly as today.
  • Zero-price floor: _setEpochCharmAnchor requires wad > 0, but integer division can still produce 0 (anchor < 2 wei / < 5 wei) — steal/guard/revenge spends must never become 0 silently; enforce a minimum or revert.
  • Autoroll ordering: warbowSteal / warbowActivateGuard / warbowRevenge call _requireLiveAndAutoroll() which can roll the Last Buy epoch and re-anchor in the same transaction. Cost must be computed after autoroll so the paid amount matches the post-roll anchor; document this so frontends quote with the caveat.
  • Do not introduce owner-settable multipliers beyond what the feature needs; ratios 1/2 and 1/5 are constants.
  • DeployDev/Anvil anchor stays 1000e18 (#303) → dev guard becomes 500 DOUB, dev steal and revenge 200 DOUB each. Bots, E2E, and play skills that budget for 1k/10k/1k must be updated.

Relevant files

Contracts

  • contracts/src/arena/TimeArena.sol — constants, warbowSteal, warbowActivateGuard, warbowRevenge, _setEpochCharmAnchor, _startNextLastBuyEpoch, _sampleCharmAnchor, setEpochCharmAnchorWad, _requireLiveAndAutoroll
  • contracts/src/oracle/ArenaCharmPriceTwap.sol (anchor source on 4326; read-only here)
  • contracts/test/TimeArena.t.sol (test_warbow_*), contracts/test/TimeArenaTwapHardReset4326.t.sol
  • contracts/PARAMETERS.md

Frontend

  • frontend/src/lib/arenaWarbowConstants.ts (hardcoded WADs), frontend/src/pages/arena/useArenaWarbowHero.ts (stealDoubWei / guardDoubWei / revengeDoubWei, approval sizing, steal preflight copy), frontend/src/pages/arena/arenaV2AdvancedSessionBridge.ts, frontend/src/pages/arena/ArenaWarbowHeroPanel.tsx (+ tests), frontend/src/lib/abis.ts, frontend/public/abis/TimeArena.json, frontend/src/lib/warbowRevengeRowPreview.ts

Indexer

  • indexer/src/chain_timer.rs, indexer/src/api_arena.rs (/v1/arena/timers already carries epoch_charm_anchor_wad; optionally add explicit warbow_steal_doub_wad / warbow_guard_doub_wad / warbow_revenge_doub_wad head fields so the frontend never recomputes ratios)

Bots / sims / docs / skills

  • bots/timearena/src/timearena_bot/actions.py, strategies/pvp.py (approval/budget amounts)
  • simulations/ecostrategy/constants.py, warbow_pvp.py
  • docs/product/time-arena.md, docs/product/arena-v2.md, docs/frontend/design.md, docs/testing/invariants-and-business-logic.md (INV-TIME-ARENA-WARBOW-DOUB), skills/play-time-arena-warbow/SKILL.md
  1. In TimeArena.sol, replace the three constants with view functions keeping the existing names/selectors:
    • WARBOW_GUARD_DOUB() → anchor / 2
    • WARBOW_REVENGE_DOUB() → anchor / 5
    • WARBOW_STEAL_DOUB() → anchor / 5
      where anchor = epochCharmAnchorWad != 0 ? epochCharmAnchorWad : charmPriceWad (same fallback as effectiveCharmPriceWad). Add ratio constants WARBOW_GUARD_ANCHOR_DIV = 2, WARBOW_STEAL_ANCHOR_DIV = 5, WARBOW_REVENGE_ANCHOR_DIV = 5 for documentation/tests. WARBOW_STEAL_LIMIT_BYPASS_DOUB stays a constant.
  2. In warbowSteal / warbowActivateGuard / warbowRevenge, read the price after _requireLiveAndAutoroll() and require(price > 0, "TimeArena: warbow price"). Steal-limit bypass keeps pulling the fixed 50_000e18 on top of the dynamic steal price.
  3. Emit the paid amount as today (WarBowSteal / WarBowGuard / WarBowRevenge already carry spent); no event changes required. Consider emitting nothing new — indexer derives prices from the persisted LastBuyEpochCharmAnchored anchor.
  4. Frontend: delete the hardcoded steal/guard/revenge WADs from arenaWarbowConstants.ts; when indexerOn, derive stealDoubWei = anchor / 5n, guardDoubWei = anchor / 2n, and revengeDoubWei = anchor / 5n from epoch_charm_anchor_wad on GET /v1/arena/timers (or from new explicit indexer fields); when indexer is off, keep the existing RPC reads (they still work since selectors are unchanged). Approval prompts must size to the live quote (steal + optional bypass), with headroom or a re-quote path for the same-transaction autoroll re-anchor case.
  5. Update bots to quote via the contract getters instead of constants, update sims, and refresh all docs/skills tables plus INV-TIME-ARENA-WARBOW-DOUB (new invariant text: guard = anchor/2, steal = revenge = anchor/5, bypass fixed at 50_000e18, updated at every anchor event).
  6. This is a UUPS implementation upgrade — no storage additions, so DeployTimeArenaImpl.s.sol upgrade path applies for live deployments.

Acceptance criteria

  • With anchor 20_000e18: WARBOW_GUARD_DOUB() returns 10_000e18, WARBOW_STEAL_DOUB() and WARBOW_REVENGE_DOUB() return 4_000e18; steal, guard, and revenge pull exactly those amounts.
  • After a Last Buy epoch roll that re-anchors (Anvil spot path and 4326 TWAP path), the very next steal/guard/revenge in the same block uses the new anchor-derived prices — including when the roll happens via autoroll inside the WarBow transaction itself.
  • setEpochCharmAnchorWad (owner break-glass) immediately changes all three prices.
  • Prices are stable within an epoch (10%/day effectiveCharmPriceWad growth does not move steal/guard/revenge).
  • Steal-limit-bypass cost is unchanged (50_000e18) and still pulled on top of the dynamic steal price when the daily cap is exceeded.
  • Steal/guard/revenge spends still route 100% to podium vaults and increment totalDoubRaised (updated INV-TIME-ARENA-WARBOW-DOUB holds).
  • Steal/guard/revenge revert (not pull 0) if the derived price would be 0.
  • Frontend cost pills, approval sizing, and steal preflight copy show the dynamic prices in both indexer-first and RPC modes; no stale hardcoded 1k/10k/1k anywhere (grep for WARBOW_STEAL_DOUB_WAD / WARBOW_GUARD_DOUB_WAD hardcodes).
  • ABI selectors for WARBOW_STEAL_DOUB() / WARBOW_GUARD_DOUB() / WARBOW_REVENGE_DOUB() unchanged; frontend/public/abis/TimeArena.json and bot ABI regenerated if metadata changed.
  • Docs, PARAMETERS.md, play skill, and sims updated to the ratio-based pricing.

Test plan — all paths

Forge (contracts/test/TimeArena.t.sol + new cases)

  • Steal/guard/revenge price getters at initialize anchor (1000e18 dev → 200/500/200), at 20_000e18 (→ 4k/10k/4k), and at odd anchors (rounding: 999e18/2, 999e18/5 — floor division asserted).
  • test_warbow_steal_pulls_doub / test_warbow_guard_pulls_doub / test_warbow_revenge_pulls_doub updated: exact pull equals current getter; ERC20 parity check (_pullDoubExact) still enforced.
  • Steal-limit bypass: over-cap steal pulls dynamic steal price + 50_000e18 (test_warbow_steal_limit_override_pulls_doub updated for a non-default anchor).
  • Epoch roll re-anchor: buy → expire Last Buy timer → next steal/guard pays new-anchor price; separate test where the roll happens via autoroll inside the WarBow call and the paid amount matches the post-roll anchor.
  • setEpochCharmAnchorWad mid-epoch changes prices; effectiveCharmPriceWad growth over 3 simulated days does not.
  • Zero/dust anchor: anchor of 1 wei → steal/guard/revenge revert with the price-floor error.
  • 4326 TWAP path: extend TimeArenaTwapHardReset4326.t.sol — mocked Kumbaya TWAP re-anchor updates steal/guard/revenge.
  • Routing: steal/guard/revenge gross still splits to podium vaults, totalDoubRaised increments (regression on #310 tests).
  • Upgrade test: deploy old impl, state in-flight, upgrade to new impl, storage intact and new prices live.

Frontend (npm run typecheck && npm run lint && npm test)

  • useArenaWarbowHero unit tests: indexer-on derives steal/guard/revenge from epoch_charm_anchor_wad; indexer-off falls back to RPC reads; approval needWei follows the dynamic quote (steal + bypass when over cap).
  • ArenaWarbowHeroPanel cost-pill rendering for a non-default anchor.

Indexer (cargo clippy --all-targets -- -D warnings && cargo test)

  • If new head fields are added: /v1/arena/timers serves warbow_steal_doub_wad / warbow_guard_doub_wad / warbow_revenge_doub_wad consistent with epoch_charm_anchor_wad.

E2E / Anvil

  • bash scripts/e2e-anvil.sh green; add a WarBow scenario asserting guard cost anchor/2 after a forced epoch roll (or extend scripts/verify-podium-live-anvil.sh flow).

Test plan — attack / hack / abuse vectors

  • TWAP manipulation to cheapen steal/guard: on 4326 the anchor comes from a 15m Kumbaya TWAP (#303). Test that a single-block price spike/dump in the mocked pool moves the TWAP (and thus derived prices) only within TWAP-bounded limits; document that price manipulation cost ≥ moving the 15m TWAP. If ArenaCharmBounds-style clamps exist for anchors, assert they bound the derived prices too.
  • Cheap-steal BP farming: a manipulated-down anchor makes steals cheap while the BP drain stays 10% of victim BP — steal remains gated by the daily cap (3/day unless the fixed 50k bypass is paid), the 2×–10× BP band, and the level gate. Forge test quantifying the worst-case BP-per-DOUB at a dust-but-nonzero anchor; record the result in the threat model (docs/onchain/security-and-threat-model.md).
  • Sandwich cheap-guard-then-steal: attacker pushes anchor down before an epoch roll, buys guard cheap, restores price. Forge test demonstrating the guard bought cheap still only gives the standard 6h WARBOW_GUARD_DURATION_SEC and 1% drain — i.e. quantify the maximum economic benefit and record it in the threat model.
  • Same-tx autoroll repricing: user signs expecting price X; autoroll re-anchors to Y > X inside the tx. Assert the tx reverts on insufficient allowance rather than pulling more than approved (_pullDoubExact + ERC20 approval is the cap); frontend test that the re-quote path recovers. Applies to steal, guard, and revenge.
  • Zero/dust anchor griefing: owner or oracle failure sets anchor to dust → free steals/guards? Assert the price-floor revert; assert _setEpochCharmAnchor still rejects 0.
  • Fee-on-transfer / balance-delta parity: _pullDoubExact parity check still rejects any non-exact pull at the new dynamic amounts (regression of #123-style checks).
  • Owner break-glass abuse: setEpochCharmAnchorWad is onlyOwner; confirm no new unauthenticated path can move the anchor (fuzz non-owner callers revert).
  • Revenge-window arbitrage: victim delays revenge up to 24h across an epoch roll to pay a cheaper post-roll price. Test documents the behavior (revenge pays the price at execution time, by design) so QA and the play skill state it explicitly. Same timing note applies to steals held for a cheaper epoch.
  • Bypass-ratio inversion: with a very high anchor, the dynamic steal price can exceed the fixed 50k bypass, or with a very low anchor the bypass dwarfs the steal price. Tests document both regimes; no invariant breaks (bypass is additive), but the play-skill economics table must state it.
  • Reentrancy: steal/guard/revenge remain nonReentrant; add a malicious-token-style test only if DOUB were swappable (it is not — document why not applicable).

Verification criteria

Layer Command Required result
Contracts cd contracts && forge test PASS (incl. new test_warbow_steal_price_*, test_warbow_guard_price_*, test_warbow_revenge_price_*, 4326 TWAP re-anchor tests)
Indexer cd indexer && cargo clippy --all-targets -- -D warnings && cargo test PASS
Frontend cd frontend && npm run typecheck && npm run lint && npm test PASS
E2E bash scripts/e2e-anvil.sh PASS
Anvil WarBow flow steal/guard/revenge on a fresh Anvil stack after a forced epoch roll Paid amounts = anchor/5, anchor/2, anchor/5 on WarBowSteal/WarBowGuard/WarBowRevenge events
Docs invariants + PARAMETERS.md + product docs + play skill updated Reviewed in MR

MR checklist row per touched layer (item → command → PASS/FAIL) per AGENTS.md.

  • #252 (WarBow PvP), #310 (WarBow DOUB routing), #303 (production charm price TWAP init), #305 (epoch anchor + 10%/day growth), #352 (4326 TWAP hard reset), #301 (indexer-first display)
## Summary Reprice WarBow **guard**, **revenge**, and **steal** from fixed DOUB constants to values derived from the DOUB/CHARM TWAP anchor, recomputed every time the anchor is updated: - **Guard** = **1/2** of the epoch DOUB/CHARM anchor (e.g. anchor `20_000e18` DOUB per CHARM → guard costs **10,000 DOUB**). - **Revenge** = **1/5** of the epoch DOUB/CHARM anchor (e.g. anchor `20_000e18` → revenge costs **4,000 DOUB**). - **Steal** = **1/5** of the epoch DOUB/CHARM anchor — same ratio as revenge (e.g. anchor `20_000e18` → steal costs **4,000 DOUB**). Only the steal-limit bypass (`WARBOW_STEAL_LIMIT_BYPASS_DOUB = 50_000e18`) is **out of scope** and stays fixed. ## Current codebase - `contracts/src/arena/TimeArena.sol` declares the prices as compile-time constants (lines ~56–58): `WARBOW_STEAL_DOUB = 1000e18`, `WARBOW_REVENGE_DOUB = 1000e18`, `WARBOW_GUARD_DOUB = 10_000e18`. `warbowSteal()` (~line 541), `warbowRevenge()` (~line 578), and `warbowActivateGuard()` (~line 596) pull these exact amounts via `_pullDoubExact` and route 100% to podium vaults via `_routeWarbowDoubSpend` (#310). - The DOUB/CHARM price anchor lives in `charmPriceWad` / `epochCharmAnchorWad` (+ `epochAnchorTimestamp`). It is (re)set by: - `initialize(...)` / `setCharmPriceWad` (pre-start only), - `setEpochCharmAnchorWad` (owner break-glass, #305), - `_startNextLastBuyEpoch()` → `_sampleCharmAnchor()` → `_setEpochCharmAnchor()` on every Last Buy podium roll — Anvil Kumbaya spot when `charmAnchorKumbayaRouter` is configured, **Kumbaya V3 TWAP** via `ArenaCharmPriceTwap.compute(megaethMainnetConfig())` on chain 4326 (#303, #352), otherwise carry-forward. - Within an epoch `effectiveCharmPriceWad()` grows the anchor ~10%/day (#305); the **anchor itself** only changes at re-anchor events. - Frontend reads `WARBOW_STEAL_DOUB()` / `WARBOW_GUARD_DOUB()` / `WARBOW_REVENGE_DOUB()` via RPC when the indexer is off, and **hardcodes** `WARBOW_STEAL_DOUB_WAD = 1000e18` / `WARBOW_GUARD_DOUB_WAD = 10_000e18` / `WARBOW_REVENGE_DOUB_WAD = 1000e18` in `frontend/src/lib/arenaWarbowConstants.ts` when `VITE_INDEXER_URL` is set (`indexerOn`), per `INV-FRONTEND-301-INDEXER-FIRST-DISPLAY` (`useArenaWarbowHero.ts` lines ~130–137). These hardcoded values become wrong once prices are dynamic. - The indexer already exposes `charm_price_wad`, `epoch_charm_anchor_wad`, and `epoch_anchor_timestamp_sec` on `GET /v1/arena/timers` (`indexer/src/chain_timer.rs`, `indexer/src/api_arena.rs`) and persists `LastBuyEpochCharmAnchored` (`decoder.rs` / `persist.rs`). - Docs/params pin the fixed values: `contracts/PARAMETERS.md` (WarBow table), `docs/product/time-arena.md` (§ guard 10000e18 / revenge 1000e18), `docs/product/arena-v2.md`, `docs/frontend/design.md`, `skills/play-time-arena-warbow/SKILL.md`, and invariant **`INV-TIME-ARENA-WARBOW-DOUB`** in `docs/testing/invariants-and-business-logic.md`. ## Why this is needed Steal, guard, and revenge costs are economic levers relative to the price of a CHARM buy. With fixed 1,000 / 10,000 / 1,000 DOUB costs and a market-driven DOUB/CHARM anchor (production initializes from live Kumbaya TWAP, #303), the relative cost of aggression, protection, and retaliation drifts arbitrarily: if DOUB/CHARM is 20k, guard at 10k DOUB is only half a buy and steal at 1k is 5% of a buy; if DOUB/CHARM falls to 2k, guard costs five buys. Tying guard to 1/2 and steal/revenge to 1/5 of the anchor keeps WarBow PvP economics proportional to buy pricing across market conditions and across re-anchor events, without manual owner retuning. ## Constraints / guardrails - **Onchain authority** (yieldomega-guardrails §3): pricing must be computed in `TimeArena`; indexer and frontend only display derived values. - **UUPS storage safety**: `TimeArena` is upgradeable — append-only storage; do not reorder existing slots. Prefer deriving prices in view functions from the existing `epochCharmAnchorWad` rather than adding new storage. - **ABI compatibility**: `WARBOW_STEAL_DOUB()` / `WARBOW_GUARD_DOUB()` / `WARBOW_REVENGE_DOUB()` selectors are consumed by `frontend/src/lib/abis.ts`, `frontend/public/abis/TimeArena.json`, `bots/timearena/src/timearena_bot/abis/timearena.json`, and cost pills (skill doc). Keep the same function names/selectors (a `public` view function has the same ABI as a `public constant` getter) or provide a migration for every consumer in the same MR. - **Anchor semantics**: prices derive from the **epoch anchor** (`epochCharmAnchorWad`, the "initial TWAP" of the epoch), **not** from the growing `effectiveCharmPriceWad()`. Steal/guard/revenge costs change only at re-anchor events (epoch roll, owner break-glass, pre-start set) and are stable within an epoch. - **Routing invariant unchanged** (#310 / `INV-TIME-ARENA-WARBOW-DOUB`): 100% of steal/guard/revenge DOUB (including the fixed bypass) still routes to podium vaults via `_routeWarbowDoubSpend`, `totalDoubRaised` still increments by gross, no DOUB stranded on `TimeArena`. Update the invariant text, not the behavior. - **Steal-limit bypass stays fixed**: `WARBOW_STEAL_LIMIT_BYPASS_DOUB = 50_000e18` remains a constant; only the base steal spend becomes dynamic. The bypass is pulled **in addition to** the dynamic steal price when the daily cap is exceeded, exactly as today. - **Zero-price floor**: `_setEpochCharmAnchor` requires `wad > 0`, but integer division can still produce 0 (anchor < 2 wei / < 5 wei) — steal/guard/revenge spends must never become 0 silently; enforce a minimum or revert. - **Autoroll ordering**: `warbowSteal` / `warbowActivateGuard` / `warbowRevenge` call `_requireLiveAndAutoroll()` which can roll the Last Buy epoch and **re-anchor in the same transaction**. Cost must be computed **after** autoroll so the paid amount matches the post-roll anchor; document this so frontends quote with the caveat. - **Do not** introduce owner-settable multipliers beyond what the feature needs; ratios 1/2 and 1/5 are constants. - DeployDev/Anvil anchor stays `1000e18` (#303) → dev guard becomes **500 DOUB**, dev steal and revenge **200 DOUB** each. Bots, E2E, and play skills that budget for 1k/10k/1k must be updated. ## Relevant files **Contracts** - `contracts/src/arena/TimeArena.sol` — constants, `warbowSteal`, `warbowActivateGuard`, `warbowRevenge`, `_setEpochCharmAnchor`, `_startNextLastBuyEpoch`, `_sampleCharmAnchor`, `setEpochCharmAnchorWad`, `_requireLiveAndAutoroll` - `contracts/src/oracle/ArenaCharmPriceTwap.sol` (anchor source on 4326; read-only here) - `contracts/test/TimeArena.t.sol` (`test_warbow_*`), `contracts/test/TimeArenaTwapHardReset4326.t.sol` - `contracts/PARAMETERS.md` **Frontend** - `frontend/src/lib/arenaWarbowConstants.ts` (hardcoded WADs), `frontend/src/pages/arena/useArenaWarbowHero.ts` (`stealDoubWei` / `guardDoubWei` / `revengeDoubWei`, approval sizing, steal preflight copy), `frontend/src/pages/arena/arenaV2AdvancedSessionBridge.ts`, `frontend/src/pages/arena/ArenaWarbowHeroPanel.tsx` (+ tests), `frontend/src/lib/abis.ts`, `frontend/public/abis/TimeArena.json`, `frontend/src/lib/warbowRevengeRowPreview.ts` **Indexer** - `indexer/src/chain_timer.rs`, `indexer/src/api_arena.rs` (`/v1/arena/timers` already carries `epoch_charm_anchor_wad`; optionally add explicit `warbow_steal_doub_wad` / `warbow_guard_doub_wad` / `warbow_revenge_doub_wad` head fields so the frontend never recomputes ratios) **Bots / sims / docs / skills** - `bots/timearena/src/timearena_bot/actions.py`, `strategies/pvp.py` (approval/budget amounts) - `simulations/ecostrategy/constants.py`, `warbow_pvp.py` - `docs/product/time-arena.md`, `docs/product/arena-v2.md`, `docs/frontend/design.md`, `docs/testing/invariants-and-business-logic.md` (`INV-TIME-ARENA-WARBOW-DOUB`), `skills/play-time-arena-warbow/SKILL.md` ## Recommended direction 1. In `TimeArena.sol`, replace the three constants with view functions keeping the existing names/selectors: - `WARBOW_GUARD_DOUB()` → `anchor / 2` - `WARBOW_REVENGE_DOUB()` → `anchor / 5` - `WARBOW_STEAL_DOUB()` → `anchor / 5` where `anchor = epochCharmAnchorWad != 0 ? epochCharmAnchorWad : charmPriceWad` (same fallback as `effectiveCharmPriceWad`). Add ratio constants `WARBOW_GUARD_ANCHOR_DIV = 2`, `WARBOW_STEAL_ANCHOR_DIV = 5`, `WARBOW_REVENGE_ANCHOR_DIV = 5` for documentation/tests. `WARBOW_STEAL_LIMIT_BYPASS_DOUB` stays a constant. 2. In `warbowSteal` / `warbowActivateGuard` / `warbowRevenge`, read the price **after** `_requireLiveAndAutoroll()` and `require(price > 0, "TimeArena: warbow price")`. Steal-limit bypass keeps pulling the fixed `50_000e18` on top of the dynamic steal price. 3. Emit the paid amount as today (`WarBowSteal` / `WarBowGuard` / `WarBowRevenge` already carry `spent`); no event changes required. Consider emitting nothing new — indexer derives prices from the persisted `LastBuyEpochCharmAnchored` anchor. 4. Frontend: delete the hardcoded steal/guard/revenge WADs from `arenaWarbowConstants.ts`; when `indexerOn`, derive `stealDoubWei = anchor / 5n`, `guardDoubWei = anchor / 2n`, and `revengeDoubWei = anchor / 5n` from `epoch_charm_anchor_wad` on `GET /v1/arena/timers` (or from new explicit indexer fields); when indexer is off, keep the existing RPC reads (they still work since selectors are unchanged). Approval prompts must size to the live quote (steal + optional bypass), with headroom or a re-quote path for the same-transaction autoroll re-anchor case. 5. Update bots to quote via the contract getters instead of constants, update sims, and refresh all docs/skills tables plus `INV-TIME-ARENA-WARBOW-DOUB` (new invariant text: guard = anchor/2, steal = revenge = anchor/5, bypass fixed at 50_000e18, updated at every anchor event). 6. This is a UUPS implementation upgrade — no storage additions, so `DeployTimeArenaImpl.s.sol` upgrade path applies for live deployments. ## Acceptance criteria - [ ] With anchor `20_000e18`: `WARBOW_GUARD_DOUB()` returns `10_000e18`, `WARBOW_STEAL_DOUB()` and `WARBOW_REVENGE_DOUB()` return `4_000e18`; steal, guard, and revenge pull exactly those amounts. - [ ] After a Last Buy epoch roll that re-anchors (Anvil spot path and 4326 TWAP path), the very next steal/guard/revenge in the same block uses the **new** anchor-derived prices — including when the roll happens via autoroll inside the WarBow transaction itself. - [ ] `setEpochCharmAnchorWad` (owner break-glass) immediately changes all three prices. - [ ] Prices are stable within an epoch (10%/day `effectiveCharmPriceWad` growth does **not** move steal/guard/revenge). - [ ] Steal-limit-bypass cost is unchanged (`50_000e18`) and still pulled on top of the dynamic steal price when the daily cap is exceeded. - [ ] Steal/guard/revenge spends still route 100% to podium vaults and increment `totalDoubRaised` (updated `INV-TIME-ARENA-WARBOW-DOUB` holds). - [ ] Steal/guard/revenge revert (not pull 0) if the derived price would be 0. - [ ] Frontend cost pills, approval sizing, and steal preflight copy show the dynamic prices in both indexer-first and RPC modes; no stale hardcoded 1k/10k/1k anywhere (`grep` for `WARBOW_STEAL_DOUB_WAD` / `WARBOW_GUARD_DOUB_WAD` hardcodes). - [ ] ABI selectors for `WARBOW_STEAL_DOUB()` / `WARBOW_GUARD_DOUB()` / `WARBOW_REVENGE_DOUB()` unchanged; `frontend/public/abis/TimeArena.json` and bot ABI regenerated if metadata changed. - [ ] Docs, `PARAMETERS.md`, play skill, and sims updated to the ratio-based pricing. ## Test plan — all paths **Forge (`contracts/test/TimeArena.t.sol` + new cases)** - Steal/guard/revenge price getters at initialize anchor (`1000e18` dev → 200/500/200), at `20_000e18` (→ 4k/10k/4k), and at odd anchors (rounding: `999e18/2`, `999e18/5` — floor division asserted). - `test_warbow_steal_pulls_doub` / `test_warbow_guard_pulls_doub` / `test_warbow_revenge_pulls_doub` updated: exact pull equals current getter; ERC20 parity check (`_pullDoubExact`) still enforced. - Steal-limit bypass: over-cap steal pulls `dynamic steal price + 50_000e18` (`test_warbow_steal_limit_override_pulls_doub` updated for a non-default anchor). - Epoch roll re-anchor: buy → expire Last Buy timer → next steal/guard pays new-anchor price; separate test where the roll happens via autoroll **inside** the WarBow call and the paid amount matches the post-roll anchor. - `setEpochCharmAnchorWad` mid-epoch changes prices; `effectiveCharmPriceWad` growth over 3 simulated days does **not**. - Zero/dust anchor: anchor of 1 wei → steal/guard/revenge revert with the price-floor error. - 4326 TWAP path: extend `TimeArenaTwapHardReset4326.t.sol` — mocked Kumbaya TWAP re-anchor updates steal/guard/revenge. - Routing: steal/guard/revenge gross still splits to podium vaults, `totalDoubRaised` increments (regression on #310 tests). - Upgrade test: deploy old impl, state in-flight, upgrade to new impl, storage intact and new prices live. **Frontend (`npm run typecheck && npm run lint && npm test`)** - `useArenaWarbowHero` unit tests: indexer-on derives steal/guard/revenge from `epoch_charm_anchor_wad`; indexer-off falls back to RPC reads; approval `needWei` follows the dynamic quote (steal + bypass when over cap). - `ArenaWarbowHeroPanel` cost-pill rendering for a non-default anchor. **Indexer (`cargo clippy --all-targets -- -D warnings && cargo test`)** - If new head fields are added: `/v1/arena/timers` serves `warbow_steal_doub_wad` / `warbow_guard_doub_wad` / `warbow_revenge_doub_wad` consistent with `epoch_charm_anchor_wad`. **E2E / Anvil** - `bash scripts/e2e-anvil.sh` green; add a WarBow scenario asserting guard cost `anchor/2` after a forced epoch roll (or extend `scripts/verify-podium-live-anvil.sh` flow). ## Test plan — attack / hack / abuse vectors - **TWAP manipulation to cheapen steal/guard**: on 4326 the anchor comes from a 15m Kumbaya TWAP (#303). Test that a single-block price spike/dump in the mocked pool moves the TWAP (and thus derived prices) only within TWAP-bounded limits; document that price manipulation cost ≥ moving the 15m TWAP. If `ArenaCharmBounds`-style clamps exist for anchors, assert they bound the derived prices too. - **Cheap-steal BP farming**: a manipulated-down anchor makes steals cheap while the BP drain stays 10% of victim BP — steal remains gated by the daily cap (3/day unless the **fixed** 50k bypass is paid), the 2×–10× BP band, and the level gate. Forge test quantifying the worst-case BP-per-DOUB at a dust-but-nonzero anchor; record the result in the threat model (`docs/onchain/security-and-threat-model.md`). - **Sandwich cheap-guard-then-steal**: attacker pushes anchor down before an epoch roll, buys guard cheap, restores price. Forge test demonstrating the guard bought cheap still only gives the standard 6h `WARBOW_GUARD_DURATION_SEC` and 1% drain — i.e. quantify the maximum economic benefit and record it in the threat model. - **Same-tx autoroll repricing**: user signs expecting price X; autoroll re-anchors to Y > X inside the tx. Assert the tx **reverts** on insufficient allowance rather than pulling more than approved (`_pullDoubExact` + ERC20 approval is the cap); frontend test that the re-quote path recovers. Applies to steal, guard, and revenge. - **Zero/dust anchor griefing**: owner or oracle failure sets anchor to dust → free steals/guards? Assert the price-floor revert; assert `_setEpochCharmAnchor` still rejects 0. - **Fee-on-transfer / balance-delta parity**: `_pullDoubExact` parity check still rejects any non-exact pull at the new dynamic amounts (regression of #123-style checks). - **Owner break-glass abuse**: `setEpochCharmAnchorWad` is `onlyOwner`; confirm no new unauthenticated path can move the anchor (fuzz non-owner callers revert). - **Revenge-window arbitrage**: victim delays revenge up to 24h across an epoch roll to pay a cheaper post-roll price. Test documents the behavior (revenge pays the price at execution time, by design) so QA and the play skill state it explicitly. Same timing note applies to steals held for a cheaper epoch. - **Bypass-ratio inversion**: with a very high anchor, the dynamic steal price can exceed the fixed 50k bypass, or with a very low anchor the bypass dwarfs the steal price. Tests document both regimes; no invariant breaks (bypass is additive), but the play-skill economics table must state it. - **Reentrancy**: steal/guard/revenge remain `nonReentrant`; add a malicious-token-style test only if DOUB were swappable (it is not — document why not applicable). ## Verification criteria | Layer | Command | Required result | |-------|---------|-----------------| | Contracts | `cd contracts && forge test` | PASS (incl. new `test_warbow_steal_price_*`, `test_warbow_guard_price_*`, `test_warbow_revenge_price_*`, 4326 TWAP re-anchor tests) | | Indexer | `cd indexer && cargo clippy --all-targets -- -D warnings && cargo test` | PASS | | Frontend | `cd frontend && npm run typecheck && npm run lint && npm test` | PASS | | E2E | `bash scripts/e2e-anvil.sh` | PASS | | Anvil WarBow flow | steal/guard/revenge on a fresh Anvil stack after a forced epoch roll | Paid amounts = `anchor/5`, `anchor/2`, `anchor/5` on `WarBowSteal`/`WarBowGuard`/`WarBowRevenge` events | | Docs | invariants + `PARAMETERS.md` + product docs + play skill updated | Reviewed in MR | MR checklist row per touched layer (*item → command → PASS/FAIL*) per `AGENTS.md`. ## Related - #252 (WarBow PvP), #310 (WarBow DOUB routing), #303 (production charm price TWAP init), #305 (epoch anchor + 10%/day growth), #352 (4326 TWAP hard reset), #301 (indexer-first display)
PlasticDigits commented 2026-07-07 13:02:00 +00:00 (Migrated from gitlab.com)

changed title from WarBow guard/revenge prices derived from DOUB/CHARM TWAP anchor (guard = anchor/2, revenge = anchor/5) to WarBow steal/guard/revenge prices derived from DOUB/CHARM TWAP anchor (guard = anchor/2, steal = revenge = anchor/5)

<p>changed title from <code class="idiff">WarBow guard/revenge prices derived from DOUB/CHARM TWAP anchor (guard = anchor/2, revenge = anchor/5)</code> to <code class="idiff">WarBow <span class="idiff left addition">steal/</span>guard/revenge prices derived from DOUB/CHARM TWAP anchor (guard = anchor/2, <span class="idiff right addition">steal = </span>revenge = anchor/5)</code></p>
PlasticDigits commented 2026-07-07 13:02:00 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-07-11 01:54:50 +00:00 (Migrated from gitlab.com)

mentioned in commit a55964fa25

mentioned in commit a55964fa25f436733be6e01cd6e209ccc76611f6
PlasticDigits commented 2026-07-11 01:54:57 +00:00 (Migrated from gitlab.com)

mentioned in merge request !542

mentioned in merge request !542
PlasticDigits commented 2026-07-13 09:24:05 +00:00 (Migrated from gitlab.com)

mentioned in commit 8cc66b98d3

mentioned in commit 8cc66b98d3c1af029084b76605aa0070fcf9d4c2
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-07-13 09:24:05 +00:00
Sign in to join this conversation.
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#367
No description provided.