Rewards
Seasons
The quarterly season pot: what funds it, how standing is measured, and how much of it each season releases.
Status#
SeasonRewardsPool holds a THD pot, an ETH pot and one pot per listed ERC-20 currency, all paid against one quarterly leaderboard through cumulative Merkle roots.
A season is a calendar quarter#
A season is a real calendar quarter — 1 January, 1 April, 1 July, 1 October, 00:00:00 UTC. There is no duration constant in seconds; the index is computed on chain as year × 4 + (quarter − 1) by the same civil-date routine ProtocolFeeSplitter uses, so the two calendars cannot disagree. seasonIndexNow(), seasonStart(i) and seasonEnd(i) read it.
What funds the pot#
| Pot | Source | Share | Arrives as |
|---|---|---|---|
| THD | SpoilsSplitter — the seasons share of the Spoils user pool | seasonsBps 2,000 at deployment on production; a change takes effect from the next season | An ERC-20 transfer |
| ETH | ProtocolFeeSplitter’s opex gate — a share of the protocol treasury’s leg above a monthly threshold | seasonShareBps 2,500 of revenue above opexThresholdWei 25 ETH a month on production; capped on chain at MAX_SEASON_SHARE_BPS 5,000 | ETH through receive() |
| ETH or a currency | Creator milestone sales — ItemMall’s MILESTONE_SEASON_BPS | 5,000 bps of every sale | ETH, or receiveToken(token, amount) |
receiveToken accepts only an owner-allowed contributor, only a currency listed on CurrencyRegistry, never THD and never a launch token of this platform, and credits the measured balance change rather than the requested amount. Milestone sales: The shop.
Standing#
season score = Σ max(0, 101 − that day's rank) over the quarter's daily standings (top 100)
qualifiers = accounts that scored on at least 10 days of the quarter
leaf = keccak256(abi.encode(seasonIndex, account, thdCumulative, ethCumulative))
currency leaf = tokenLeafHash(seasonIndex, token, account, cumulative)Scoring is off chain, from the indexed daily standings; the contract sees only the root, the allocated totals and the qualifier count. Points rank you; they are never paid out. See The battlepass.
How much a season releases#
// SeasonRewardsPool.releaseFrom
scaled = min(qualifiers, QUALIFIER_TARGET); // 100
released = pool * RELEASE_SHARE_BPS * scaled // 5000
/ (BPS_DENOMINATOR * QUALIFIER_TARGET);
// i.e. pool × 50% × min(1, qualifiers / 100)| Rule | Value |
|---|---|
RELEASE_SHARE_BPS | 5000 — at most half the pot per season. Constant, no setter |
QUALIFIER_TARGET | 100 — fewer qualifiers release proportionally less. Constant, no setter |
| Pool | Snapshotted per pot at the season’s first publication |
| Same count, every pot | THD, ETH and each currency release the same fraction |
| Zero qualifiers | Refused — NoQualifiers |
| What is not released | Stays in the pot and rolls into later seasons |
Claiming, vest and deadline#
claim(seasonIndex, account, thdCumulative, ethCumulative, proof) settles THD and ETH at once; claimToken settles one currency. Both are permissionless and always pay account, never the caller.
| Pot | Vest | Claim window | Unclaimed after it | Claim gate |
|---|---|---|---|---|
| Season THD | Linear, 90 days from the season’s first publication | 360 days from it | Burned — burnExpiredThd(seasonIndex), anyone may call, supply re-read | Required |
| Season ETH and currencies | None | None | Stays claimable and reserved | Not checked |
| Unallocated THD | — | — | Never burned — the next season’s pot | — |
An account that cannot take ETH still receives its THD; the ETH half stays reserved for it. Claim gate and vesting: THD claims.
Who publishes, and what they cannot do#
Season roots are published by the reward root keeper through RewardRootRelay and the pool’s PublisherDesk, with a 1-day delay in which the owner or guardian Safe may veto. Only an ended season may be published.
| Authority | Reach |
|---|---|
| Can | Publish a season root, choose which contracts may pay currency in, repoint the treasury within TreasuryRegistry, and withdraw unreserved ETH and currencies to it |
| Cannot | Withdraw THD at all (WithdrawTokenIsThd), withdraw a reserved entitlement (bounded by availableEth() and availableToken()), pay an account the root does not name, publish a season that has not ended, re-publish a season after its THD claim window (SeasonThdClaimWindowClosed), or renounce ownership (RenounceDisabled) |
| Re-publication | Allowed after claims, and a season’s allocated totals only rise — but a root can redistribute what nobody has claimed yet. That is a stated trust surface |