EcoStrategy sim suite: extend coverage for I-01-eco audit gaps (cooldown, sybil, flag mechanics, UTC rollover) #167

Closed
opened 2026-05-06 03:59:17 +00:00 by Brouie · 4 comments
Brouie commented 2026-05-06 03:59:17 +00:00 (Migrated from gitlab.com)

follow-up to #161 after the D + E ship — the A–E scope as-shipped is fine (verified separately on `390f9be`), but pulling four audit-coverage gaps out into their own ticket since closing them needs real implementation, not just polish.

every numeric constant the sim mirrors matches the contract. the gaps below are about which audit-finding scenarios got modeled vs tallied vs skipped — not correctness errors in what's already there.


Gap 1 — buy cooldown not enforced in `_simulate_sale` (M-03-eco baseline)

`scenarios.py:131-202` arrival loop draws `idx = rng.randrange(population)` per Poisson tick and lets the same wallet buy on consecutive ticks. no per-wallet `nextBuyAllowedAt` array, no gate.

onchain (`TimeCurve.sol:115`, `:128`, `:463`, `:598`) the `buyCooldownSec` gate is per-wallet and is the central mechanism the audit's M-03-eco section asks the sim to reason about. without modeling it, sybil-vs-whale comparisons can't separate "many wallets" from "one whale" — they're behaviorally identical in this sim — and any predator-pace experiment in C/D understates what the contract actually allows.

verified: `grep -n "cooldown|next_allowed|nextBuyAllowed" simulations/ecostrategy/scenarios.py` returns nothing.

fix shape: per-wallet `next_allowed_at: list[float]` next to budgets, gate arrivals on `now >= next_allowed_at[idx]`, set `next_allowed_at[idx] = now + p.buy_cooldown_sec` on a successful buy. pull `buy_cooldown_sec` from canonical params.


Gap 2 — M-03-eco sybil scenario structurally absent

audit M-03-eco asks for "scenarios with a single capital pool split across 1, 5, 25, and 100 wallets, measuring spend share, CHARM share, podium share, BP share, and value returned to early non-sybil buyers."

none of A–E expose a `wallets_per_operator` knob. scenario A's believer/shark split is a cohort partition, not a sybil cluster. scenario B's `cluster_frac` aggregates charm weight onto one index to demonstrate referral stacking, not buy-cadence parallelism.

verified: `grep -rn "wallets_per_operator|cluster_size|operator_capital|sybil" simulations/ecostrategy/` returns nothing.

fix shape: add a scenario F (or extend B) parameterized by `(operator_capital, wallets_per_operator)`. metric set: spend share, CHARM weight share, podium-slot count, BP share for the operator vs solo wallets at equal aggregate spend. depends on Gap 1 — without cooldown the sybil run is a no-op.


Gap 3 — L-02-eco flag claim/penalty mechanics are tally-only

scenario D advertises "optional WarBow flag plant tally" but only counts `flags_planted += 1`. it doesn't track `warbowPendingFlagOwner`, the 300-second silence window, the +1000 claim award, or the −2000 penalty when an interrupting buy lands after silence.

audit L-02-eco asks for "same-block flag claim vs interrupt-buy scenarios in simulations and manual QA." current code can't distinguish a successful claim from a penalty event.

contract refs: plant slot at `TimeCurve.sol:570-572`, penalty branch at `:511-518` with `pen = WARBOW_FLAG_CLAIM_BP * 2`, claim award at `:613` via `_addBattlePoints(msg.sender, WARBOW_FLAG_CLAIM_BP)`, silence at `WARBOW_FLAG_SILENCE_SEC = 300` (`:66`).

verified: `grep -n "WARBOW_FLAG|FLAG_CLAIM|FLAG_SILENCE|FLAG_PENALTY"` across `constants.py`, `warbow_pvp.py`, `scenarios.py` returns nothing — none of these constants are defined in sim.

fix shape: add `WARBOW_FLAG_CLAIM_BP = 1000`, `WARBOW_FLAG_SILENCE_SEC = 300` to constants. add `pending_flag_owner: int | None`, `pending_flag_plant_at: float` state to `WarBowWorld`. implement `try_plant_flag`, `try_claim_flag`, and an interrupter hook on each buy that applies the 2× penalty if silence elapsed.


Gap 4 — L-01-eco UTC rollover burst not exercised under default horizons

`utc_day = int(now_sec // 86400)` is correct (`warbow_pvp.py:58-60`), matches `block.timestamp / SECONDS_PER_DAY` at `TimeCurve.sol:628`. but every scenario starts at `now=0.0` (`scenarios.py:120`) with default `horizon_sec=28_800` (8h) — `now` never crosses a multiple of 86400 inside a single run.

the audit's rollover-burst attack pattern (3 steals before midnight + 3 after) is structurally unreachable on the default harness. `test_daily_limit_blocks_without_bypass` exercises caps within one UTC day but doesn't test the rollover transition.

fix shape: add an optional `start_unix_offset_sec` to `_simulate_sale` (or a dedicated rollover scenario) that begins `now` shortly before a UTC boundary. add a unit test asserting a 4th steal succeeds at `now = SECONDS_PER_DAY` without bypass burn.


Audit-coverage table at `390f9be`

  • H-01-eco — covered (E)
  • M-01-eco — partial (B aggregates referrer onto buyer, no sweep over referred-buy share)
  • M-02-eco — partial (2x rule, guard, revenge in WarBowWorld, no burn-cost accounting, no BP-ratio stress sweep)
  • M-03-eco — not covered (this gap)
  • M-04-eco — operational, partial via #160 indexer
  • M-05-eco — partial (anchor projection in E, no policy-violation branch)
  • L-01-eco — not exercised (this gap)
  • L-02-eco — not modeled (this gap)
  • I-01-eco — mostly addressed; main remaining items are this gap set

Priority order

Gap 1 (cooldown) is the unblocker — Gap 2 needs it. Gap 3 is independently scoped. Gap 4 is a small param + one unit test. cooldown + sybil together close the audit's biggest open question; flag mechanics + rollover close the L-tier gaps.

cc @PlasticDigits — file separately or roll into one MR, your call. happy to take cooldown + rollover myself if you'd rather keep flag mechanics + sybil scenario in your queue.

follow-up to #161 after the D + E ship — the A–E scope as-shipped is fine (verified separately on \`390f9be\`), but pulling four audit-coverage gaps out into their own ticket since closing them needs real implementation, not just polish. every numeric constant the sim mirrors matches the contract. the gaps below are about which audit-finding scenarios got modeled vs tallied vs skipped — not correctness errors in what's already there. -------- ## Gap 1 — buy cooldown not enforced in \`_simulate_sale\` (M-03-eco baseline) \`scenarios.py:131-202\` arrival loop draws \`idx = rng.randrange(population)\` per Poisson tick and lets the same wallet buy on consecutive ticks. no per-wallet \`nextBuyAllowedAt\` array, no gate. onchain (\`TimeCurve.sol:115\`, \`:128\`, \`:463\`, \`:598\`) the \`buyCooldownSec\` gate is per-wallet and is the central mechanism the audit's M-03-eco section asks the sim to reason about. without modeling it, sybil-vs-whale comparisons can't separate "many wallets" from "one whale" — they're behaviorally identical in this sim — and any predator-pace experiment in C/D understates what the contract actually allows. verified: \`grep -n "cooldown\|next_allowed\|nextBuyAllowed" simulations/ecostrategy/scenarios.py\` returns nothing. fix shape: per-wallet \`next_allowed_at: list[float]\` next to budgets, gate arrivals on \`now >= next_allowed_at[idx]\`, set \`next_allowed_at[idx] = now + p.buy_cooldown_sec\` on a successful buy. pull \`buy_cooldown_sec\` from canonical params. -------- ## Gap 2 — M-03-eco sybil scenario structurally absent audit M-03-eco asks for "scenarios with a single capital pool split across 1, 5, 25, and 100 wallets, measuring spend share, CHARM share, podium share, BP share, and value returned to early non-sybil buyers." none of A–E expose a \`wallets_per_operator\` knob. scenario A's believer/shark split is a cohort partition, not a sybil cluster. scenario B's \`cluster_frac\` aggregates charm weight onto one index to demonstrate referral stacking, not buy-cadence parallelism. verified: \`grep -rn "wallets_per_operator\|cluster_size\|operator_capital\|sybil" simulations/ecostrategy/\` returns nothing. fix shape: add a scenario F (or extend B) parameterized by \`(operator_capital, wallets_per_operator)\`. metric set: spend share, CHARM weight share, podium-slot count, BP share for the operator vs solo wallets at equal aggregate spend. depends on Gap 1 — without cooldown the sybil run is a no-op. -------- ## Gap 3 — L-02-eco flag claim/penalty mechanics are tally-only scenario D advertises "optional WarBow flag plant tally" but only counts \`flags_planted += 1\`. it doesn't track \`warbowPendingFlagOwner\`, the 300-second silence window, the +1000 claim award, or the −2000 penalty when an interrupting buy lands after silence. audit L-02-eco asks for "same-block flag claim vs interrupt-buy scenarios in simulations and manual QA." current code can't distinguish a successful claim from a penalty event. contract refs: plant slot at \`TimeCurve.sol:570-572\`, penalty branch at \`:511-518\` with \`pen = WARBOW_FLAG_CLAIM_BP * 2\`, claim award at \`:613\` via \`_addBattlePoints(msg.sender, WARBOW_FLAG_CLAIM_BP)\`, silence at \`WARBOW_FLAG_SILENCE_SEC = 300\` (\`:66\`). verified: \`grep -n "WARBOW_FLAG\|FLAG_CLAIM\|FLAG_SILENCE\|FLAG_PENALTY"\` across \`constants.py\`, \`warbow_pvp.py\`, \`scenarios.py\` returns nothing — none of these constants are defined in sim. fix shape: add \`WARBOW_FLAG_CLAIM_BP = 1000\`, \`WARBOW_FLAG_SILENCE_SEC = 300\` to constants. add \`pending_flag_owner: int | None\`, \`pending_flag_plant_at: float\` state to \`WarBowWorld\`. implement \`try_plant_flag\`, \`try_claim_flag\`, and an interrupter hook on each buy that applies the 2× penalty if silence elapsed. -------- ## Gap 4 — L-01-eco UTC rollover burst not exercised under default horizons \`utc_day = int(now_sec // 86400)\` is correct (\`warbow_pvp.py:58-60\`), matches \`block.timestamp / SECONDS_PER_DAY\` at \`TimeCurve.sol:628\`. but every scenario starts at \`now=0.0\` (\`scenarios.py:120\`) with default \`horizon_sec=28_800\` (8h) — \`now\` never crosses a multiple of 86400 inside a single run. the audit's rollover-burst attack pattern (3 steals before midnight + 3 after) is structurally unreachable on the default harness. \`test_daily_limit_blocks_without_bypass\` exercises caps within one UTC day but doesn't test the rollover transition. fix shape: add an optional \`start_unix_offset_sec\` to \`_simulate_sale\` (or a dedicated rollover scenario) that begins \`now\` shortly before a UTC boundary. add a unit test asserting a 4th steal succeeds at \`now = SECONDS_PER_DAY\` without bypass burn. -------- ## Audit-coverage table at \`390f9be\` - H-01-eco — covered (E) - M-01-eco — partial (B aggregates referrer onto buyer, no sweep over referred-buy share) - M-02-eco — partial (2x rule, guard, revenge in WarBowWorld, no burn-cost accounting, no BP-ratio stress sweep) - M-03-eco — not covered (this gap) - M-04-eco — operational, partial via #160 indexer - M-05-eco — partial (anchor projection in E, no policy-violation branch) - L-01-eco — not exercised (this gap) - L-02-eco — not modeled (this gap) - I-01-eco — mostly addressed; main remaining items are this gap set ## Priority order Gap 1 (cooldown) is the unblocker — Gap 2 needs it. Gap 3 is independently scoped. Gap 4 is a small param + one unit test. cooldown + sybil together close the audit's biggest open question; flag mechanics + rollover close the L-tier gaps. cc @PlasticDigits — file separately or roll into one MR, your call. happy to take cooldown + rollover myself if you'd rather keep flag mechanics + sybil scenario in your queue.
PlasticDigits commented 2026-05-06 08:56:43 +00:00 (Migrated from gitlab.com)

mentioned in commit 544e1dca1b

mentioned in commit 544e1dca1b914540c6710be7630a1ed80a80464b
PlasticDigits commented 2026-05-06 08:57:28 +00:00 (Migrated from gitlab.com)

Shipped: EcoStrategy sim coverage (GitLab #167)

Merged to main (includes merge with latest origin/main so #169 scenario D isolated `after_tick` stays intact).

What changed

  • Per-wallet buy cooldown in `_simulate_sale` using `TimeCurveParams.buyCooldownSec` (default 300 in `canonical_timecurve_params`), mirroring `nextBuyAllowedAt`.
  • `WarBowWorld`: `WARBOW_FLAG_CLAIM_BP` / `WARBOW_FLAG_SILENCE_SEC`, `apply_interrupting_buy`, `plant_flag_after_buy`, `try_claim_flag`, counters for claims/penalties.
  • Scenario F: `operator_capital` split across `wallets_per_operator` wallets + `operator_*` share metrics.
  • `start_unix_offset_sec` on `_simulate_sale` + `sim_start_unix_offset_sec` in metrics for UTC-boundary harnesses.
  • CLI: `--scenario F` and `all` includes F; JSON payload includes `gitlab_issue_coverage` → #167.
  • Docs: `INV-SIM-167` in `docs/testing/invariants-and-business-logic.md`; cross-links in `docs/testing/strategy.md`, `simulations/README.md`, `skills/README.md` (third-party modeling entry).

Verification checklist

  • `cd simulations && PYTHONPATH=. python3 -m unittest discover -s tests -v` — all green (includes new #167 tests + existing #169 parity test).
  • `PYTHONPATH=. python3 -m ecostrategy --scenario all --seed 42` — runs A–F; JSON shows `gitlab_issue_coverage` and F `operator_spend_share` / `operator_wallet_count`.
  • Spot-check `simulations/ecostrategy/constants.py` `MIRRORED_FROM_CONTRACT` lists `WARBOW_FLAG_*`.
  • Read `INV-SIM-167` section in invariants doc and confirm anchors match issue gaps (cooldown, sybil F, flag ladder, UTC offset).

Please verify when you have a cycle. @brouie — requesting your sign-off on whether this closes the I-01-eco gap set from #167 or if you want follow-up (e.g. auto `try_claim_flag` ticks in scenarios).

Issue left open per maintainer request.

## Shipped: EcoStrategy sim coverage (GitLab #167) Merged to **main** (includes merge with latest **origin/main** so **#169** scenario D isolated \`after_tick\` stays intact). ### What changed - **Per-wallet buy cooldown** in \`_simulate_sale\` using \`TimeCurveParams.buyCooldownSec\` (default **300** in \`canonical_timecurve_params\`), mirroring \`nextBuyAllowedAt\`. - **\`WarBowWorld\`**: \`WARBOW_FLAG_CLAIM_BP\` / \`WARBOW_FLAG_SILENCE_SEC\`, \`apply_interrupting_buy\`, \`plant_flag_after_buy\`, \`try_claim_flag\`, counters for claims/penalties. - **Scenario F**: \`operator_capital\` split across \`wallets_per_operator\` wallets + \`operator_*\` share metrics. - **\`start_unix_offset_sec\`** on \`_simulate_sale\` + \`sim_start_unix_offset_sec\` in metrics for UTC-boundary harnesses. - **CLI**: \`--scenario F\` and \`all\` includes F; JSON payload includes \`gitlab_issue_coverage\` → #167. - **Docs**: \`INV-SIM-167\` in \`docs/testing/invariants-and-business-logic.md\`; cross-links in \`docs/testing/strategy.md\`, \`simulations/README.md\`, \`skills/README.md\` (third-party modeling entry). ### Verification checklist - [ ] \`cd simulations && PYTHONPATH=. python3 -m unittest discover -s tests -v\` — all green (includes new #167 tests + existing #169 parity test). - [ ] \`PYTHONPATH=. python3 -m ecostrategy --scenario all --seed 42\` — runs **A–F**; JSON shows \`gitlab_issue_coverage\` and **F** \`operator_spend_share\` / \`operator_wallet_count\`. - [ ] Spot-check \`simulations/ecostrategy/constants.py\` \`MIRRORED_FROM_CONTRACT\` lists \`WARBOW_FLAG_*\`. - [ ] Read \`INV-SIM-167\` section in invariants doc and confirm anchors match issue gaps (cooldown, sybil F, flag ladder, UTC offset). Please verify when you have a cycle. **@brouie** — requesting your sign-off on whether this closes the I-01-eco gap set from #167 or if you want follow-up (e.g. auto \`try_claim_flag\` ticks in scenarios). Issue left **open** per maintainer request.
Brouie commented 2026-05-07 02:44:09 +00:00 (Migrated from gitlab.com)

@PlasticDigits — verification PASS on 544e1dc (merge 14e0375).

  1. unittest discover — 42/42 OK in 0.502s. includes new #167 cases (test_utc_rollover_fourth_steal_without_bypass, scenario F operator metrics, flag mechanics interrupt/penalty/claim) and the #169 regressions (test_scenario_d_warbow_guards_parity_seed_42, test_scenario_d_ordering_branches).

  2. python3 -m ecostrategy --scenario all --seed 42 — runs A through F, JSON payload includes gitlab_issue_coverage: https://gitlab.com/PlasticDigits/yieldomega/-/issues/167. scenario F metrics at seed 42 / pop 1000 default:

operator_spend_share:           0.0126
operator_charm_weight_share:    0.0126
operator_bp_share:              0.0159
operator_podium_slots_top3_bp:  0
operator_wallet_count:          5

(0 podium slots at this scale is expected — operator pool is 5 wallets out of 1000 sybiling against ~$500k of $40M+ aggregate spend; the BP share advantage shows up but not enough to crack top-3 at default seed. confirms the metric machinery works without implying that 5/1000 sybils dominate, which would itself be a finding.)

  1. MIRRORED_FROM_CONTRACT at simulations/ecostrategy/constants.py:34-56 lists both WARBOW_FLAG_CLAIM_BP and WARBOW_FLAG_SILENCE_SEC. cross-checked vs contract — both match TimeCurve.sol:64 (1000 BP) and :66 (300s). penalty math matches contract :511-518 (2 × WARBOW_FLAG_CLAIM_BP).

  2. INV-SIM-167 at docs/testing/invariants-and-business-logic.md:1123 reads cleanly — all 4 audit-gap anchors present (cooldown M-03-eco baseline, scenario F sybil M-03-eco slice, flag interrupt/claim L-02-eco, UTC offset L-01-eco). cross-links from INV-SIM-161 at :1117 and INV-SIM-169-D at the same anchor.

on your auto try_claim_flag question — recommendation: yes-later, not now. the mechanism is implemented and unit-tested in isolation (test_warbow_flag_* cases at test_ecostrategy.py:99-114). adding it to scenario after_tick flow is mechanically trivial (the helper has access to world, now, rng already), but it would shift the seed-42 metrics on D — _scenario_c_style_after_tick recomputes the leader each tick, and flag claims add BP that could flip leader tracking. since seed-42 just got pinned by #169 as a regression test, churning those expectations right after lands feels premature. the audit narrative #167 cites (interrupt-buy penalty, claim BP award) is already covered as unit tests; the downstream "operator who plants AND claims" or "leader who plants AND defends-via-silence" narratives are adjacent to L-02-eco but not in the audit scope.

better fit: spawn a follow-up ticket like "scenario G: planters who actually claim" or extend D with a claim_flag_prob arg + paired-run parity test, when an audit thread asks for it explicitly. happy to file when you decide.

one nit on test pinning that would be useful follow-up:

  • scenario F operator-share assertion uses > 0.0 floor (test_scenario_f_operator_metrics); a regression range like 0.01 ≤ operator_spend_share ≤ 0.05 for fixed seed would catch numeric drift better
  • same-buyer-after-silence flag-claim arm at warbow_pvp.py:147 (buyer == owner: return 0) covered by branch but no isolated test pins it

ready for close on your side.

@PlasticDigits — verification PASS on `544e1dc` (merge `14e0375`). 1. unittest discover — 42/42 OK in 0.502s. includes new #167 cases (`test_utc_rollover_fourth_steal_without_bypass`, scenario F operator metrics, flag mechanics interrupt/penalty/claim) and the #169 regressions (`test_scenario_d_warbow_guards_parity_seed_42`, `test_scenario_d_ordering_branches`). 2. `python3 -m ecostrategy --scenario all --seed 42` — runs A through F, JSON payload includes `gitlab_issue_coverage: https://gitlab.com/PlasticDigits/yieldomega/-/issues/167`. scenario F metrics at seed 42 / pop 1000 default: ``` operator_spend_share: 0.0126 operator_charm_weight_share: 0.0126 operator_bp_share: 0.0159 operator_podium_slots_top3_bp: 0 operator_wallet_count: 5 ``` (0 podium slots at this scale is expected — operator pool is 5 wallets out of 1000 sybiling against ~$500k of $40M+ aggregate spend; the BP share advantage shows up but not enough to crack top-3 at default seed. confirms the metric machinery works without implying that 5/1000 sybils dominate, which would itself be a finding.) 3. `MIRRORED_FROM_CONTRACT` at `simulations/ecostrategy/constants.py:34-56` lists both `WARBOW_FLAG_CLAIM_BP` and `WARBOW_FLAG_SILENCE_SEC`. cross-checked vs contract — both match `TimeCurve.sol:64` (1000 BP) and `:66` (300s). penalty math matches contract `:511-518` (`2 × WARBOW_FLAG_CLAIM_BP`). 4. `INV-SIM-167` at `docs/testing/invariants-and-business-logic.md:1123` reads cleanly — all 4 audit-gap anchors present (cooldown M-03-eco baseline, scenario F sybil M-03-eco slice, flag interrupt/claim L-02-eco, UTC offset L-01-eco). cross-links from `INV-SIM-161` at `:1117` and `INV-SIM-169-D` at the same anchor. on your auto `try_claim_flag` question — recommendation: yes-later, not now. the mechanism is implemented and unit-tested in isolation (`test_warbow_flag_*` cases at `test_ecostrategy.py:99-114`). adding it to scenario after_tick flow is mechanically trivial (the helper has access to `world`, `now`, `rng` already), but it would shift the seed-42 metrics on D — `_scenario_c_style_after_tick` recomputes the leader each tick, and flag claims add BP that could flip leader tracking. since seed-42 just got pinned by #169 as a regression test, churning those expectations right after lands feels premature. the audit narrative #167 cites (interrupt-buy penalty, claim BP award) is already covered as unit tests; the downstream "operator who plants AND claims" or "leader who plants AND defends-via-silence" narratives are adjacent to L-02-eco but not in the audit scope. better fit: spawn a follow-up ticket like "scenario G: planters who actually claim" or extend D with a `claim_flag_prob` arg + paired-run parity test, when an audit thread asks for it explicitly. happy to file when you decide. one nit on test pinning that would be useful follow-up: - scenario F operator-share assertion uses `> 0.0` floor (`test_scenario_f_operator_metrics`); a regression range like `0.01 ≤ operator_spend_share ≤ 0.05` for fixed seed would catch numeric drift better - same-buyer-after-silence flag-claim arm at `warbow_pvp.py:147` (buyer == owner: return 0) covered by branch but no isolated test pins it ready for close on your side.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-09 04:57:44 +00:00
PlasticDigits commented 2026-05-13 15:13:33 +00:00 (Migrated from gitlab.com)

mentioned in commit a6634e6328

mentioned in commit a6634e6328917dab3bc47584826b88c175c25ef6
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#167
No description provided.