Contracts: cap level-up gas — cached level, consume XP, max 5 levels per buy #265
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:open_issues
agents
agent:security_audit
agent:verify
area
timecurve
arena
arena-v2
audit
block:feature_review
block:security
bug
contracts
design
devops
docs
documentation
enhancement
epic
feature
frontend
indexer
low priority
megaeth
operations
ops
performance
QA
qa checklist
referrals
removal
security
smart-contracts
tech-debt
testing
time-arena
timecurve
visual-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
code/yieldomega#265
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Bound TimeArena buy-path gas for XP/level progression by persisting cached level, tracking XP progress toward the next level (consuming threshold XP on each level-up), and applying at most five level-ups per buy. Recompute loops run only when a buy actually advances level(s). Level and XP progress must survive timer hard-resets and
lastBuyEpochrolls (no coupling to podium/timer state).Parent: #250 (XP/level shipped). Epic: #238.
Current codebase
Onchain (authoritative)
TimeArena.xp(mapping(address => uint256) public xp)_finishBuyArenaXp.levelFromXp(xp)via awhile (true)loop subtractingxpToAdvance(level)until remainder < next thresholdArenaXp.xpForCharm(charmWad)[0.99, 10]ArenaXp.xpToAdvance(L)min(20 + (L-1)*5, 100)for L→L+1TimeArena._finishBuyxp[buyer] += xpGain;emit XpGained(buyer, xpGain, ArenaXp.levelFromXp(xp[buyer]))— full recompute every buylevel(user),xpToNextLevel(user)levelFromXp(andxpToNextLevelalso sums thresholds 1..level-1)TimeMath.extendDeadlineOrResetBelowThresholdin_finishBuylastBuyEpoch; does not clearxptodayRelevant implementation:
contracts/src/arena/libraries/ArenaXp.sol—xpForCharm,levelFromXp,xpToAdvance,xpToNextLevelcontracts/src/arena/TimeArena.sol—xpmapping,_finishBuy,level,xpToNextLevel,XpGainedeventcontracts/test/TimeArena.t.sol—test_xp_levels(pure math only)docs/product/arena-v2.md— XP/level product rulesdocs/testing/invariants-and-business-logic.md—INV-TIME-ARENA-XPOffchain (derived)
indexer/src/decoder.rs,persist.rsXpGained→idx_player_xp(xp_gained,new_level)indexer/src/api_arena.rsGET /v1/arena/wallet/statslevelfrom latestnew_level;xpplaceholder"0"frontend/src/lib/arenaXpMath.tsArenaXppure functionsfrontend/src/lib/abis.tsxp,level,xpToNextLevelviewsfrontend/src/components/WalletProfileModal.tsxlevelonlyGas problem
levelFromXpiterations ≈ number of completed level-ups (after level 17, ~one iteration per 100 lifetime XP). A whale with large cumulative XP pays O(level) gas on every buy, even when the buy grants only 1–10 XP and does not change level.xpToNextLevelis worse (double loop). There is no cached level and no “XP within current level” bucket; consumed threshold XP is only implied by the subtractive loop over the lifetime total.Why this change is needed
_finishBuy) is a DoS footgun: many small buys still trigger full recompute; a single max-CHARM buy after huge history still walks every prior level.Constraints and guardrails
Repository guardrails (see
.cursor/skills/yieldomega-guardrails/SKILL.md):TimeArena+ArenaXp(or extracted library); indexer/frontend derive only.lastBuyEpoch/ podium rolls must not reset or zerolevelor in-progress XP; document explicitly inarena-v2.md.TimeArenalayout; document initializer/migration for existing deployments if any live player state exists.XpGained(player, amount, newLevel); if semantics ofamountchange (e.g. only “applied toward level” vs “raw charm XP”), document for indexer consumers.xpForCharmformula unchanged (1–10 band) unless product explicitly revises #250._finishBuy(or internal helper), not only in views.level/xpToNextLevelmust stay O(1) or cheap fixed work (no full history loop on hot paths).Relevant files
Contracts (primary)
contracts/src/arena/TimeArena.solcontracts/src/arena/libraries/ArenaXp.solcontracts/test/TimeArena.t.solcontracts/test/ArenaPrizeRouting.t.sol(regression only if touched)contracts/script/DeployDev.s.sol(if storage init needed)Docs / invariants
docs/product/arena-v2.mddocs/testing/invariants-and-business-logic.md(INV-TIME-ARENA-XP+ new gas-bound invariant)Indexer (if event/view semantics change)
indexer/src/decoder.rsindexer/src/persist.rsindexer/src/api_arena.rs(optional: aggregate realxpfor wallet stats)indexer/migrations/(only if schema changes)indexer/tests/integration_stage2.rsFrontend (mirror + display)
frontend/src/lib/arenaXpMath.tsfrontend/src/lib/arenaXpMath.test.tsfrontend/src/lib/abis.tsfrontend/src/components/WalletProfileModal.tsx(optional)Recommended solution direction
Storage model (suggested)
Replace or reinterpret monolithic lifetime
xpwith two persisted fields per player (names illustrative):level(uint256, default 1) — cached current level.xpTowardNext(uint256) — XP accumulated within the current level towardxpToAdvance(level); threshold XP is subtracted on each level-up (“remove consumed xp”).Optional third field
lifetimeXpfor analytics/API backward compatibility only if existingxp(address)view must remain cumulative; otherwise document breaking change and bump ABI hash export.Buy-path algorithm (
_finishBuy)xpGain = ArenaXp.xpForCharm(charmWad)(unchanged).xpTowardNext += xpGain.levelsGained = 0.levelsGained < 5andxpTowardNext >= xpToAdvance(level):xpTowardNext -= xpToAdvance(level)level += 1levelsGained += 1emit XpGained(buyer, xpGain, level)(confirmamount= gross charm XP vs net applied — pick one, document).levelFromXpon the hot path.Views
level(user)→ storedlevel(O(1)).xpToNextLevel(user)→xpToAdvance(level) - xpTowardNext(or 0 if at cap edge case documented).xp(user)→ define explicitly:xpTowardNextor reconstructed lifetime total; align with indexer.Pure math library
ArenaXp.levelFromXp/ table tests as reference for migration proofs and fuzz oracle (“incremental state matches full recompute”).applyXpTowardLevel(level, xpTowardNext, xpGain, maxLevelUps)pure helper for unit tests.Migration / existing chains
xpalready holds lifetime totals on a deployed net: one-time migration script or guardedinitializeV2that walks players off-chain or admin-only batch — out of scope unless deployer confirms live state; document “fresh DeployDev only” if so.Indexer / frontend
XpGained; optionally persistlevels_gainedif new event field added (only if necessary).arenaXpMath.tswith incremental helpers; property tests vslevelFromXpon random totals.Acceptance criteria
leveland in-level XP progress persisted onchain; threshold XP consumed on each level-up (not re-subtracted via full history loop on every buy).levelFromXpfull loop is not invoked in_finishBuy(gas snapshot or test hook documents this).level()andxpToNextLevel()are O(1) (no loops over player history).xpForCharm/ threshold table unchanged vs #250 unless product amends spec.lastBuyEpochincrement does not modify level or XP progress (Forge test).buy(DOUB) andbuyWithCredpaths use the same XP/level update helper.XpGainedstill emitted on every buy with correctnewLevelafter incremental update.levelFromXpreference for randomized sequences within the 5-level cap per step and across multi-tx sequences.INV-TIME-ARENA-XPupdated; new invariant e.g.INV-TIME-ARENA-XP-GAS(max level-ups per buy, no reset on epoch).docs/product/arena-v2.mdupdated (storage semantics, 5-level cap, persistence across timer resets).Test plan — functional paths
xpGain< remaining to next levellevelunchanged;xpTowardNextincreases; gas stable vs high-level whale baselinexpTowardNext→ 0xpTowardNextxpGaintest hook or repeated accrual in test)levelFromXpafter N buysbuyWithCredcharmWadhardReset == true)lastBuyEpochincrements; level/XP unchangedlevel()/xpToNextLevel()after partial progresspaused/ not live_finishBuyXP section)xpGainsequences, 1–5 level-ups per step enforcedlevelFromXpon simulated lifetime total if reconstructing, or matches pure incremental oracleCommands:
forge test --match-contract TimeArena -vv;forge test --match-path ArenaXp;npm test/pnpm testforarenaXpMath.test.ts;cargo testif indexer touched.Test plan — attack vectors / abuse
_finishBuy(and no rogue external setter) mutates level; grep + negative test callinglevelsetter if anyxpTowardNextartificially inflated in test harnessxpTowardNext+ sum of consumed thresholds equals pre-tx progress +xpGain(conservation)level,xpTowardNext,xpGainbounds — use OpenZeppelinMathwhere mulDiv; fuzz with maxuint256near cap rejected by charm bounds_finishBuyafter external calls or follow checks-effects-interactions if movedlevel()always equals post-tx storage; no separate cached vs computed driftXpGainedlogs; no trust in client-supplied levelforge inspectdocumentedVerification criteria
forge testgreen forTimeArena/ArenaXptargets._finishBuywith player at level ≥ 50 (reference setup) — buy that does not level up: gas does not grow materially vs level-1 wallet; buy that levels up: bounded small loop (≤5 iterations).arenaXpMathunit tests mirror onchain incremental helper.cargo test+integration_stage2.rsXpGainedrow still inserted.cast calllevel/xpToNextLevelconsistent with UI profile modal.INV-TIME-ARENA-XP/ new invariant ininvariants-and-business-logic.md.ArenaPrizeRouting/ DOUB split tests.Out of scope (unless split to follow-up)
GET /v1/arena/wallet/statsxpfrom chain (currently"0").xplifetime → new layout.Definition of done
Shipped when acceptance criteria are met, invariants/docs updated, and gas bounded on the buy path as described, with reviewer sign-off that timer/epoch rolls do not reset player progression.
marked as related to #250
marked as related to #238
mentioned in commit
2eeb304d9fImplementation summary (#265)
Shipped on
main(2eeb304): buy-path XP/level progression now uses cachedlevel+xpTowardNextwithArenaXp.applyXpGain(≤5 level-ups per buy, surplus retained). Lifetimexpmapping unchanged for ABI/indexer;level()/xpToNextLevel()are O(1). Timer hard-reset does not clear progression.Docs:
docs/product/arena-v2.md,INV-TIME-ARENA-XP-GASindocs/testing/invariants-and-business-logic.md, guardrails +skills/README.mdcrosslinks.Verification checklist
cd contracts && forge test --match-contract 'ArenaXpTest|TimeArenaTest'cd frontend && npx vitest run src/lib/arenaXpMath.test.tscast calllevel(alice)/xpTowardNext(alice)/xpToNextLevel(alice)match after several buys (cooldown-aware)lastBuyEpochbumps;level/xpTowardNextmonotonic (seetest_xp_survives_timer_hard_reset)buyWithCredsame XP deltas as DOUB at same CHARM (test_xp_buy_with_cred_same_as_doub)XpGained(amount= gross charm XP,newLevel= cached level after buy)test_xp_high_level_buy_gas_bounded_no_level_up)@brouie — please run the checklist on your side and confirm before we close. Leaving issue open until verified.
mentioned in issue #264
mentioned in commit
9fce7541e6Verification complete (issue/265-verify worktree)
Verified GitLab #265 — cached XP progression on the buy path is implemented and covered by automated + manual checks. Pushed doc crosslinks to
main(9fce754).What was verified
Onchain (already shipped on
main):_cachedLevel+xpTowardNextupdated viaArenaXp.applyXpGainin_finishBuy(nolevelFromXpon hot path)xpTowardNextlevel()/xpToNextLevel()viewsbuyandbuyWithCredDocs added in this verification pass:
manual-qa-checklists.md#manual-qa-issue-265invariants-and-business-logic.md#timearena-xp-gas-gitlab-265skills/play-time-arena-doub/SKILL.mdarena-v2.md#xpCommands run (all green)
forge test --match-test test_xp_+forge test --match-path test/ArenaXp.t.sol— 12 tests passedforge test --match-contract ArenaPrizeRouting— 4 tests passed (no DOUB routing regression)npm test -- --run src/lib/arenaXpMath.test.ts— 6 tests passedDeployDev+ max-CHARM buy →level=1,xpTowardNext=10,xpToNextLevel=10,xp=10Reviewer checklist
forge test --match-contract "ArenaXp|TimeArena" --match-test "test_xp|test_applyXpGain"passes locallynpm test -- --run src/lib/arenaXpMath.test.tspasses infrontend/cast calllevel/xpTowardNext/xpToNextLevelmatch expected for 10 CHARM first buytest_xp_survives_timer_hard_resetandtest_xp_high_level_buy_gas_bounded_no_level_upremain greenXpGained.new_level)All acceptance criteria from the issue body and verification criteria are satisfied. Closing as verified.
mentioned in issue #299
mentioned in issue #304