Prize Math Specification
Given N tickets sold, this page fully specifies the reward state: who wins, how much, and where every wei goes.
Notation
| Symbol | Meaning | Contract field |
|---|---|---|
N |
Total tickets sold | entrantCount[gameId] |
e |
Price per ticket (wei) | config.entryAmount |
B_g |
Grand prize log base | config.grandPrizeLogBase |
B_v |
Virality bonus log base | config.viralityBonusLogBase |
p_g |
Grand prize proportion weight | config.grandPrizeProportion |
p_v |
Virality bonus proportion weight | config.viralityBonusProportion |
f_bps |
Host deposit fee (basis points) | depositFeeBps |
F |
Accrued host fees (wei) | accruedDepositFees |
All division below is integer floor division, exactly as the contract computes it on-chain (which is where the dust amounts come from).
0. Deposit fee — host revenue, skimmed first, locked at game start
The contract applies an optional host deposit fee that is skimmed from
the ETH players spend on entries. Critically, the fee is part of the
entry price, not an extra charge on top of it: a player sending exactly
n * e wei always receives exactly n entries regardless of the fee
rate.
Order of operations in receive():
num_entries = msg.value / e
refundAmount = msg.value % e
effectiveValue = msg.value - refundAmount ← dust refunded, unchanged by fee
require(num_entries > 0) ← or revert NoValidEntries
feeAmount = effectiveValue * f_bps / 10000 ← fee skimmed from entry revenue
poolContribution = effectiveValue - feeAmount ← what reaches the pools
f_bps is part of gameConfig and is locked when a game starts. The host
configures it on the next game via the constructor or
setNextGameConfig(...), and once a game starts the value is copied into
gameConfigRecord[gameId] and cannot be changed for the duration of
that game. Calling setNextGameConfig mid-game only modifies the
parameters of game gameId + 1.
The fee is bounded: 0 ≤ f_bps ≤ MAX_DEPOSIT_FEE_BPS where
MAX_DEPOSIT_FEE_BPS = 1000 (10% cap). The cap is enforced in both the
constructor and setNextGameConfig.
When f_bps = 0, the contract behaves exactly as if the fee feature did
not exist: no event is emitted, no state is mutated for the fee, and
poolContribution == effectiveValue.
When f_bps > 0:
feeAmountis added to the contract'saccruedDepositFeesbalance (a single counter that survives across games).- The host can withdraw the accrued total at any time via
withdrawDepositFees(). poolContributionreplaceseffectiveValueas the amount fed into the grand-prize and adoption-bonus pool splits below.
Total accounting for a single deposit:
msg.value = effectiveValue + refundAmount
= (feeAmount + poolContribution) + refundAmount
= feeAmount + (grandPrizeContribution + viralityBonusContribution) + refundAmount
Every wei is accounted for: fee revenue, pool deposits, or dust refund.
1. Fund splitting
Each ticket nominally costs e wei, but the deposit fee may skim a
portion of that ticket's revenue before it reaches the pools. Define:
per-ticket fee contribution: f = e * f_bps / 10000 (approximate, ignoring rounding across multiple entries in one deposit)
per-ticket pool contribution: e' = e - f
In the actual contract, the fee is computed once per deposit on
effectiveValue (not per entry), so for a deposit of k entries:
feeAmount = k * e * f_bps / 10000 and poolContribution = k * e - feeAmount.
The pools then split poolContribution:
Virality contribution for this deposit: v_dep = poolContribution * p_v / (p_v + p_g)
Grand prize contribution for this deposit: g_dep = poolContribution - v_dep
After N tickets sold:
Grand Prize Pool: G ≈ N * e' * p_g / (p_g + p_v) (accumulated via prizePool[gameId])
Total Virality Pool: V ≈ N * e' * p_v / (p_g + p_v) (distributed across cohort sub-pools)
Host Fee Revenue: F ≈ N * e * f_bps / 10000 (accumulated via accruedDepositFees)
(The ≈ signs hide a few wei of integer-division rounding dust.)
Within a single deposit, the grand-prize contribution is computed as
poolContribution - viralityBonusContribution (not independently rounded),
so g_dep + v_dep = poolContribution exactly. The per-deposit rounding
loss relative to the theoretical values above is at most 1 wei per deposit.
2. Grand prize system
The winning ticket positions are branded Power Slots in the user-facing docs (see Power Slots). This section uses the formal term "winner" for the math; they refer to the same tickets.
2a. How many winners (Power Slots)?
W = floor(log_{B_g}(N)) + 1
| N | B_g=2 | B_g=3 | B_g=5 | B_g=7 | B_g=10 |
|---|---|---|---|---|---|
| 10 | 4 | 3 | 2 | 2 | 2 |
| 100 | 7 | 5 | 3 | 3 | 3 |
| 1,000 | 10 | 7 | 5 | 4 | 4 |
| 10,000 | 14 | 9 | 6 | 5 | 5 |
| 100,000 | 17 | 11 | 8 | 6 | 6 |
2b. Which ticket IDs win?
Ticket k wins if its position from the end is a power of B_g
(including B_g^0 = 1):
position_from_end(k) = 1 + N - k
ticket k wins ⇔ position_from_end(k) = B_g^j for some integer j ≥ 0
Equivalently:
k_j = N + 1 - B_g^j for j = 0, 1, 2, …, W-1
where W - 1 = floor(log_{B_g}(N)).
2c. Prize per winner
P = floor(G / W)
Dust = G - W * P (locked in contract)
3. Virality bonus system
3a. Cohort assignment
Each entrant ID k (1-indexed) belongs to a cohort:
cohort(k) = floor(log_{B_v}(k)) + 1
Cohort d |
Entrant ID range | Size of cohort |
|---|---|---|
| 1 | 1 to B_v - 1 |
B_v - 1 |
| 2 | B_v to B_v² - 1 |
B_v² - B_v |
d (general) |
B_v^(d-1) to B_v^d - 1 |
B_v^d - B_v^(d-1) |
This is cohort membership, which governs claim eligibility. Pool
funding is indexed by max(2, floor(log_{B_v}(k)) + 1) (see 3c), so
Pool[1] is always empty and Pool[2] is funded by every entry with ID
1 to B_v² - 1.
3b. How many cohorts exist?
C = floor(log_{B_v}(N)) + 1
This is the cohort of the last entrant.
3c. Contribution routing
Each ticket with ID k contributes v wei to a cohort pool:
c = floor(log_{B_v}(k)) + 1
if c == 1: contribution → Pool[2] ← cohort 1 funds are REDIRECTED
if c >= 2: contribution → Pool[c]
Why redirect? Cohort 1 has no earlier cohort to reward. Sending its contributions to Pool[2] ensures they fund cohort 1 itself once cohort 3 arrives and unlocks Pool[2].
3d. Pool balances
Let v = virality contribution per ticket. For a game with N tickets
where C = floor(log_{B_v}(N)) + 1 cohorts exist:
Pool[2] (receives from cohort 1 + cohort 2 entries):
Pool[2] = min(N, B_v² - 1) * v
Pool[j] for j ≥ 3 (receives from cohort j entries only):
Pool[j] = (min(N, B_v^j - 1) - B_v^(j-1) + 1) * v if N >= B_v^(j-1)
= 0 if N < B_v^(j-1)
The last cohort C may be partially filled:
Entries in last cohort = N - B_v^(C-1) + 1
Max possible entries = B_v^C - B_v^(C-1)
3e. Prize per team from each pool
When pool j is claimable (see 3g), its balance is divided equally
among j - 1 earlier cohorts ("teams"):
team_prize(j) = floor(Pool[j] / (j - 1))
3f. Prize per member
Each team's prize is divided equally among its members:
individual_prize(d, j) = floor(team_prize(j) / |cohort d|)
Where |cohort d| is the cohort size:
|cohort 1| = B_v - 1|cohort d| = B_v^d - B_v^(d-1)ford ≥ 2
Key insight: Earlier cohorts earn from every later pool. Cohort 1 gets a slice of Pool[2], Pool[3], Pool[4], etc.
3g. Claimability rules
A cohort d member can claim from Pool[j] only when:
d < j(the pool must be from a later cohort)j < C(the pool's cohort must have been surpassed — a later cohort must exist)
| Pool | Claimable? | By whom? |
|---|---|---|
| Pool[2] | Yes, if C ≥ 3 |
Cohort 1 |
| Pool[3] | Yes, if C ≥ 4 |
Cohorts 1, 2 |
| Pool[j] | Yes, if C ≥ j+1 |
Cohorts 1 through j-1 |
| Pool[C] (last) | NEVER claimable by participants | Host only (3h) |
3h. Last cohort's pool
Pool[C] — the pool funded by the last/current cohort — is never unlocked
for participant claims. After the game ends, the host calls
clearLeftoverAdoptionBonus(gameId) to sweep Pool[C] as operator revenue.
The larger the game grows beyond a cohort boundary, the more this leftover accumulates.
4. Complete state summary for N tickets
Host deposit fees
F = Σ (effectiveValue_i * f_bps_at_game_start_i / 10000) ← accruedDepositFees
over every deposit i that succeeded
Grand prize
effectiveValue_i = num_entries_i * e
feeAmount_i = effectiveValue_i * f_bps / 10000
poolContrib_i = effectiveValue_i - feeAmount_i
v_dep_i = poolContrib_i * p_v / (p_v + p_g)
g_dep_i = poolContrib_i - v_dep_i
G = Σ g_dep_i ← grand prize pool
W = floor(log_{B_g}(N)) + 1 ← number of winners
P = floor(G / W) ← prize per winner
Winners = {N + 1 - B_g^j : j = 0..W-1} ← winning ticket IDs
Dust = G - W * P ← locked remainder
Virality bonus
C = floor(log_{B_v}(N)) + 1 ← number of cohorts
For each pool j = 2, 3, …, C:
Pool[j] = (entries that routed to j) * v_per_ticket
team_prize(j) = floor(Pool[j] / (j - 1)) ← per-team share
claimable = (j < C) ← surpassed?
For each cohort d = 1, 2, …, C:
size(d) = B_v^d - B_v^(d-1) (or B_v - 1 for d=1)
claims from = pools j where d < j < C
per_member(d) = Σ floor(team_prize(j) / size(d)) for each eligible j
Leftover = Pool[C] ← host-claimable after game ends
5. Edge cases
Only 1 ticket sold (N = 1)
- Grand prize:
W=1, the single entrant wins everything inG - Virality:
C=1, Pool[2] has the contribution (cohort 1 redirected), butC=1so no pool is claimable. Host gets Pool[2] as leftover.
N < B_v (all entrants in cohort 1)
All virality contributions go to Pool[2]. C=1. Pool[2] is the last
cohort pool, so nothing is claimable. Host gets all virality as leftover.
N exactly at a power boundary (N = B_v^d)
The N-th entrant starts a new cohort (cohort d+1). This means Pool[d]
just became claimable (it's now surpassed).
Very large N
- Winner count grows as
O(log N)— slow growth means large individual prizes - Cohort sizes grow geometrically (
B_v^d), so per-member payouts shrink for later cohorts - Last-cohort leftover grows linearly with how many entries fall in the final partial cohort
For two side-by-side worked examples (one without a fee, one with a 1% fee), see Worked Examples.