Platform › Rewards › Seasons

Rewards

Seasons

The quarterly season pot: what funds it, how standing is measured, and how much of it each season releases.

Version
1.0
Updated
2026-10-14
Source
THD contract sources · THD contracts — design reference, parts 1 and 2 · THD — Full Plan v3.45

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#

Every source of the season pot
PotSourceShareArrives as
THDSpoilsSplitter — the seasons share of the Spoils user poolseasonsBps 2,000 at deployment on production; a change takes effect from the next seasonAn ERC-20 transfer
ETHProtocolFeeSplitter’s opex gate — a share of the protocol treasury’s leg above a monthly thresholdseasonShareBps 2,500 of revenue above opexThresholdWei 25 ETH a month on production; capped on chain at MAX_SEASON_SHARE_BPS 5,000ETH through receive()
ETH or a currencyCreator milestone sales — ItemMall’s MILESTONE_SEASON_BPS5,000 bps of every saleETH, 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#

code
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#

code
// 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)
Release rules
RuleValue
RELEASE_SHARE_BPS5000 — at most half the pot per season. Constant, no setter
QUALIFIER_TARGET100 — fewer qualifiers release proportionally less. Constant, no setter
PoolSnapshotted per pot at the season’s first publication
Same count, every potTHD, ETH and each currency release the same fraction
Zero qualifiersRefused — NoQualifiers
What is not releasedStays 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.

Vest, deadline and gate
PotVestClaim windowUnclaimed after itClaim gate
Season THDLinear, 90 days from the season’s first publication360 days from itBurned — burnExpiredThd(seasonIndex), anyone may call, supply re-readRequired
Season ETH and currenciesNoneNoneStays claimable and reservedNot 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.

What the pool's owner can and cannot do
AuthorityReach
CanPublish a season root, choose which contracts may pay currency in, repoint the treasury within TreasuryRegistry, and withdraw unreserved ETH and currencies to it
CannotWithdraw 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-publicationAllowed 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