TimeCurve: WarBow steal 2x rule + per-wallet bypass + 10% victim floor (Finding 4 from #131 + Product notes) #134

Closed
opened 2026-05-04 08:36:08 +00:00 by PlasticDigits · 8 comments
PlasticDigits commented 2026-05-04 08:36:08 +00:00 (Migrated from gitlab.com)

Parent

Split from #131 (Finding 4).

Maintainer notes (from #131 discussion)

Per PlasticDigits on #131:

  • Daily-limit bypass (+50 CL8Y) counters should be per attacker wallet, not global — document as an invariant.
  • Successful steals should impose at least 10% of the defender’s BP (narrow interpretation: minimum drain / eligibility — align implementation + NatSpec with product).

Problem

warbowSteal enforces:

require(vbp > 0 && vbp >= 2 * abp, "TimeCurve: steal 2x rule");

When abp == 0 (fresh wallet), condition reduces to vbp > 0, so the “must outrank attacker by 2×” rule does not constrain sybil entrants. Combines poorly with WARBOW_MAX_STEALS_PER_VICTIM_PER_DAY + cheap bypass (WARBOW_STEAL_LIMIT_BYPASS_BURN_WAD) economics called out in the review.

Also verify whether daily bypass burn is globally shared vs per stealer wallet relative to Product note.

  1. Tighten ranking rule per product/security: e.g. require abp > 0 AND vbp >= 2 * abp, or use lifetime / cumulative BP if that better matches podium fairness (needs game-design signoff vs podium snapshots).
  2. Enforce “≥ 10% of defender BP” semantics explicitly in take/edge cases (guard, rounding) — ensure consistent with WARBOW_STEAL_DRAIN_GUARDED_BPS path.
  3. Per-wallet bypass accounting: if today’s bookkeeping is chain-global for the bypass, refactor storage (e.g. mapping(address stealer => uint8 count) keyed by (victim, day) tiers) as needed so each distinct attacker wallet pays / exhausts bypass independently — document invariant in docs/testing/invariants-and-business-logic.md + WarBow UX copy if surfaced.

Acceptance criteria

  • Sybil-at-zero-BP attackers cannot materially bypass intended “underdog steals only when materially behind” framing without product-approved exceptions documented in NatSpec.
  • Bypass / daily caps behave per per-wallet spec from Product (#131 note).
  • 10% defender floor (or clarified equivalent) enforced and named in docs.

Verification checklist

  • forge test — extend test_warbow_steal_revert_2x_rule and daily-limit / bypass matrices; property or fuzz-friendly cases for rotating fresh wallets if feasible.
  • Arena / Simple UX: tooltips or friendlyRevert* messages for new reverts (skills/play-timecurve-warbow / docs/frontend/timecurve-views.md cross-links).
  • docs/testing/invariants-and-business-logic.md new rows (steal rule, bypass per wallet, victim floor).
  • docs/product/primitives.md or WarBow subsection if authoritative for PvP rules.
  • Indexer-only if events or enums change (unlikely unless new revert paths change emitted fields).

Repo touch list

Area Change
Code contracts/src/TimeCurve.sol (steal + storage for per-wallet caps)
Tests contracts/test/TimeCurve.t.sol
Docs / invariants invariants-and-business-logic.md, product + frontend WarBow docs
Indexer Only if ABI/events change
Frontend Copy + preflight only if rules change user-visible behavior
## Parent Split from **#131** (Finding 4). ## Maintainer notes (from #131 discussion) Per **PlasticDigits** on #131: - **Daily-limit bypass (`+50 CL8Y`) counters should be _per attacker wallet_, not global** — document as an invariant. - **Successful steals should impose at least 10% of the defender’s BP** (narrow interpretation: minimum drain / eligibility — align implementation + NatSpec with product). ## Problem [`warbowSteal`](contracts/src/TimeCurve.sol) enforces: ```solidity require(vbp > 0 && vbp >= 2 * abp, "TimeCurve: steal 2x rule"); ``` When **`abp == 0`** (fresh wallet), condition reduces to **`vbp > 0`**, so the “must outrank attacker by 2×” rule does not constrain sybil entrants. Combines poorly with **`WARBOW_MAX_STEALS_PER_VICTIM_PER_DAY`** + cheap bypass (**`WARBOW_STEAL_LIMIT_BYPASS_BURN_WAD`**) economics called out in the review. Also verify whether **daily bypass burn is globally shared** vs **per stealer wallet** relative to Product note. ## Recommended fix 1. **Tighten ranking rule** per product/security: e.g. require **`abp > 0` AND `vbp >= 2 * abp`**, **or** use **lifetime / cumulative BP** if that better matches podium fairness (needs game-design signoff vs podium snapshots). 2. **Enforce “≥ 10% of defender BP” semantics** explicitly in `take`/edge cases (`guard`, rounding) — ensure **consistent with** `WARBOW_STEAL_DRAIN_GUARDED_BPS` path. 3. **Per-wallet bypass accounting:** if today’s bookkeeping is chain-global for the bypass, refactor storage (e.g. `mapping(address stealer => uint8 count)` keyed by `(victim, day)` tiers) **as needed** so **each distinct attacker wallet** pays / exhausts bypass independently — **document invariant** in `docs/testing/invariants-and-business-logic.md` + WarBow UX copy if surfaced. ## Acceptance criteria - Sybil-at-zero-BP attackers cannot materially bypass intended “underdog steals only when materially behind” framing without product-approved exceptions documented in NatSpec. - **Bypass / daily caps** behave per **per-wallet** spec from Product (#131 note). - **10% defender floor** (or clarified equivalent) enforced and named in docs. ## Verification checklist - [ ] `forge test` — extend [`test_warbow_steal_revert_2x_rule`](contracts/test/TimeCurve.t.sol) and daily-limit / bypass matrices; property or fuzz-friendly cases for rotating fresh wallets if feasible. - [ ] **Arena / Simple** UX: tooltips or `friendlyRevert*` messages for new reverts (**`skills/play-timecurve-warbow`** / `docs/frontend/timecurve-views.md` cross-links). - [ ] **`docs/testing/invariants-and-business-logic.md`** new rows (steal rule, bypass per wallet, victim floor). - [ ] **`docs/product/primitives.md`** or WarBow subsection if authoritative for PvP rules. - [ ] Indexer-only if events or enums change (**unlikely** unless new revert paths change emitted fields). ## Repo touch list | Area | Change | |------|--------| | **Code** | `contracts/src/TimeCurve.sol` (steal + storage for per-wallet caps) | | **Tests** | `contracts/test/TimeCurve.t.sol` | | **Docs / invariants** | `invariants-and-business-logic.md`, product + frontend WarBow docs | | **Indexer** | Only if ABI/events change | | **Frontend** | Copy + preflight only if rules change user-visible behavior |
PlasticDigits commented 2026-05-04 08:36:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #131

mentioned in issue #131
PlasticDigits commented 2026-05-04 09:58:07 +00:00 (Migrated from gitlab.com)

mentioned in commit fc6ba68fcc

mentioned in commit fc6ba68fcc366c3b19beb9015acb3896c226df21
PlasticDigits commented 2026-05-04 10:17:25 +00:00 (Migrated from gitlab.com)

mentioned in commit a9e8d1b046

mentioned in commit a9e8d1b046e9dfa7dba0e1440b6a4e065aa942e1
PlasticDigits commented 2026-05-04 10:17:39 +00:00 (Migrated from gitlab.com)

Agent follow-up (pushed main @ a9e8d1b)

Contract logic: Already on main from earlier work (#134 steal ranking: abp > 0, victim BP ≥ 2× attacker BP, per-victim and per-attacker UTC-day caps, shared bypass burn, _warbowStealDrainBp). This commit finishes alignment for fuzz + docs:

Verification checklist for @brouie

  1. cd contracts && forge test --match-path test/TimeCurve.t.sol && forge test --match-path test/TimeCurveWarBowCl8yBurns.t.sol — all green (WarBow fuzz uses 256+ runs locally).
  2. On a deployment: warbowSteal reverts steal 2x rule when attacker BP is 0 or victim BP < 2× attacker BP.
  3. Same UTC day: 4th steal from same attacker or onto same victim without bypass → daily-limit revert; bypass pulls limit-burn once when either gate binds.
  4. Docs: single #timecurve-warbow-steal-rules-gitlab-134 anchor; play skill mentions both counters.

Leaving issue open per request.

## Agent follow-up (pushed `main` @ a9e8d1b) **Contract logic:** Already on `main` from earlier work (#134 steal ranking: `abp > 0`, **victim BP ≥ 2× attacker BP**, per-victim **and** per-attacker UTC-day caps, shared bypass burn, `_warbowStealDrainBp`). This commit finishes **alignment for fuzz + docs**: - [`contracts/test/TimeCurveWarBowCl8yBurns.t.sol`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/contracts/test/TimeCurveWarBowCl8yBurns.t.sol): steal/revenge burn fuzz setups — stealer **one** qualifying buy, victim **two** buys (+ cooldown), matching **INV-WARBOW-134-RANK**. - [`docs/testing/invariants-and-business-logic.md` §134](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/testing/invariants-and-business-logic.md): removed duplicate §134 section; Forge row describes fuzz BP geometry. - [`skills/play-timecurve-warbow/SKILL.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/skills/play-timecurve-warbow/SKILL.md) + [`skills/README.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/skills/README.md): participant/agent cross-links for #134 (`stealsCommittedByAttackerOnDay`, dual UTC caps). **Verification checklist for @brouie** 1. `cd contracts && forge test --match-path test/TimeCurve.t.sol && forge test --match-path test/TimeCurveWarBowCl8yBurns.t.sol` — all green (WarBow fuzz uses 256+ runs locally). 2. On a deployment: `warbowSteal` reverts **steal 2x rule** when attacker BP is 0 or victim BP \< 2× attacker BP. 3. Same UTC day: 4th steal from same attacker **or** onto same victim without bypass → daily-limit revert; bypass pulls limit-burn once when **either** gate binds. 4. Docs: single [\#timecurve-warbow-steal-rules-gitlab-134](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/testing/invariants-and-business-logic.md) anchor; play skill mentions both counters. Leaving issue **open** per request.
Brouie commented 2026-05-05 01:59:12 +00:00 (Migrated from gitlab.com)

@PlasticDigits — verification PASS on a9e8d1b. walked your checklist:

  1. forge test --match-test 'warbow_steal' green: 9/9 PASS across both files. key tests:
  • test_warbow_steal_revert_2x_rule ✓
  • test_warbow_steal_revert_zero_attacker_bp ✓ (fresh-wallet bypass closed)
  • test_warbow_steal_attacker_daily_limit_fourth_needs_bypass ✓ (per-attacker UTC day cap binds)
  • test_warbow_steal_drains_ten_percent_and_burns_one_reserve ✓ (10% floor)
  • test_warbow_steal_fresh_wallet_drains_high_bp_victim_for_one_cl8y ✓ (regression test for yesterday's documented attack)
  • testFuzz_warbow_steal_burn_exact_to_sink ✓ (256 runs)
  1. contract behavior verified at source: abp > 0 && vbp >= 2 * abp ranking (TimeCurve.sol L654), stealsCommittedByAttackerOnDay per-attacker counter alongside per-victim, both checked at L627-631, both incremented at L664-669. bypass burn (WARBOW_STEAL_LIMIT_BYPASS_BURN_WAD) charged when EITHER limit binds (L642-648) — override is per-wallet on each side, not a global gate. 10% drain floor matches floor(victimBP × 1000 / 10_000) at _warbowStealDrainBp (L1052) gated by require(take > 0, "TimeCurve: steal zero").
  2. dual-cap behavior tested: 4th steal binds attacker cap → bypass burn → succeeds, also tested for victim-cap binding from prior runs.
  3. #timecurve-warbow-steal-rules-gitlab-134 anchor reads cleanly — both counters mentioned, single section (no duplicate §134), play skill cross-links match.
    small follow-up worth flagging: there's no explicit fuzz over payBypassBurn=false with attacker-cap-only-binding state machine — the four-victim test covers attacker-cap-binding via the same flow, but no dedicated revert-message fuzz on attackerLimitBefore && !victimLimitBefore && !payBypassBurn. low priority, gap relative to spec coverage rather than a correctness issue.
    also: the constant name WARBOW_MAX_STEALS_PER_VICTIM_PER_DAY is now reused for the per-attacker daily cap (both share threshold of 3, so behaviorally correct), and the event field bypassedVictimDailyLimit is set on either limit kind binding now. cosmetic / observability only — could rename in a v2 event or alias for clarity.
    ready for close on your side.
@PlasticDigits — verification PASS on `a9e8d1b`. walked your checklist: 1. `forge test --match-test 'warbow_steal'` green: 9/9 PASS across both files. key tests: - `test_warbow_steal_revert_2x_rule` ✓ - `test_warbow_steal_revert_zero_attacker_bp` ✓ (fresh-wallet bypass closed) - `test_warbow_steal_attacker_daily_limit_fourth_needs_bypass` ✓ (per-attacker UTC day cap binds) - `test_warbow_steal_drains_ten_percent_and_burns_one_reserve` ✓ (10% floor) - `test_warbow_steal_fresh_wallet_drains_high_bp_victim_for_one_cl8y` ✓ (regression test for yesterday's documented attack) - `testFuzz_warbow_steal_burn_exact_to_sink` ✓ (256 runs) 2. contract behavior verified at source: `abp > 0 && vbp >= 2 * abp` ranking (TimeCurve.sol L654), `stealsCommittedByAttackerOnDay` per-attacker counter alongside per-victim, both checked at L627-631, both incremented at L664-669. bypass burn (`WARBOW_STEAL_LIMIT_BYPASS_BURN_WAD`) charged when EITHER limit binds (L642-648) — override is per-wallet on each side, not a global gate. 10% drain floor matches `floor(victimBP × 1000 / 10_000)` at `_warbowStealDrainBp` (L1052) gated by `require(take > 0, "TimeCurve: steal zero")`. 3. dual-cap behavior tested: 4th steal binds attacker cap → bypass burn → succeeds, also tested for victim-cap binding from prior runs. 4. `#timecurve-warbow-steal-rules-gitlab-134` anchor reads cleanly — both counters mentioned, single section (no duplicate §134), play skill cross-links match. small follow-up worth flagging: there's no explicit fuzz over `payBypassBurn=false` with attacker-cap-only-binding state machine — the four-victim test covers attacker-cap-binding via the same flow, but no dedicated revert-message fuzz on `attackerLimitBefore && !victimLimitBefore && !payBypassBurn`. low priority, gap relative to spec coverage rather than a correctness issue. also: the constant name `WARBOW_MAX_STEALS_PER_VICTIM_PER_DAY` is now reused for the per-attacker daily cap (both share threshold of 3, so behaviorally correct), and the event field `bypassedVictimDailyLimit` is set on either limit kind binding now. cosmetic / observability only — could rename in a v2 event or alias for clarity. ready for close on your side.
Brouie commented 2026-05-05 02:21:46 +00:00 (Migrated from gitlab.com)

mentioned in issue #148

mentioned in issue #148
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-05 02:33:44 +00:00
PlasticDigits commented 2026-05-17 13:45:30 +00:00 (Migrated from gitlab.com)

mentioned in issue #211

mentioned in issue #211
PlasticDigits commented 2026-05-19 07:53:23 +00:00 (Migrated from gitlab.com)

mentioned in issue #217

mentioned in issue #217
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#134
No description provided.