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:

  • feeAmount is added to the contract's accruedDepositFees balance (a single counter that survives across games).
  • The host can withdraw the accrued total at any time via withdrawDepositFees().
  • poolContribution replaces effectiveValue as 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) for d ≥ 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:

  1. d < j (the pool must be from a later cohort)
  2. 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 in G
  • Virality: C=1, Pool[2] has the contribution (cohort 1 redirected), but C=1 so 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.

results matching ""

    No results matching ""