Referrals: flat 5 CRED per side (replace 5% of epoch CRED tranche)

Summary

Replace the current percentage-based referral Play CRED reward (5% of CRED_PER_BUY = 1.75 CRED per side) with a fixed flat bonus of 5 CRED minted to referrer and buyer on every qualifying referred buy. CRED remains immediately minted (not epoch-pooled). Parent context: Arena v2 referrals (#240 (closed)), CRED-not-CHARM migration (#253 (closed)).


Current codebase

Onchain (authority)

TimeArena.sol:

  • CRED_PER_BUY = 35e18 — epoch pool CRED added on every DOUB/CRED buy path that accrues charm.
  • REFERRAL_CRED_BPS = 500 — referral side uses 5% of CRED_PER_BUY, not a fixed amount.
  • _accrueCharmAndCred(buyer, charmWad, codeHash) on DOUB buys (buy, buy(charm, codeHash), router buyFor):
    • Accrues charmWad only to charmWeight (no referral CHARM bonus).
    • If codeHash != 0 and referralRegistry wired: resolves referrer via ReferralRegistry.ownerOfCode, blocks self-referral, mints each = CRED_PER_BUY × REFERRAL_CRED_BPS / 10_000 to referrer and buyer via playCred.mint, emits ReferralCredApplied(buyer, referrer, codeHash, each, each).
  • buyWithCred uses _accrueCharmOnlyno referral path today (no codeHash param).

ReferralRegistry.sol: unchanged registration model (1 CL8Y burn, codeHash → owner).

Forge coverage: TimeArena.t.sol::test_referred_buy_mints_cred_not_charm, test_self_referral_reverts assert 1.75e18 per side.

Indexer (derived)

  • Decodes ReferralCredAppliedidx_arena_referral_cred (referrer_cred, buyer_cred).
  • HTTP (schema ≥ 2.3.0): GET /v1/referrals/applied, referrer-leaderboard, wallet-cred-summary — sums referrer_cred (indexer/src/api.rs).

Frontend (derived)

Docs / invariants


Why change

Product wants a simple, legible rule: “Referred buy → 5 CRED to guide, 5 CRED to buyer, right away.”

Problems with the current 5%-of-35 design:

  1. UX/math friction — guides and buyers must understand epoch tranche (35 CRED) and BPS (500) to know they earn 1.75 CRED, not “5”.
  2. Silent coupling — if CRED_PER_BUY is retuned for epoch economics, referral payouts move unintentionally.
  3. Frontend drift — arena buy preview still speaks in BPS/CHARM-era patterns; /referrals copy references “5% of the 35 CRED mint”.
  4. Spec debt#240 open decision #3 locked the BPS basis; this issue supersedes that decision with governance/product sign-off.

Non-goals (unless explicitly expanded in implementation):

  • Changing registration burn (still 1 CL8Y).
  • Reintroducing CHARM weight referral bonuses.
  • Changing epoch CRED_PER_BUY pool mechanics (still 35 CRED per buy into epoch pool).

Constraints & guardrails

  • Onchain authority — payout amount enforced only in TimeArena + PlayCred.mint; indexer/frontend are derived (#253 (closed)).
  • AGPL — follow docs/licensing.md.
  • No CHARM inflation — referral must not increase charmWeight beyond purchased charmWad.
  • Self-referral — must still revert (TimeArena: self-referral).
  • Invalid/unregistered code — must still revert (no silent zero-code fallback).
  • Immediate mint — flat 5 CRED per side via playCred.mint, same as today (not added to epochCredPool).
  • Scope of qualifying buys — default: same as today (referred DOUB buy with non-zero codeHash). Document explicitly if buyWithCred + referral is out of scope.
  • Constant naming — prefer explicit REFERRAL_CRED_FLAT_WAD = 5e18 (or similar); remove or deprecate REFERRAL_CRED_BPS from product path (avoid dual formulas).
  • Event shape — keep ReferralCredApplied unless a breaking ABI change is justified; indexer table columns unchanged (amounts differ).
  • Supersedes — update INV-REFERRAL-253-CRED, guardrails 6f, time-arena.md decision #3, manual QA §253.
  • Third-party agents — crosslink skills/README.md / play skills if referral economics are referenced.

Relevant files

Layer Files
Contracts TimeArena.sol, TimeArena.t.sol, DevStackIntegration.t.sol
Indexer decoder.rs, persist.rs, api.rs, integration_stage2.rs
Frontend useArenaSaleSession.ts, arenaV2SaleSessionBridge.test.ts, ReferralProgramEarningsSection.tsx, ReferralLeaderboardSection.tsx, indexerApi.ts
Docs referrals.md, time-arena.md, glossary.md, invariants-and-business-logic.md, manual-qa-checklists.md, guardrails skill

  1. Contracts

    • Add uint256 public constant REFERRAL_CRED_FLAT_WAD = 5e18; (name TBD).
    • In _accrueCharmAndCred, replace Math.mulDiv(CRED_PER_BUY, REFERRAL_CRED_BPS, 10_000) with REFERRAL_CRED_FLAT_WAD for both mints and event fields.
    • Remove REFERRAL_CRED_BPS from public API or leave deprecated with NatSpec “unused; flat CRED per #___” — pick one, don’t keep both active.
    • Confirm PlayCred minter role unchanged.
  2. Forge tests

    • Update existing referral tests to expect 5e18 each side.
    • Add test: changing CRED_PER_BUY in a fork/mock does not change referral mint amount (documents decoupling).
  3. Frontend

    • Replace BPS-based buy preview with flat “+5 CRED” (read constant from chain or hardcode with ABI read of REFERRAL_CRED_FLAT_WAD).
    • Update /referrals lede/copy (“5 CRED per referred buy per side”).
  4. Indexer

    • No schema migration required if event fields unchanged; integration fixtures expecting 1.75e185e18.
    • Optional: bump x-schema-version patch + release note if response semantics documented.
  5. Docs / invariants

    • Supersede #240 decision #3 row with flat 5 CRED per side.
    • Rename or extend INV-REFERRAL-253-CRED → flat-amount invariant.

Acceptance criteria

  • On referred DOUB buy with valid codeHash: playCred.mint(referrer, 5e18) and playCred.mint(buyer, 5e18); ReferralCredApplied emits 5e18, 5e18.
  • Referral mint amount independent of CRED_PER_BUY (35 CRED epoch tranche unchanged).
  • charmWeight increases by charmWad only (no referral CHARM).
  • Self-referral and invalid code behaviors unchanged (revert).
  • No-referral buy (codeHash == 0) mints zero referral CRED.
  • Indexer persists and serves referrer_cred / buyer_cred = 5e18` on qualifying rows.
  • /referrals and /arena copy/preview show flat 5 CRED, not “5%” or “1.75”.
  • Docs + INV-REFERRAL-* + guardrails updated; #253 (closed) QA checklist revised.

Test plan — functional paths

Path Expected
DOUB buy(charm) no code Epoch +35 CRED pool; no referral mint
DOUB buy(charm, validCode) Referrer + buyer each +5 CRED mint; epoch pool still +35
DOUB buy(charm, invalidCode) Revert invalid referral
DOUB buy(charm, ownCode) Revert self-referral
Router buyFor(buyer, charm, code, flag) Referral CRED to buyer + referrer, not router
buyWithCred(charm) (if unchanged scope) No referral CRED (document)
Indexer ingest + replay Idempotent row in idx_arena_referral_cred
GET /v1/referrals/applied referrer_cred / buyer_cred = 5000000000000000000
Leaderboard / wallet summary Totals reflect 5 CRED × buy count

Automated: forge test --match-contract TimeArenaTest (referral tests), cargo test integration_stage2 (if PG URL set), npm test referral/arena session tests, Playwright referrals-surface.spec.ts.


Test plan — attack, hack & abuse vectors

Vector Mitigation to verify
Self-referral / same-wallet guide+buyer Still reverts; no double 5+5 on one wallet
Sybil: many buyer wallets, one guide Each buy pays guide 5 CRED — economic acceptance; verify no extra mint beyond once per tx
Sybil: one buyer, rotating codes Each valid code pays that guide 5 CRED; buyer always gets 5 CRED — product risk, not exploit if intentional
Unregistered / garbage codeHash Revert; no mint
Referral on zero/min buy spam Each qualifying buy mints 10 CRED total (5+5) — confirm bounded by buy cooldown + min charm; assess CRED inflation vs product
CRED mint without PlayCred minter Arena must hold minter role (DeployDev wiring test)
Indexer forged amounts UI must not invent totals; chain event is source
Frontend wrong-network buy Write gating still blocks (INV-FRONTEND-95)
Reorg / duplicate log index ON CONFLICT DO NOTHING idempotency on idx_arena_referral_cred

Document any accepted inflation tradeoff (flat 5 > old 1.75) for treasury/ops.


Verification criteria (issue close)

  • Forge referral tests green with 5e18 assertions
  • Manual or Anvil: one referred buy → wallet PlayCred.balanceOf +5 for referrer and buyer
  • Indexer row + HTTP /v1/referrals/applied show 5e18 per side
  • /referrals UI shows CRED totals consistent with indexed data
  • Docs crosslinked; superseded BPS decision noted in time-arena.md
  • No remaining product references to “5% of 35 CRED” or REFERRAL_CRED_BPS in live Arena v2 paths (except legacy TimeCurve sections)