TimeArena: CRED buy at 100 CRED/CHARM + first-buy 150 CRED bonus (next epoch)
Summary
Update TimeArena CRED-buy economics and add a one-time first-buy CRED bonus. Today buyWithCred burns a flat 70 CRED per buy regardless of CHARM size; product requires 100 CRED per 1e18 CHARM (WAD). Separately, each wallet’s first arena buy ever (not reset by timer hard-resets or epoch rolls) should credit 150 CRED toward claimable balance for the next Last Buy epoch.
Parent context: Arena v2 epic #238 (closed), Play CRED #248 (closed).
Current codebase
| Area | Behavior |
|---|---|
TimeArena.buyWithCred |
Burns fixed CRED_BUY_BURN = 70e18; accrues CHARM/XP/timers via _accrueCharmOnly; no DOUB prize routing, no epochCredPool accrual, no referral CRED mint. |
TimeArena.buy (DOUB) |
Pulls DOUB, routes 40/30/30, adds 35 CRED to epochCredPool[lastBuyEpoch], referral mints 5%+5% of that tranche. |
PlayCred |
Non-transferable ERC20; only TimeArena (MINTER) mints/burns. |
pendingCred(user, epoch) |
Pro-rata only: epochCredPool[epoch] * epochCharmWad / epochCharmTotal (zero if no CHARM weight). |
claimCred(epoch) |
Mints pro-rata share; zeros epochCharmWad[epoch][user]. |
buyCount[user] |
Increments every buy; used for Last Buy podium; not reset on timer hard-reset. |
| Tests / docs | INV-TIME-ARENA-CRED-BURN-BUY expects 70 CRED; docs/product/arena-v2.md documents 70 CRED burn. |
| Indexer | Decodes Buy with paidWithCred; no dedicated first-buy bonus event today. |
Relevant files
contracts/src/arena/TimeArena.solcontracts/src/PlayCred.solcontracts/src/interfaces/IPlayCred.solcontracts/test/TimeArena.t.soldocs/product/arena-v2.mddocs/testing/invariants-and-business-logic.mdindexer/src/decoder.rs(if new events)contracts/script/DeployDev.s.sol,DeployProduction.s.sol(no deploy change unless new immutables)
Why this is needed
- 100 CRED / CHARM — Flat 70 CRED mis-prices large vs small CHARM buys inside the 0.99–10 band; burn should scale with
charmWadso CRED and DOUB buys align on CHARM intent. - First-buy 150 CRED (next epoch) — Onboarding incentive: reward first participation without diluting the current epoch pro-rata pool; “next epoch” keeps accounting clean when the first buy happens mid-epoch.
Constraints and guardrails
- Onchain authority — All economics enforced in
TimeArena; indexer/frontend derived only (guardrails). - AGPL-3.0 — Contract changes under repo license.
- UUPS upgrade — Prefer storage-append + initializer-safe changes; document migration if upgrading live proxy.
- PlayCred non-transferable — Bonus and claims must use mint/burn via
TimeArenaonly; no peer transfers. - CRED buys unchanged side effects — Still no DOUB routing /
totalDoubRaised/epochCredPoolaccrual on CRED path unless explicitly scoped (default: unchanged). - First-buy flag — Must be permanent per wallet (
buyCount == 0before increment, or dedicatedboolset once); must not reset onlastBuyEpochhard-reset orrollPodiumEpoch. - First-buy eligibility — Clarify in implementation: applies to first DOUB or CRED buy (recommend any
_finishBuy); document if routerbuyForcounts for the buyer address only. - Rounding — Use
Math.mulDiv(charmWad, 100e18, WAD)(or equivalent) for burn; revert on zero burn. - Reentrancy — Keep
nonReentranton external entrypoints; no external calls before state updates in bonus path.
Recommended direction
1) Variable CRED burn (100 per CHARM)
- Replace constant-only burn with e.g.
credBurn = Math.mulDiv(charmWad, 100e18, WAD)in_buyCred. - Remove or repurpose
CRED_BUY_BURNconstant; addCRED_PER_CHARM_WAD = 100e18(name TBD). - Revert if
credBurn == 0orbalanceOf(buyer) < credBurnbefore burn.
2) First-buy 150 CRED → next epoch pending
- On first buy (
buyCount[buyer] == 0before_finishBuyincrement):- Target epoch:
lastBuyEpoch + 1(if hard-reset increments epoch in same tx, use post-resetlastBuyEpoch + 1— document chosen rule in NatSpec). - Credit 150e18 via dedicated storage, e.g.
mapping(uint256 => mapping(address => uint256)) epochFixedCredBonus.
- Target epoch:
- Extend
pendingCred(user, epoch)to returnproRata + epochFixedCredBonus[epoch][user](when user has bonus and/or weight). - Extend
claimCred(epoch)to mint pro-rata + bonus, clear bonus and epoch CHARM weight as today. - Emit event e.g.
FirstBuyCredScheduled(address indexed buyer, uint256 indexed targetEpoch, uint256 amount)for indexer/audit.
3) Docs and invariants
- Update
arena-v2.md, invariant tableINV-TIME-ARENA-CRED-BURN-BUY, addINV-TIME-ARENA-FIRST-BUY-CRED-BONUS.
Acceptance criteria
-
buyWithCred(cw)burns exactlycw * 100e18 / 1e18(18-decimal CRED). - Burn scales across CHARM band (0.99e18 … 10e18); reverts below min/max CHARM as today.
- Wallet’s first
_finishBuyschedules 150e18 CRED for next epoch per spec; second buy from same wallet does not schedule again. - Timer hard-reset / new
lastBuyEpochdoes not reset first-buy consumption flag. -
pendingCredandclaimCredinclude fixed bonus; claim afterepoch < lastBuyEpochworks with zero pro-rata but non-zero bonus. - DOUB buy path unchanged except first-buy bonus hook (35 CRED pool accrual still per DOUB buy).
-
PlayCredremains non-transferable; no new transfer paths. - Forge tests and invariant doc updated;
forge testgreen.
Test plan — functional paths
| # | Scenario | Expected |
|---|---|---|
| 1 | buyWithCred(1e18) |
Burns 100e18 CRED |
| 2 | buyWithCred(10e18) |
Burns 1000e18 CRED |
| 3 | buyWithCred(99e16) |
Burns 99e16 CRED (or documented rounding) |
| 4 | Insufficient CRED balance | Revert before state change |
| 5 | First wallet buy (DOUB) | Bonus scheduled; buyCount becomes 1 |
| 6 | First wallet buy (CRED) | Same bonus once |
| 7 | Second buy same wallet | No second bonus |
| 8 | Claim epoch with only bonus (no CHARM) | Mints 150e18 |
| 9 | Claim epoch with pro-rata + bonus | Mints sum |
| 10 | Claim clears bonus + charm weight | pendingCred → 0 |
| 11 | Epoch not ended | claimCred reverts |
| 12 | paidWithCred on Buy event |
Still true for CRED path |
Test plan — attack vectors / abuse
| Vector | Test / mitigation |
|---|---|
| CHARM rounding dust | Min CHARM buy: burn > 0; no free buys |
| Reentrancy via PlayCred | PlayCred has no hooks; keep nonReentrant on arena |
| Double first-buy bonus | Two buys same block from fresh wallet: only one bonus (use flag before increment) |
Contract wallet / buyFor |
Bonus attributed to buyer, not router; test buyFor does not grant router |
| Epoch boundary gaming | Buy triggering hard-reset: bonus targets documented epoch; fuzz/advance block |
| Claim without participation | Bonus claimable without CHARM in target epoch if spec requires — test isolation from pro-rata |
| Sybil farming | Product accepts new-wallet bonus; document no onchain Sybil resistance (ops/monitoring) |
| Overflow | mulDiv on max CHARM (10e18) * 100e18 within uint256 |
| Paused arena | Bonus not scheduled when paused / not live |
| Upgrade storage clash | If UUPS: append-only slots; test initializer on fork |
Verification criteria
-
forge test --match-contract TimeArenapasses including updated CRED tests. - Manual: deploy Dev stack, first buy →
pendingCredforlastBuyEpoch+1shows 150e18 (or post-claim behavior documented). - Manual:
buyWithCredwith known balance decreases by100 * charm. - NatSpec on new public mappings/views.
- Indexer decodes new event if added (optional follow-up issue if out of scope).
Out of scope (link separately)
- Frontend CRED pay selector — separate issue (depends on this for burn formula).
- Changing 35 CRED DOUB accrual or referral 5%+5% split.
Depends / blocks
- Blocks frontend issue for accurate onchain burn checks.