fix(arena): accrue epoch CRED pool on buyWithCred buys (#311) #479

Merged
PlasticDigits merged 2 commits from local1/311-verify-cred-pool-accrue into main 2026-06-14 05:07:39 +00:00
PlasticDigits commented 2026-06-14 02:50:08 +00:00 (Migrated from gitlab.com)

Summary

  • buyWithCred now calls _accrueCharmAndCred (with zero codeHash) so each CRED buy adds 35 CRED (CRED_PER_BUY) to epochCredPool[lastBuyEpoch] while still burning player CRED.
  • Added forge tests for pool delta, mixed DOUB/CRED pro-rata, epoch-boundary accrual, and CRED-only fair-share claims.
  • Indexer pending_cred preview counts all epoch buys (DOUB + CRED); frontend optimistic preview aligned.
  • Replaced INV-TIME-ARENA-CRED-NO-POOL with INV-TIME-ARENA-CRED-POOL-ACCRUE; updated product docs, play skill, and Anvil smoke (verify-cred-buy-anvil.sh).

Acceptance checklist

Criterion Command / evidence Result
Each buyWithCred adds 35 CRED to epochCredPool[epoch] forge test --match-test test_buy_with_cred PASS
CHARM weight accrual unchanged in intent forge test --match-test test_xp_buy_with_cred_same_as_doub PASS
claimCred pro-rata fair for mixed DOUB/CRED buyers forge test --match-test test_buyWithCred_mixed_pro_rata_with_doub PASS
Invariant doc updated; CRED-NO-POOL removed docs/testing/invariants-and-business-logic.md → INV-TIME-ARENA-CRED-POOL-ACCRUE PASS
Forge pool delta assertion test_buy_with_cred, test_cred_only_buyer_fair_share PASS
verify-cred-buy-anvil.sh bash scripts/verify-cred-buy-anvil.sh PASS
No regression in DOUB pool accrual forge test --match-test test_cred_accrue_on_doub_buy PASS
Indexer pool-related fields cd indexer && cargo test arena_wallet_stats PASS
CRED-only buyer fair share (attack) test_cred_only_buyer_fair_share PASS
Epoch boundary pool credit test_buyWithCred_at_epoch_boundary PASS
Double-claim still one-shot test_claimCred_cannot_double_claim PASS

Test plan

export PATH="$HOME/.foundry/bin:$PATH"
cd contracts && forge test --match-contract TimeArenaTest
bash scripts/verify-cred-buy-anvil.sh
source /usr/local/cargo/env 2>/dev/null || true
cd indexer && cargo clippy --all-targets -- -D warnings && cargo test arena_wallet_stats
cd frontend && npm run typecheck && npm test -- --run src/lib/arenaWalletXpOptimistic.test.ts

Note

Medium Risk
Changes how pending CRED is computed for wallets and UI; wrong counts would mislead players before claims, but behavior is intended to mirror onchain #311 pool accrual.

Overview
Aligns offchain CRED pool math and docs with onchain rule that each buyWithCred adds 35 CRED to epochCredPool (same as DOUB buys), not DOUB-only pool sizing.

Indexer and frontend now treat epoch pool size as all buys in the epoch (DOUB + CRED): arena_wallet_stats uses COUNT(*) on idx_arena_buy instead of DOUB-only; wallet stats expose epoch_buy_count (with epoch_doub_buy_count deprecated but duplicated for older clients). applyCharmCredBuyToWalletStats increments epoch buy count and pending CRED for both pay paths, so optimistic UI matches pendingCred after CRED buys.

Forge tests are renamed and expanded for pool delta, mixed pro-rata, epoch boundaries, and CRED-only fair share; invariant doc replaces INV-TIME-ARENA-CRED-POOL-PARITY with INV-TIME-ARENA-CRED-POOL-ACCRUE. Glossary states DOUB and CRED buys accrue 35 CRED per buy. verify-cred-buy-anvil.sh gets a small cast_u256 helper fix for keyed calls.

Reviewed by Cursor Bugbot for commit 6dda90975b. Bugbot is set up for automated code reviews on this repo. Configure here.

## Summary - `buyWithCred` now calls `_accrueCharmAndCred` (with zero `codeHash`) so each CRED buy adds **35 CRED** (`CRED_PER_BUY`) to `epochCredPool[lastBuyEpoch]` while still burning player CRED. - Added forge tests for pool delta, mixed DOUB/CRED pro-rata, epoch-boundary accrual, and CRED-only fair-share claims. - Indexer `pending_cred` preview counts all epoch buys (DOUB + CRED); frontend optimistic preview aligned. - Replaced `INV-TIME-ARENA-CRED-NO-POOL` with `INV-TIME-ARENA-CRED-POOL-ACCRUE`; updated product docs, play skill, and Anvil smoke (`verify-cred-buy-anvil.sh`). ## Acceptance checklist | Criterion | Command / evidence | Result | |-----------|-------------------|--------| | Each `buyWithCred` adds 35 CRED to `epochCredPool[epoch]` | `forge test --match-test test_buy_with_cred` | PASS | | CHARM weight accrual unchanged in intent | `forge test --match-test test_xp_buy_with_cred_same_as_doub` | PASS | | `claimCred` pro-rata fair for mixed DOUB/CRED buyers | `forge test --match-test test_buyWithCred_mixed_pro_rata_with_doub` | PASS | | Invariant doc updated; `CRED-NO-POOL` removed | `docs/testing/invariants-and-business-logic.md` → `INV-TIME-ARENA-CRED-POOL-ACCRUE` | PASS | | Forge pool delta assertion | `test_buy_with_cred`, `test_cred_only_buyer_fair_share` | PASS | | `verify-cred-buy-anvil.sh` | `bash scripts/verify-cred-buy-anvil.sh` | PASS | | No regression in DOUB pool accrual | `forge test --match-test test_cred_accrue_on_doub_buy` | PASS | | Indexer pool-related fields | `cd indexer && cargo test arena_wallet_stats` | PASS | | CRED-only buyer fair share (attack) | `test_cred_only_buyer_fair_share` | PASS | | Epoch boundary pool credit | `test_buyWithCred_at_epoch_boundary` | PASS | | Double-claim still one-shot | `test_claimCred_cannot_double_claim` | PASS | ## Test plan ```bash export PATH="$HOME/.foundry/bin:$PATH" cd contracts && forge test --match-contract TimeArenaTest bash scripts/verify-cred-buy-anvil.sh source /usr/local/cargo/env 2>/dev/null || true cd indexer && cargo clippy --all-targets -- -D warnings && cargo test arena_wallet_stats cd frontend && npm run typecheck && npm test -- --run src/lib/arenaWalletXpOptimistic.test.ts ``` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how pending CRED is computed for wallets and UI; wrong counts would mislead players before claims, but behavior is intended to mirror onchain #311 pool accrual. > > **Overview** > Aligns **offchain CRED pool math and docs** with onchain rule that **each `buyWithCred` adds 35 CRED** to `epochCredPool` (same as DOUB buys), not DOUB-only pool sizing. > > **Indexer and frontend** now treat epoch pool size as **all buys in the epoch** (DOUB + CRED): `arena_wallet_stats` uses `COUNT(*)` on `idx_arena_buy` instead of DOUB-only; wallet stats expose **`epoch_buy_count`** (with **`epoch_doub_buy_count`** deprecated but duplicated for older clients). **`applyCharmCredBuyToWalletStats`** increments epoch buy count and pending CRED for **both** pay paths, so optimistic UI matches `pendingCred` after CRED buys. > > **Forge tests** are renamed and expanded for pool delta, mixed pro-rata, epoch boundaries, and CRED-only fair share; invariant doc replaces **`INV-TIME-ARENA-CRED-POOL-PARITY`** with **`INV-TIME-ARENA-CRED-POOL-ACCRUE`**. Glossary states DOUB and CRED buys accrue 35 CRED per buy. **`verify-cred-buy-anvil.sh`** gets a small `cast_u256` helper fix for keyed calls. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 6dda90975b2407b117817eb5deb65a49ddb378c2. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
PlasticDigits (Migrated from gitlab.com) approved these changes 2026-06-14 02:50:08 +00:00
project_80561744_bot_5118c68a88bff95eacb6ab1c47c19cae commented 2026-06-14 02:50:17 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
project_80561744_bot_5118c68a88bff95eacb6ab1c47c19cae commented 2026-06-14 02:50:26 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-14 02:56:25 +00:00 (Migrated from gitlab.com)

Security review — MR !479

Commit reviewed: 7d862aafa1db0a44eb185681f39753d39694c0d5

Scope: buyWithCred → _accrueCharmAndCred (epoch CRED pool accrual), related Forge tests, indexer pending_cred / epoch_buy_count, frontend optimistic CRED preview, verify-cred-buy-anvil.sh, docs/skills.

Outcome: FINDINGS: 0 medium+

Summary

Reviewed the on-chain CRED pool economics, claim path, reentrancy/ordering, and off-chain preview/indexer changes introduced by this diff. No prior security-review threads on this MR.

On-chain (TimeArena._buyCred): buyWithCred still burns credBurn = mulDiv(charmWad, CRED_PER_CHARM_WAD, WAD) before state updates, passes bytes32(0) (no referral mint path), and remains nonReentrant. Pool accrual is a fixed CRED_PER_BUY (35e18) per buy into epochCredPool[lastBuyEpoch]; claimCred mints pro-rata by epochCharmWad / epochCharmTotal with one-shot weight zeroing (unchanged). For a sole 1 CHARM CRED buyer, max pro-rata recovery is 35 CRED against a 100 CRED burn; mixed-epoch and CRED-only fair-share cases are covered by new Forge tests (test_cred_only_buyer_fair_share, test_buyWithCred_mixed_pro_rata_with_doub, test_buyWithCred_at_epoch_boundary). No plausible path to mint more than fair pool share, double-claim, or reenter via PlayCred (non-transferable, no hooks).

Off-chain (indexer + frontend): SQL still parameterized; epoch_buys count change only affects pending_cred preview/display, not claim execution (on-chain claimCred / pendingCred are authoritative). No injection, authz, SSRF, or XSS surface in the modified paths.

Inline threads: none (no medium+ findings).

Security review: no medium+ findings on this diff.

## Security review — MR !479 **Commit reviewed:** `7d862aafa1db0a44eb185681f39753d39694c0d5` **Scope:** `buyWithCred` → `_accrueCharmAndCred` (epoch CRED pool accrual), related Forge tests, indexer `pending_cred` / `epoch_buy_count`, frontend optimistic CRED preview, `verify-cred-buy-anvil.sh`, docs/skills. **Outcome:** **FINDINGS: 0** medium+ ### Summary Reviewed the on-chain CRED pool economics, claim path, reentrancy/ordering, and off-chain preview/indexer changes introduced by this diff. No prior security-review threads on this MR. **On-chain (`TimeArena._buyCred`):** `buyWithCred` still burns `credBurn = mulDiv(charmWad, CRED_PER_CHARM_WAD, WAD)` before state updates, passes `bytes32(0)` (no referral mint path), and remains `nonReentrant`. Pool accrual is a fixed `CRED_PER_BUY` (35e18) per buy into `epochCredPool[lastBuyEpoch]`; `claimCred` mints pro-rata by `epochCharmWad / epochCharmTotal` with one-shot weight zeroing (unchanged). For a sole 1 CHARM CRED buyer, max pro-rata recovery is 35 CRED against a 100 CRED burn; mixed-epoch and CRED-only fair-share cases are covered by new Forge tests (`test_cred_only_buyer_fair_share`, `test_buyWithCred_mixed_pro_rata_with_doub`, `test_buyWithCred_at_epoch_boundary`). No plausible path to mint more than fair pool share, double-claim, or reenter via `PlayCred` (non-transferable, no hooks). **Off-chain (indexer + frontend):** SQL still parameterized; `epoch_buys` count change only affects `pending_cred` preview/display, not claim execution (on-chain `claimCred` / `pendingCred` are authoritative). No injection, authz, SSRF, or XSS surface in the modified paths. **Inline threads:** none (no medium+ findings). Security review: no medium+ findings on this diff.
PlasticDigits commented 2026-06-14 03:00:42 +00:00 (Migrated from gitlab.com)

added 61 commits

  • 7d862aaf...03a43d4a - 60 commits from branch main
  • 6dda9097 - Merge origin/main into local1/311-verify-cred-pool-accrue

Compare with previous version

added 61 commits <ul><li>7d862aaf...03a43d4a - 60 commits from branch <code>main</code></li><li>6dda9097 - Merge origin/main into local1/311-verify-cred-pool-accrue</li></ul> [Compare with previous version](/PlasticDigits/yieldomega/-/merge_requests/109/diffs?diff_id=1850561473&start_sha=7d862aafa1db0a44eb185681f39753d39694c0d5)
PlasticDigits commented 2026-06-14 03:00:46 +00:00 (Migrated from gitlab.com)

Resolved merge conflicts with main (merge commit 6dda9097).

Conflicts resolved

  • TimeArena.sol — kept main's _applyBuyWarBowBp signature (prevDsBuyer / prevDsStreak for #310 ambush BP).
  • TimeArena.t.sol — merged CRED pool test coverage from both branches (accrue, mixed pro-rata, epoch roll, hard-reset boundary, fair-share claim).
  • Docs / invariants / play skill — unified on INV-TIME-ARENA-CRED-POOL-ACCRUE with combined test list; kept FIRST_BUY_CRED_BONUS = 1100e18 from main (#299).
  • verify-cred-buy-anvil.sh — kept cast_u256 helper; main's pool-delta assertion message.

Sanity checks

  • forge test --match-contract TimeArenaTest — 102 passed
  • cargo test arena_wallet_stats — passed
  • npm run typecheck + arenaWalletXpOptimistic.test.ts — passed
Resolved merge conflicts with `main` (merge commit `6dda9097`). **Conflicts resolved** - `TimeArena.sol` — kept main's `_applyBuyWarBowBp` signature (`prevDsBuyer` / `prevDsStreak` for #310 ambush BP). - `TimeArena.t.sol` — merged CRED pool test coverage from both branches (accrue, mixed pro-rata, epoch roll, hard-reset boundary, fair-share claim). - Docs / invariants / play skill — unified on `INV-TIME-ARENA-CRED-POOL-ACCRUE` with combined test list; kept `FIRST_BUY_CRED_BONUS = 1100e18` from main (#299). - `verify-cred-buy-anvil.sh` — kept `cast_u256` helper; main's pool-delta assertion message. **Sanity checks** - `forge test --match-contract TimeArenaTest` — 102 passed - `cargo test arena_wallet_stats` — passed - `npm run typecheck` + `arenaWalletXpOptimistic.test.ts` — passed
project_80561744_bot_5118c68a88bff95eacb6ab1c47c19cae commented 2026-06-14 03:01:03 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-14 03:03:11 +00:00 (Migrated from gitlab.com)

Security review — MR !479

Commit reviewed: 6dda90975b2407b117817eb5deb65a49ddb378c2

Scope: Re-review after merge with main. Current diff vs 03a43d4a: Forge test consolidation, indexer epoch_buys / pending_cred preview, frontend optimistic CRED preview (arenaWalletXpOptimistic.ts), docs/invariants, verify-cred-buy-anvil.sh. On-chain buyWithCred → _accrueCharmAndCred (epoch pool +35 CRED per buy) is already on main via fix/311-cred-buy-epoch-pool; verified at HEAD in TimeArena._buyCred.

Outcome: FINDINGS: 0 medium+

Summary

Re-reviewed prior thread (7d862aaf) against post-merge HEAD. No new attack surface in the incremental diff; on-chain economics at HEAD unchanged in intent from the earlier review.

On-chain (TimeArena._buyCred): buyWithCred burns credBurn = mulDiv(charmWad, CRED_PER_CHARM_WAD, WAD) before _accrueCharmAndCred, passes bytes32(0) (no referral mint), and remains nonReentrant. Pool accrual is fixed CRED_PER_BUY (35e18) into epochCredPool[lastBuyEpoch]; claimCred pro-rata mint is unchanged (mulDiv(pool, weight, total) with one-shot weight zeroing). A sole 1 CHARM CRED buyer recovers at most 35 CRED against a 100 CRED burn; mixed DOUB/CRED pro-rata, epoch-boundary pool routing, and fair-share bounds are covered by Forge tests (test_cred_only_buyer_fair_share, test_buyWithCred_mixed_pro_rata_with_doub, test_buyWithCred_at_epoch_boundary). PlayCred is non-transferable ERC20 with minter-only burn/mint — no external hooks or reentrancy sink. This change closes the pre-#311 asymmetry where CRED buyers accrued CHARM weight without adding to epochCredPool (diluting DOUB-funded claims); no path to over-mint pool share, double-claim, or referral abuse.

Off-chain (indexer + frontend): SQL remains parameterized ($1 wallet, $2 epoch); COUNT(*) on idx_arena_buy only affects pending_cred_accrual display/preview — claim execution stays on-chain (claimCred / pendingCred). No injection, authz bypass, SSRF, or XSS in modified paths.

Inline threads: none (no medium+ findings).

Security review: no medium+ findings on this diff.

## Security review — MR !479 **Commit reviewed:** `6dda90975b2407b117817eb5deb65a49ddb378c2` **Scope:** Re-review after merge with `main`. Current diff vs `03a43d4a`: Forge test consolidation, indexer `epoch_buys` / `pending_cred` preview, frontend optimistic CRED preview (`arenaWalletXpOptimistic.ts`), docs/invariants, `verify-cred-buy-anvil.sh`. On-chain `buyWithCred` → `_accrueCharmAndCred` (epoch pool +35 CRED per buy) is already on `main` via `fix/311-cred-buy-epoch-pool`; verified at HEAD in `TimeArena._buyCred`. **Outcome:** **FINDINGS: 0** medium+ ### Summary Re-reviewed prior thread (`7d862aaf`) against post-merge HEAD. No new attack surface in the incremental diff; on-chain economics at HEAD unchanged in intent from the earlier review. **On-chain (`TimeArena._buyCred`):** `buyWithCred` burns `credBurn = mulDiv(charmWad, CRED_PER_CHARM_WAD, WAD)` before `_accrueCharmAndCred`, passes `bytes32(0)` (no referral mint), and remains `nonReentrant`. Pool accrual is fixed `CRED_PER_BUY` (35e18) into `epochCredPool[lastBuyEpoch]`; `claimCred` pro-rata mint is unchanged (`mulDiv(pool, weight, total)` with one-shot weight zeroing). A sole 1 CHARM CRED buyer recovers at most 35 CRED against a 100 CRED burn; mixed DOUB/CRED pro-rata, epoch-boundary pool routing, and fair-share bounds are covered by Forge tests (`test_cred_only_buyer_fair_share`, `test_buyWithCred_mixed_pro_rata_with_doub`, `test_buyWithCred_at_epoch_boundary`). `PlayCred` is non-transferable ERC20 with minter-only burn/mint — no external hooks or reentrancy sink. This change closes the pre-#311 asymmetry where CRED buyers accrued CHARM weight without adding to `epochCredPool` (diluting DOUB-funded claims); no path to over-mint pool share, double-claim, or referral abuse. **Off-chain (indexer + frontend):** SQL remains parameterized (`$1` wallet, `$2` epoch); `COUNT(*)` on `idx_arena_buy` only affects `pending_cred_accrual` display/preview — claim execution stays on-chain (`claimCred` / `pendingCred`). No injection, authz bypass, SSRF, or XSS in modified paths. **Inline threads:** none (no medium+ findings). Security review: no medium+ findings on this diff.
PlasticDigits commented 2026-06-14 05:07:36 +00:00 (Migrated from gitlab.com)

approved this merge request

approved this merge request
PlasticDigits (Migrated from gitlab.com) merged commit 08492fd892 into main 2026-06-14 05:07:39 +00:00
PlasticDigits commented 2026-06-14 05:07:40 +00:00 (Migrated from gitlab.com)

mentioned in commit 08492fd892

mentioned in commit 08492fd892d9ced7a6624a9a96850465f9dfaa1e
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!479
No description provided.