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.sol
  • contracts/src/PlayCred.sol
  • contracts/src/interfaces/IPlayCred.sol
  • contracts/test/TimeArena.t.sol
  • docs/product/arena-v2.md
  • docs/testing/invariants-and-business-logic.md
  • indexer/src/decoder.rs (if new events)
  • contracts/script/DeployDev.s.sol, DeployProduction.s.sol (no deploy change unless new immutables)

Why this is needed

  1. 100 CRED / CHARM — Flat 70 CRED mis-prices large vs small CHARM buys inside the 0.99–10 band; burn should scale with charmWad so CRED and DOUB buys align on CHARM intent.
  2. 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 TimeArena only; no peer transfers.
  • CRED buys unchanged side effects — Still no DOUB routing / totalDoubRaised / epochCredPool accrual on CRED path unless explicitly scoped (default: unchanged).
  • First-buy flag — Must be permanent per wallet (buyCount == 0 before increment, or dedicated bool set once); must not reset on lastBuyEpoch hard-reset or rollPodiumEpoch.
  • First-buy eligibility — Clarify in implementation: applies to first DOUB or CRED buy (recommend any _finishBuy); document if router buyFor counts for the buyer address only.
  • Rounding — Use Math.mulDiv(charmWad, 100e18, WAD) (or equivalent) for burn; revert on zero burn.
  • Reentrancy — Keep nonReentrant on external entrypoints; no external calls before state updates in bonus path.

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_BURN constant; add CRED_PER_CHARM_WAD = 100e18 (name TBD).
  • Revert if credBurn == 0 or balanceOf(buyer) < credBurn before burn.

2) First-buy 150 CRED → next epoch pending

  • On first buy (buyCount[buyer] == 0 before _finishBuy increment):
    • Target epoch: lastBuyEpoch + 1 (if hard-reset increments epoch in same tx, use post-reset lastBuyEpoch + 1 — document chosen rule in NatSpec).
    • Credit 150e18 via dedicated storage, e.g. mapping(uint256 => mapping(address => uint256)) epochFixedCredBonus.
  • Extend pendingCred(user, epoch) to return proRata + 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 table INV-TIME-ARENA-CRED-BURN-BUY, add INV-TIME-ARENA-FIRST-BUY-CRED-BONUS.

Acceptance criteria

  • buyWithCred(cw) burns exactly cw * 100e18 / 1e18 (18-decimal CRED).
  • Burn scales across CHARM band (0.99e18 … 10e18); reverts below min/max CHARM as today.
  • Wallet’s first _finishBuy schedules 150e18 CRED for next epoch per spec; second buy from same wallet does not schedule again.
  • Timer hard-reset / new lastBuyEpoch does not reset first-buy consumption flag.
  • pendingCred and claimCred include fixed bonus; claim after epoch < lastBuyEpoch works 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).
  • PlayCred remains non-transferable; no new transfer paths.
  • Forge tests and invariant doc updated; forge test green.

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 TimeArena passes including updated CRED tests.
  • Manual: deploy Dev stack, first buy → pendingCred for lastBuyEpoch+1 shows 150e18 (or post-claim behavior documented).
  • Manual: buyWithCred with known balance decreases by 100 * charm.
  • NatSpec on new public mappings/views.
  • Indexer decodes new event if added (optional follow-up issue if out of scope).

  • 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.