Docs·Rewards and mechanics
Rewards and mechanics
The Fortune Wheel
Where a spin comes from, who resolves it and what bounds them, the jackpot, and where the THD goes.
Status#
The contract holds only the parts that have to be enforceable. A prize is a list of (bytes32 itemType, uint256 amount) pairs, and every item type the contract does not recognise is credited verbatim and is otherwise inert — so adding a segment that pays a new item is a row in an off-chain catalogue rather than a contract change.
Where a spin comes from#
A spin is not bought. spin() is not payable, pulls no token and charges nothing; it consumes an entitlement the account already holds. There are exactly three ways an entitlement comes into existence:
| Source | Call | Who may call | Rate |
|---|---|---|---|
| A paid creation | mintCreationSpins | An address on the isCreationMinter allowlist | One ready spin and one unlock slot per creation paid for |
| A graduation | mintGraduationSpins | An address on the isGraduationMinter allowlist | One ready spin per 2 ETH raised, computed by the caller. No unlock slot. |
| An arrow in a published root | claimPrize, crediting ITEM_ARROW | Anyone, with a proof | Bounded by MAX_ARROWS_PER_SPIN_BPS, a lifetime ceiling expressed as a share of the spins actually consumed. The value is not published — see above. |
The creation fee itself is the $1 flat creation fee, and the wheel never sees it. It is told how many creations were paid for and by whom; the fee, its currency and the launch stay outside this contract entirely.
How a spin is resolved#
The draw is off chain, and the contract says so in its own source: there is no randomness in the file, by design rather than by omission. No VRF, no blockhash, no prevrandao, no commit-reveal. Provably-fair was considered on 12-09-2026 and deliberately not adopted.
spin(count, clientSeed)consumes the entitlements and emitsSpinsConsumed, carrying the first spin id, the count, whether the spins came from locked arrows, the client seed and the pool at that moment. That event is the anchor the off-chain draw resolves against.- The keeper publishes one result batch per day:
publishResults({ root, thdAllocated, arrowsAllocated, spinsCovered, jackpotWinner }). - Winners prove their own leaf with
claimPrize, which is a permissionless relay —msg.senderappears nowhere in the accounting. The leaf iskeccak256(abi.encode(index, account, thdAmount, itemTypes, itemAmounts)). - Prizes unclaimed after
CLAIM_WINDOW— 7 days, a constant rather than a setter — return to the pool through the permissionlessreclaimExpiredPrizes.
clientSeed is recorded and not used on chain. Carrying it costs one event field and leaves a later commitment scheme available without a redeploy; it is not a commitment to anything today.
Items, arrows and crafting#
Three item types have meaning to the contract. Everything else is a label it carries without interpreting.
| Item | What the contract does with it |
|---|---|
ITEM_WOODEN_SHAFT | Craft input. SHAFTS_PER_ARROW is 10. |
ITEM_STEEL_TIP | Craft input. TIPS_PER_ARROW is 4. |
ITEM_ARROW | A playable spin. Credited straight to readySpinsOf, never to the item ledger — and refused by creditParts, which is what stops the keeper minting playable spins directly. |
Any other bytes32 | Credited verbatim to the item ledger and otherwise inert. |
craft() turns 10 shafts and 4 tips into a locked arrow. A locked arrow needs an unlock slot, and unlock slots are minted only by the paid-creation route — so crafting produces arrows and only creating produces the ability to fire them.
The jackpot#
The jackpot is funded in the currency a creation fee was paid in, by addresses on the isJackpotContributor allowlist, through fundJackpot(currency, amount). A winner takes JACKPOT_WINNER_BPS — 9,000, so 90% — of every registered pot; the 10% left behind is arithmetic rather than policy. The same win also pays THD — the prize the segment pays when the pots are not armed — reserved out of the prize pool at declaration and credited to the winner by claimJackpot. A basket unclaimed after JACKPOT_CLAIM_WINDOW (90 days) returns to the live pots, and its THD to the prize pool, through the permissionless reclaimUnclaimedJackpot.
| Read | Value |
|---|---|
ARMING_THRESHOLD_USD | 2000 * 10**18 — $2,000, at USD_DECIMALS = 18 |
armedValueUsd() · isArmed() | The pots’ combined value and whether it has reached the threshold. Publishable because it is a balance, not a probability. |
MAX_JACKPOT_CURRENCIES | 32 |
The pool, and the two ways THD leaves#
pool() is thd.balanceOf(wheel) − totalObligations — what is available to allocate, after everything already owed. It is funded from two places and never from a player:
| Funder | When |
|---|---|
THDEmissionControl, the creators bucket | During the emission |
SpoilsVault | After the THD/ETH pool is seeded — a 20% baseline of each day’s consumption plus half of each day’s shortfall. See Spoils. |
THD leaves by exactly two routes, both to a fixed storage destination and neither taking an address from the caller: settleWinnings to rewardDistributor — callable only by that distributor, ThdRewardVest, which pulls each winner’s THD through settleFrom and vests it to the end of the 90-day window — and sweepRetiredPool to the registry-gated spoilsDestination after a deliberate retire(), which stops spins at once, and a 30-day RETIREMENT_GRACE in which the final results publish. There is no THD sweep and no surplus function: sweepStrandedToken refuses THD outright. There is no receive() and no fallback(), so a plain ETH transfer to the wheel reverts.