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)
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% ofCRED_PER_BUY, not a fixed amount._accrueCharmAndCred(buyer, charmWad, codeHash)on DOUB buys (buy,buy(charm, codeHash), routerbuyFor):- Accrues
charmWadonly tocharmWeight(no referral CHARM bonus). - If
codeHash != 0andreferralRegistrywired: resolves referrer viaReferralRegistry.ownerOfCode, blocks self-referral, mintseach = CRED_PER_BUY × REFERRAL_CRED_BPS / 10_000to referrer and buyer viaplayCred.mint, emitsReferralCredApplied(buyer, referrer, codeHash, each, each).
- Accrues
buyWithCreduses_accrueCharmOnly— no referral path today (nocodeHashparam).
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
ReferralCredApplied→idx_arena_referral_cred(referrer_cred,buyer_cred). - HTTP (schema ≥ 2.3.0):
GET /v1/referrals/applied,referrer-leaderboard,wallet-cred-summary— sumsreferrer_cred(indexer/src/api.rs).
Frontend (derived)
/referrals: leaderboard + earnings from indexer CRED fields (ReferralProgramEarningsSection.tsx,ReferralLeaderboardSection.tsx)./arenabuy session: still reads onchainREFERRAL_CRED_BPSviareferralEachBpsRand computes preview bonus as % of something (useArenaSaleSession.ts) — misaligned with flat CRED product intent.
Docs / invariants
docs/product/referrals.md·docs/product/time-arena.md#referrals— documents 5% of 35 CRED.INV-REFERRAL-253-CRED(invariants-and-business-logic.md) — encodes BPS formula.- Guardrails item 6f (
.cursor/skills/yieldomega-guardrails/SKILL.md).
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:
- UX/math friction — guides and buyers must understand epoch tranche (
35 CRED) and BPS (500) to know they earn 1.75 CRED, not “5”. - Silent coupling — if
CRED_PER_BUYis retuned for epoch economics, referral payouts move unintentionally. - Frontend drift — arena buy preview still speaks in BPS/CHARM-era patterns;
/referralscopy references “5% of the 35 CRED mint”. - 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_BUYpool 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
charmWeightbeyond purchasedcharmWad. - 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 toepochCredPool). - Scope of qualifying buys — default: same as today (referred DOUB buy with non-zero
codeHash). Document explicitly ifbuyWithCred+ referral is out of scope. - Constant naming — prefer explicit
REFERRAL_CRED_FLAT_WAD = 5e18(or similar); remove or deprecateREFERRAL_CRED_BPSfrom product path (avoid dual formulas). - Event shape — keep
ReferralCredAppliedunless a breaking ABI change is justified; indexer table columns unchanged (amounts differ). - Supersedes — update
INV-REFERRAL-253-CRED, guardrails 6f,time-arena.mddecision #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 |
Recommended direction
-
Contracts
- Add
uint256 public constant REFERRAL_CRED_FLAT_WAD = 5e18;(name TBD). - In
_accrueCharmAndCred, replaceMath.mulDiv(CRED_PER_BUY, REFERRAL_CRED_BPS, 10_000)withREFERRAL_CRED_FLAT_WADfor both mints and event fields. - Remove
REFERRAL_CRED_BPSfrom public API or leave deprecated with NatSpec “unused; flat CRED per #___” — pick one, don’t keep both active. - Confirm
PlayCredminter role unchanged.
- Add
-
Forge tests
- Update existing referral tests to expect
5e18each side. - Add test: changing
CRED_PER_BUYin a fork/mock does not change referral mint amount (documents decoupling).
- Update existing referral tests to expect
-
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
/referralslede/copy (“5 CRED per referred buy per side”).
- Replace BPS-based buy preview with flat “+5 CRED” (read constant from chain or hardcode with ABI read of
-
Indexer
- No schema migration required if event fields unchanged; integration fixtures expecting
1.75e18→5e18. - Optional: bump
x-schema-versionpatch + release note if response semantics documented.
- No schema migration required if event fields unchanged; integration fixtures expecting
-
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)andplayCred.mint(buyer, 5e18);ReferralCredAppliedemits5e18, 5e18. - Referral mint amount independent of
CRED_PER_BUY(35 CRED epoch tranche unchanged). -
charmWeightincreases bycharmWadonly (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. -
/referralsand/arenacopy/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
5e18assertions - Manual or Anvil: one referred buy → wallet
PlayCred.balanceOf+5 for referrer and buyer - Indexer row + HTTP
/v1/referrals/appliedshow 5e18 per side -
/referralsUI 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_BPSin live Arena v2 paths (except legacy TimeCurve sections)
Related issues
- Epic: #240 (closed)
- CRED-not-CHARM baseline: #253 (closed) (closed — this issue modifies payout math only)