Fix buyWithCred to contribute 35 CRED to epoch pool per buy (#311) #460

Merged
PlasticDigits merged 1 commit from fix/311-cred-buy-epoch-pool into main 2026-06-13 09:28:49 +00:00
PlasticDigits commented 2026-06-13 08:05:13 +00:00 (Migrated from gitlab.com)

Summary

Fixes #311: buyWithCred now mirrors DOUB buys for epoch CRED economics by calling _accrueCharmAndCred (with codeHash = 0, so no referral mint). Each CRED buy still burns player CRED and skips DOUB podium routing, but adds CRED_PER_BUY (35e18) to epochCredPool[lastBuyEpoch].

  • Replaces INV-TIME-ARENA-CRED-NO-POOL with INV-TIME-ARENA-CRED-POOL-PARITY
  • Adds Forge tests for pool delta, mixed DOUB/CRED pro-rata, and epoch-boundary accrual
  • Updates product docs, play skill, frontend CRED copy, and verify-cred-buy-anvil.sh (pool assertion + onboarding level expectation aligned with #299)

Acceptance checklist

Criterion Verification Result
Each buyWithCred adds 35 CRED to epochCredPool[epoch] cd contracts && forge test --match-test "cred_accrue_on_cred_buy|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_cred_pro_rata_mixed_doub_and_cred_buyers PASS
Invariant doc updated; CRED-NO-POOL revised docs/testing/invariants-and-business-logic.md §311 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
Epoch boundary credits correct pool forge test --match-test test_cred_accrue_buyWithCred_at_epoch_boundary PASS
Double-claim still blocked forge test --match-test test_claimCred_cannot_double_claim PASS
Indexer pool decode (unchanged surface) cd indexer && cargo test SKIP — unrelated pre-existing config::database_pool_max_tests::rejects_zero failure on main

Third-party verification

export PATH="$HOME/.foundry/bin:$PATH"
cd contracts && forge test --match-test "cred|buy_with_cred|buyWithCred"
bash scripts/verify-cred-buy-anvil.sh

Confirm onchain after a CRED buy:

cast call $TIME_ARENA "epochCredPool(uint256)(uint256)" $EPOCH --rpc-url $RPC
# delta per buy should be 35000000000000000000 (35e18)

Docs: invariants §311 · play skill


Note

Medium Risk
Changes on-chain epoch CRED economics for every buyWithCred, which shifts pro-rata claim outcomes when DOUB and CRED buyers share an epoch; behavior is covered by new tests and Anvil smoke checks.

Overview
buyWithCred no longer uses charm-only accrual; it calls _accrueCharmAndCred(buyer, charmWad, bytes32(0)) so each CRED buy adds CRED_PER_BUY (35e18) to epochCredPool[lastBuyEpoch] like DOUB buys, with no referral mint. The unused _accrueCharmOnly helper is removed.

Docs, invariants (INV-TIME-ARENA-CRED-POOL-PARITY replaces CRED-NO-POOL), play skill, and arena CRED copy now state DOUB and CRED buys both feed the epoch pool. Forge adds mixed pro-rata, pool delta, and hard-reset boundary tests; verify-cred-buy-anvil.sh asserts epochCredPool +35 after a CRED buy and expects level 2 after one starter buyWithCred (10 CHARM XP).

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

## Summary Fixes [#311](https://gitlab.com/PlasticDigits/yieldomega/-/issues/311): **`buyWithCred`** now mirrors DOUB buys for epoch CRED economics by calling **`_accrueCharmAndCred`** (with `codeHash = 0`, so no referral mint). Each CRED buy still burns player CRED and skips DOUB podium routing, but adds **`CRED_PER_BUY` (35e18)** to **`epochCredPool[lastBuyEpoch]`**. - Replaces **`INV-TIME-ARENA-CRED-NO-POOL`** with **`INV-TIME-ARENA-CRED-POOL-PARITY`** - Adds Forge tests for pool delta, mixed DOUB/CRED pro-rata, and epoch-boundary accrual - Updates product docs, play skill, frontend CRED copy, and **`verify-cred-buy-anvil.sh`** (pool assertion + onboarding level expectation aligned with #299) ## Acceptance checklist | Criterion | Verification | Result | |-----------|--------------|--------| | Each `buyWithCred` adds 35 CRED to `epochCredPool[epoch]` | `cd contracts && forge test --match-test "cred_accrue_on_cred_buy\|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_cred_pro_rata_mixed_doub_and_cred_buyers` | PASS | | Invariant doc updated; `CRED-NO-POOL` revised | `docs/testing/invariants-and-business-logic.md` §311 | 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 | | Epoch boundary credits correct pool | `forge test --match-test test_cred_accrue_buyWithCred_at_epoch_boundary` | PASS | | Double-claim still blocked | `forge test --match-test test_claimCred_cannot_double_claim` | PASS | | Indexer pool decode (unchanged surface) | `cd indexer && cargo test` | SKIP — unrelated pre-existing `config::database_pool_max_tests::rejects_zero` failure on main | ## Third-party verification ```bash export PATH="$HOME/.foundry/bin:$PATH" cd contracts && forge test --match-test "cred|buy_with_cred|buyWithCred" bash scripts/verify-cred-buy-anvil.sh ``` Confirm onchain after a CRED buy: ```bash cast call $TIME_ARENA "epochCredPool(uint256)(uint256)" $EPOCH --rpc-url $RPC # delta per buy should be 35000000000000000000 (35e18) ``` Docs: [invariants §311](docs/testing/invariants-and-business-logic.md#timearena-cred-pool-parity-gitlab-311) · [play skill](skills/play-time-arena-doub/SKILL.md) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes on-chain epoch CRED economics for every `buyWithCred`, which shifts pro-rata claim outcomes when DOUB and CRED buyers share an epoch; behavior is covered by new tests and Anvil smoke checks. > > **Overview** > **`buyWithCred`** no longer uses charm-only accrual; it calls **`_accrueCharmAndCred(buyer, charmWad, bytes32(0))`** so each CRED buy adds **`CRED_PER_BUY` (35e18)** to **`epochCredPool[lastBuyEpoch]`** like DOUB buys, with no referral mint. The unused **`_accrueCharmOnly`** helper is removed. > > Docs, invariants (**`INV-TIME-ARENA-CRED-POOL-PARITY`** replaces **`CRED-NO-POOL`**), play skill, and arena CRED copy now state DOUB and CRED buys both feed the epoch pool. **Forge** adds mixed pro-rata, pool delta, and hard-reset boundary tests; **`verify-cred-buy-anvil.sh`** asserts **`epochCredPool +35`** after a CRED buy and expects level **2** after one starter **`buyWithCred`** (10 CHARM XP). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 52b75ff0d9fac20e1d6858bdaee00647e9bd68c5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
project_80561744_bot_5118c68a88bff95eacb6ab1c47c19cae commented 2026-06-13 08:05:22 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
project_80561744_bot_5118c68a88bff95eacb6ab1c47c19cae commented 2026-06-13 08:05:31 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-13 08:07:30 +00:00 (Migrated from gitlab.com)

Security review — MR !460

Commit reviewed: 52b75ff0d9fac20e1d6858bdaee00647e9bd68c5

Scope: On-chain change in TimeArena._buyCred (swap _accrueCharmOnly → _accrueCharmAndCred with codeHash = bytes32(0)); supporting Forge tests, docs/invariants, frontend CRED copy, and verify-cred-buy-anvil.sh pool assertion.

Outcome: FINDINGS: 0 medium+

Inline threads: None (no medium+ findings to anchor).

Analysis summary

Reviewed attacker-controlled paths for buyWithCred → _accrueCharmAndCred → claimCred:

Area Result
Reentrancy buyWithCred / claimCred are nonReentrant; PlayCred mint/burn has no external callbacks — no new reentrancy surface.
Referral mint bypass bytes32(0) skips the codeHash != 0 referral branch in _accrueCharmAndCred — intentional; CRED path still has no referral mint (matches DOUB referral semantics).
Pool over-mint / double-claim claimCred zeros per-epoch weight before mint; pro-rata mulDiv rounds down; unchanged claim path — no new inflation or double-claim vector.
Economic extraction Each CRED buy burns charmWad × CRED_PER_CHARM_WAD / WAD (≥99 CRED at min CHARM) while adding only CRED_PER_BUY (35) to epochCredPool; solo-epoch max claim is 35 < burn. Mixed-epoch pro-rata tested in test_cred_pro_rata_mixed_doub_and_cred_buyers.
Epoch boundary _prepareBuyBeforeTimer may increment lastBuyEpoch before accrual; boundary test confirms pool credits post-reset epoch — consistent with DOUB path.
CEI / ordering CRED burned before pool accrual and _finishBuy — accrual cannot proceed without successful burn.

This MR closes the pre-existing free-rider gap where CRED buyers accrued epoch CHARM weight without contributing to epochCredPool, diluting DOUB-funded pool claims. The change aligns CRED economics with DOUB without widening auth or external-call surfaces.

Security review: no medium+ findings on this diff.

## Security review — MR !460 **Commit reviewed:** `52b75ff0d9fac20e1d6858bdaee00647e9bd68c5` **Scope:** On-chain change in `TimeArena._buyCred` (swap `_accrueCharmOnly` → `_accrueCharmAndCred` with `codeHash = bytes32(0)`); supporting Forge tests, docs/invariants, frontend CRED copy, and `verify-cred-buy-anvil.sh` pool assertion. **Outcome:** `FINDINGS: 0` medium+ **Inline threads:** None (no medium+ findings to anchor). ### Analysis summary Reviewed attacker-controlled paths for `buyWithCred` → `_accrueCharmAndCred` → `claimCred`: | Area | Result | |------|--------| | **Reentrancy** | `buyWithCred` / `claimCred` are `nonReentrant`; `PlayCred` mint/burn has no external callbacks — no new reentrancy surface. | | **Referral mint bypass** | `bytes32(0)` skips the `codeHash != 0` referral branch in `_accrueCharmAndCred` — intentional; CRED path still has no referral mint (matches DOUB referral semantics). | | **Pool over-mint / double-claim** | `claimCred` zeros per-epoch weight before mint; pro-rata `mulDiv` rounds down; unchanged claim path — no new inflation or double-claim vector. | | **Economic extraction** | Each CRED buy burns `charmWad × CRED_PER_CHARM_WAD / WAD` (≥99 CRED at min CHARM) while adding only `CRED_PER_BUY` (35) to `epochCredPool`; solo-epoch max claim is 35 < burn. Mixed-epoch pro-rata tested in `test_cred_pro_rata_mixed_doub_and_cred_buyers`. | | **Epoch boundary** | `_prepareBuyBeforeTimer` may increment `lastBuyEpoch` before accrual; boundary test confirms pool credits post-reset epoch — consistent with DOUB path. | | **CEI / ordering** | CRED burned before pool accrual and `_finishBuy` — accrual cannot proceed without successful burn. | This MR closes the pre-existing **free-rider** gap where CRED buyers accrued epoch CHARM weight without contributing to `epochCredPool`, diluting DOUB-funded pool claims. The change aligns CRED economics with DOUB without widening auth or external-call surfaces. **Security review: no medium+ findings on this diff.**
PlasticDigits (Migrated from gitlab.com) merged commit 621957a6ec into main 2026-06-13 09:28:49 +00:00
PlasticDigits commented 2026-06-13 09:28:50 +00:00 (Migrated from gitlab.com)

mentioned in commit 621957a6ec

mentioned in commit 621957a6ec058508c265e96ccbe743372fe004af
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!460
No description provided.