Technical White Paper

Despot: A Parimutuel Trading Protocol
with Program-Derived Positions on Solana

Jotaro Yakazu

Abstract

Despot is a parimutuel trading protocol implemented as a single Solana program. It operates a fixed set of independently priced assets. Each asset is issued against a deterministic integer bonding curve that mints units on purchase and burns them on sale, and the lamports paid in are held in a reserve account owned by the program. That reserve backs the asset's weight in contests. On a fixed slot schedule the program halts all trading, derives a 256-bit random value from a pre-committed seed chain combined with a Solana slot hash, and resolves a set of contests, each between one defending asset and one or more challenging assets selected by their holders' weighted allocation votes. The losing asset's units held in positions are transferred in full to the holders of the winning assets, so payouts on the winning side are funded, in units, by holders on the losing side, with no operator take on the transfer. The cycle repeats every 4 423 slots at the launch settings, about 16 minutes at the current slot time; both figures may differ at the time of reading (see Section 6).

The central design decision is that an asset unit is not an SPL token. It is a balance stored in a program-derived account, one per (owner, asset) pair, writable only by the program. This gives the unit the properties a market participant expects of a fungible token, continuous two-sided liquidity at a published price, while the program retains control over when the unit may trade, what fraction of a balance may be sold, and to whom the balance moves when a contest is lost. We are not aware of a prior design that uses program-derived accounts to give a freely traded, curve-priced asset an enforced trading calendar and an enforced partial lock.

This paper specifies the account model, the pricing curve and its exact integer arithmetic, the fee schedule, the round clock, the commitment lock, contest weight and target selection, the randomness pipeline, contest resolution with constant-time supply transfer and accumulator accounting, the uniform-price settlement auction, the invariants the program maintains, its liveness fallbacks, delegated signing and custody, and independent verification. All value arithmetic is unsigned integer arithmetic; there is no floating point anywhere a lamport is computed.

The deployment specified here is a beta, built to measure demand for the category before the design is generalised. Its constraints are deliberate, and the designs intended to replace them are set out at the end (see Section 16).

1Terminology

The product presents the protocol through a geopolitical theme. This paper uses general terms throughout; the table maps them to what a player sees. Program identifiers keep their original names where they are quoted as code.

Table 1: Terms used in this paper, with the product's presentation and the program identifier.
Term in this paperProduct presentationProgram identifier
assetcountryCountry
unit, tokensharePosition.shares
contestbattleresolve_battle
resolution windowwarwar stage
contest weightarmyarmy
captureconquestconquer
captured supply, capture poolloot, loot poolSlot, pool
settlement auctionloot salequeue_sell, close_window
commitment lockhalf-locklocked
allocation votevote for a targetset_vote
sealed buffersealed stagelockdown stage
reservereserveReserve

2Account Model

The protocol is one Anchor program. All state lives in accounts whose addresses are derived from fixed seeds and the program id (program-derived addresses, PDAs). A PDA has no corresponding private key: no external party can sign for it, so its contents change only through the program's own instructions.

Table 2: Program-derived accounts. Seeds are hashed with the program id, so no account has a private key.
AccountSeedsCountContents
Game["game"]1round schedule, fee configuration, seed-chain head, void counter
Round["round"]1captured slot hash, seed, Θ\Theta , contests, outcomes
History["history"]1receipts of the last 64 rounds
Fee vault["vault"], ["vault", i]1+n1 + ntrading fees only, never mixed with a reserve
Asset["country", idx]per asset, at most 16minted supply SS, allocation tally, capture pools, generation
Reserve["reserve", idx]per assetthe asset's lamports; balance equals R(S)R(S)
Holder["player", owner]per walletwithdrawable lamport balance, delegated key and its limits
Position["position", owner, idx]per wallet and assetshares=free+locked\mathrm{shares} = \mathrm{free} + \mathrm{locked}, vote, captured supply

The model has four global accounts (game, round, history and the first fee vault) plus up to 63 further fee vaults, two accounts per asset, one account per holder, and one position account per holder per asset held. The position account is the token: it records how many units of one asset a wallet holds and in what state. The reserve account holds the lamports backing those units, and the fee vaults hold trading fees, so asset collateral and protocol revenue never share an account.

The asset set is configuration within a compile-time ceiling of NMAX=16N_{\mathrm{MAX}} = 16. Every fixed-size array (allocation tallies, capture-pool slots) is sized to that ceiling, and every loop is bounded by the active set.

3Units as Program-Derived Positions

An asset unit presents to the holder as a fungible token and to the program as a ledger entry under its exclusive write authority. This section sets out why the unit is not an SPL token, how the position account reproduces token semantics, and what the program gains by owning the account.

3.1Limits of an SPL mint

An SPL token resides in a token account controlled by its holder through the Token program. The holder can transfer it, or route it through any venue, at any slot, independently of the issuing program. The protocol requires four properties an SPL token cannot provide:

  1. An enforced trading calendar. Contest outcomes are public on chain as soon as they resolve. Any venue open during the resolution window would let the fastest chain reader trade on known outcomes. Every market must therefore close and reopen at fixed slots, on every venue simultaneously. For an SPL token the only available control is a freeze authority, which acts on all holders at once, cannot be scoped to a phase, and constitutes a centralised kill switch.
  2. A partial lock. A fixed fraction of each purchase must be unsellable until the end of the round in which it was bought. A token account carries a single balance and cannot withhold part of itself.
  3. No external liquidity. If units could leave the program, an external pool could price and trade them during the resolution window, reopening the information leak the calendar closes.
  4. Constant-time supply transfer. When an asset loses a contest, every unit of it held in positions moves to the winners within the same instruction. Moving SPL balances would require writing every holder's token account, an unbounded loop that no transaction can execute.

3.2The position account

Each (owner, asset) pair has one account at the address derived from ["position", owner, idx]. Its principal fields:

Table 3: Fields of the position account.
FieldWidthMeaning
sharesu64units held; invariant shares=free+locked\mathrm{shares} = \mathrm{free} + \mathrm{locked}
freeu64units currently sellable
lockedu64units under the commitment lock
locked_epochu32round of the purchase that filled locked
generationu32the asset generation this balance belongs to
vote_target, vote_weightu8, u64allocation vote and its weight
z[16]z[16], pts[16]\mathrm{pts}[16], debt[16]\mathrm{debt}[16]u128 eachrecorded captured supply and accumulator bookmarks
payout_owedu64settled auction proceeds, in lamports

Circulating supply SS is stored once, on the asset account, and all units of an asset are interchangeable. A unit is therefore fungible, publicly observable, priced by a published function of SS, and redeemable against the reserve whenever trading is open. These are the economic properties of a token.

3.3What the program controls

Timing. The phase of a round is a pure function of the slot, fixed when the round opens:

h=ti+τtrade,h = t_i + \tau_{trade},L=h+τsealed,L = h + \tau_{sealed},W=L+τres,W = L + \tau_{res},E=W+τauc=ti+τroundE = W + \tau_{auc} = t_i + \tau_{round}
(1)

where tit_{i} is the opening slot of round ii. buy and sell succeed only for ti≤slot<ht_{i} \le \mathrm{slot} < h; queue_sell succeeds only for W≤slot<EW \le \mathrm{slot} < E and only on an asset captured in that round. Every other slot rejects every trade on every asset. Since no unit exists outside the program, there is no venue to route around the calendar.

Quantity. sell and queue_sell draw only from free. The lock is held as two counters rather than a list of purchases, so the check is constant cost.

Destination. The program exposes no transfer instruction. A unit changes owner in exactly two ways: it is burned into the curve by a sale, or it is transferred on capture. Sale proceeds are credited to the owner's holder account, whose address is derived from the stored owner and never from a caller-supplied account.

Constant-time capture. Each asset account carries a generation counter gXg_{X}, and each position records the generation under which it was written. The effective balance of the position of owner oo on XX is

bal(o,X)={sharesogo=gX0go≠gX\mathrm{bal}(o, X) = \begin{cases} \mathrm{shares}_o & g_o = g_X \\ 0 & g_o \neq g_X \end{cases}
(2)

Capturing XX increments gXg_{X} and credits the units of XX held in positions to the winners' capture pools. Every position on XX evaluates as empty the next time it is touched, and no position account is visited during resolution. The cost of a capture is independent of the number of holders.

3.4Comparison

Table 4: An SPL token compared with a program-derived position.
PropertySPL tokenProgram-derived position
Holder can transfer freelyyesno transfer instruction exists
Trading halts on a scheduleno (freeze is global)yes, by slot, on every asset
Part of a balance can be lockednoyes, free and locked
Can trade on external venuesyesno
Cost of transferring a losing asset's supplyone write per holderone counter increment
Price formationwhichever venue trades itthe curve, a function of SS

4The Pricing Curve

All assets share one curve, fixed for a season. Units are divisible: the program accounts in base units, 10⁶ per displayed unit. Since a single base unit is worth a fraction of a lamport, the curve is specified by its integral, the reserve required behind a given supply, rather than by a per-unit price.

4.1Reserve function

R(S)=⌊S (a+b S)296⌋R(S) = \left\lfloor \frac{S \, (a + b\,S)}{2^{96}} \right\rfloor
(3)

SS is circulating supply in base units and SmaxS_{\mathrm{max}} its cap, and a≥1a \ge 1, b≥0b \ge 0 are integer coefficients scaled by 2⁹⁶. For an asset XX, minted(X)\mathrm{minted}(X) is its SS. RR is non-decreasing with R(0)=0R(0) = 0. Its continuous derivative is affine, so marginal price ramps linearly in supply:

p(S)=a+2b S296lamports per base unit,p(S) = \frac{a + 2b\,S}{2^{96}} \quad \text{lamports per base unit},ρ=p(Smax)p(0)=a+2b Smaxa\rho = \frac{p(S_{max})}{p(0)} = \frac{a + 2b\,S_{max}}{a}
(4)

The quoted price of the next whole unit is R(S+106)−R(S)R(S + 10^{6}) - R(S), within one lamport of the exact integral of the price over that unit, 106⋅p(S+5⋅105)10^{6} \cdot p(S + 5 \cdot 10^{5}).

4.2Settlement

Purchase and sale of nn base units at supply SS are the two halves of one integral:

cost(S,n)=R(S+n)−R(S),\mathrm{cost}(S, n) = R(S + n) - R(S),payout(S,n)=R(S)−R(S−n)\mathrm{payout}(S, n) = R(S) - R(S - n)
(5)

Both are exact integers. The sum telescopes, so any decomposition of an order into smaller orders has identical total cost, and a round trip of nn at the same SS returns exactly the amount paid, before fees. A single participant therefore faces no execution cost beyond the curve itself, and order splitting confers no advantage.

4.3Reserve invariant

reserve(X)=R(minted(X))after every instruction, for every asset\mathrm{reserve}(X) = R(\mathrm{minted}(X)) \quad \text{after every instruction, for every asset}
(6)

A purchase adds exactly cost; a sale subtracts exactly payout. The program applies these as increments and decrements and never recomputes the reserve from RR, so a violation shows up as a mismatch and is never silently corrected. Capture moves units between holders and never moves reserve. Units are created only by purchase and destroyed only by sale, the settlement auction included, or by the season-end liquidation, which pays every remaining unit the asset's average backing at a snapshot; from the start of that liquidation the equality above no longer applies to the asset.

4.4Evaluating R in 128-bit arithmetic

The product S⋅(a+bS)S \cdot (a + bS) can exceed 128 bits, so it is evaluated in two 64-bit limbs:

T=a+bS,T = a + bS,lo=S (T mod 264),\mathit{lo} = S\,(T \bmod 2^{64}),hi=S⌊T/264⌋,\mathit{hi} = S \left\lfloor T / 2^{64} \right\rfloor,mid=hi+⌊lo/264⌋,\mathit{mid} = \mathit{hi} + \left\lfloor \mathit{lo} / 2^{64} \right\rfloor,R=⌊mid/232⌋R = \left\lfloor \mathit{mid} / 2^{32} \right\rfloor
(7)

This equals ⌊S⋅T/296⌋\lfloor S\cdot T / 2^{96}\rfloor exactly (a floor of a floor), with all intermediates below 2¹²⁸. Configuration bounds enforce T<2127T < 2^{127}, supply below 2⁵⁴ base units and R(Smax)<260R(S_{\mathrm{max}}) < 2^{60} lamports.

4.5Inverting the curve for a lamport budget

Orders are usually denominated in lamports. The program finds unitsFor(S,budget)\mathrm{unitsFor}(S, \mathrm{budget}), the largest nn whose cost fits the budget, by bisection over the monotone RR; the solution is unique and requires no square root:

if budget = 0 or S ≥ Smax: return 0
hi = Smax − S; if R(S+hi) − R(S) ≤ budget: return hi
lo = 0                                   // cost(lo) ≤ budget < cost(hi)
while hi − lo > 1:
    m = lo + ⌊(hi − lo)/2⌋
    if R(S+m) − R(S) ≤ budget: lo = m else: hi = m
return lo

On the launch curve this takes at most 41 evaluations of RR, and at most 53 at the largest cap that curve can be raised to; the general bound, supply below 2⁵⁴, allows 57. The TypeScript reference library and the Rust program run the same algorithm and must agree to the base unit; a closed-form reference checks both.

4.6Rounding

Every division rounds against the trader: curve and pro-rata divisions floor, so the reserve keeps the remainder, while fees, the lock and the occupied-asset pool loss round up. A base unit on a flat segment of the integer curve may cost zero lamports; such a unit is backed by less than one lamport, the reserve retains the remainder, and an order whose total cost is zero is rejected.

4.7Launch parameters

The launch curve is fitted exactly from three inputs: a cap of 150 000 units per asset (1.5 × 10¹¹ base units), a fully issued reserve of 30 SOL, and a price ramp ρ=15\rho = 15. The first whole unit costs 25 001 lamports and the last about 375 000. The cap can only be raised, at a round boundary, along the same line, so a raise never changes the value of existing units. At mm times the launch cap the top price is 1+14m1 + 14m times the first and the full reserve is 30⋅m(2+14m)/1630 \cdot m(2 + 14m) / 16 SOL.

5Fees

The protocol's revenue is its trading fees; the one other inflow, once at a season's end, is the lamports no holder is entitled to. Fees are charged on top of the curve amount, never deducted from a reserve, and are paid into dedicated fee vault accounts.

5.1Base rate and terminal ramp

Purchases and sales pay a base rate of 50 basis points. Over the final 9% of the trading window, its last 273 slots at launch (about 59 seconds at the current slot time; see Section 6), a surcharge ramps linearly from zero to 50 basis points on both sides. With τlate=⌊τtrade⋅9/100⌋\tau _{\mathrm{late}} = \lfloor \tau _{\mathrm{trade}} \cdot 9 / 100\rfloor slots in the ramp and dd slots remaining to the halt:

s(d)={0d≥τlate⌊50⋅τlate−dτlate⌋d<τlates(d) = \begin{cases} 0 & d \ge \tau_{late} \\ \left\lfloor 50 \cdot \dfrac{\tau_{late} - d}{\tau_{late}} \right\rfloor & d < \tau_{late} \end{cases}bps(d)=50+s(d)\mathrm{bps}(d) = 50 + s(d)
(8)

The last tradable slot precedes the halt by one, so an order reaches at most ⌊50(τlate−1)/τlate⌋\lfloor 50(\tau _{\mathrm{late}} - 1)/\tau _{\mathrm{late}}\rfloor of the surcharge, 49 basis points with the 273-slot ramp. The ramp prices terminal positioning, when the allocation tallies carry the most information, at up to twice the base rate.

5.2Purchase

For a purchase of xx lamports at total rate bps, the program removes the fee from the budget, fits units to the remainder, then charges the fee on the realised cost, rounded up:

B=⌊x⋅104104+bps⌋,B = \left\lfloor \frac{x \cdot 10^4}{10^4 + \mathrm{bps}} \right\rfloor,n=unitsFor(S,B),n = \mathrm{unitsFor}(S, B),cost=R(S+n)−R(S),\mathrm{cost} = R(S+n) - R(S),fee=⌈cost⋅bps104⌉\mathrm{fee} = \left\lceil \frac{\mathrm{cost} \cdot \mathrm{bps}}{10^4} \right\rceil
(9)

Then cost+fee≤x\mathrm{cost} + \mathrm{fee} \le x holds unconditionally: cost≤B\mathrm{cost} \le B implies cost⋅(104+bps)≤x⋅104\mathrm{cost} \cdot (10^{4} + \mathrm{bps}) \le x \cdot 10^{4}, hence cost+⌈cost⋅bps/104⌉≤x\mathrm{cost} + \lceil \mathrm{cost} \cdot \mathrm{bps} / 10^{4}\rceil \le x. No search loop is needed, and nn is the maximal affordable quantity. The amount entered is always an upper bound on what is debited.

5.3Sale

A sale credits payout−⌈payout⋅bps/104⌉\mathrm{payout} - \lceil \mathrm{payout} \cdot \mathrm{bps} / 10^{4}\rceil to the seller's balance. In the settlement auction the base rate applies without surcharge.

5.4Fee-free operations

The transfer of supply on capture carries no fee, and the validated configuration rejects any non-zero capture fee. Allocation votes, claims and syncs carry no fee. A voided round moves no fees.

5.5Execution guards

Every trading instruction carries the expected round, the maximum accepted fee rate, and a maximum cost or minimum payout. If the round has advanced, the ramp has passed the accepted rate, or the price has moved beyond the bound, the instruction fails rather than executing at a worse price. A compromised read API or a sandwich attempt cannot alter the holder's execution.

5.6Fee vaults

Fees accrue to one of up to 64 fee vault accounts. Each trade names its vault, so concurrent trades on different assets do not contend for a single writable account; protocol revenue is the sum across vaults. No instruction moves lamports from a fee vault to a reserve. The only movement the other way happens once, at a season's end: retiring an asset sweeps its remaining reserve, orphaned captured supply and flooring remainders that no holder is entitled to, into the first fee vault.

6The Round Clock

Time is measured in Solana slots, not wall-clock time, and this paper gives every duration in slots. The launch values were chosen in seconds and converted once, at initialisation, at the mainnet slot time measured then, 217 ms. Two things can change them after this paper was written. Solana's slot time can change, which changes the wall-clock length of every duration while the slot counts stay fixed. The protocol's settings can change the slot counts themselves, at a round boundary and after a public notice of six rounds (26 538 slots at launch). Every wall-clock figure in this paper is therefore an estimate at 217 ms per slot, and every slot count is a launch value; both may differ at the time of reading. Rounds are numbered from 1 and follow an absolute schedule, fixed at genesis and re-anchored only when a change to the slot counts applies:

ti=ta+(i−a)⋅τround,t_i = t_a + (i - a) \cdot \tau_{round},τround=τtrade+τsealed+τres+τauc\tau_{round} = \tau_{trade} + \tau_{sealed} + \tau_{res} + \tau_{auc}
(10)

The anchor (ta,a)(t_{a}, a) is (tgenesis,1)(t_{\mathrm{genesis}}, 1) at launch; a change to the slot counts moves it to the boundary where the change applies, so no existing boundary moves. Every boundary of round ii derives from tit_{i}. No caller, the operator included, can move a boundary; a late call to open a round shortens that round and never shifts the randomness slot LL. Launch parameters:

Table 5: Windows of the round clock at the launch settings. The About column is an estimate at 217 ms per slot (see Section 6).
WindowSlotsAboutPermitted instructions
Trading, tit_{i} to hh3 04111 minbuy, buy_shares, sell, set_vote, sync
Sealed buffer, hh to LL13830 snone that modify positions
Resolution, LL to WW6912 min 30 scapture_hash, reveal_seed, resolve_battle
Settlement auction, WW to EE5532 minqueue_sell on captured assets, claim, sync

A round is 4 423 slots at launch: about 16 minutes, or 90 rounds a day, at 217 ms per slot.

Trading is the only window in which continuous trading is open, uniformly across assets. Allocation votes freeze and trading halts at the same slot hh.

Sealed buffer. A fixed-length interval in which nothing trades, votes or syncs, so the slot whose hash seeds the round is separated from the last state change.

Resolution opens at LL. The slot hash is captured, the seed is revealed and the contests resolve on chain early in the window; the remaining slots exist for presentation. No asset can be traded. Since every outcome is computable by anyone once the seed is revealed, an open market here would reward the fastest chain reader; closing every market until WW makes that information worthless.

Settlement auction opens at WW. Only assets captured in the round accept sell orders, and those settle jointly at EE (see Section 11). Winning holders claim captured supply here. All other assets remain closed until the next trading window.

A round still unresolved at EE cannot be closed. It can be voided once the stuck limit has passed (33 180 slots after LL at launch, about 2 hours at the current slot time), and the following trading window is shortened by the delay rather than displaced.

7The Commitment Lock

A fixed fraction of every purchase is locked until the round of purchase has ended. Without the lock, the dominant strategy would be to buy early, ride inflows while the allocation tallies are public, and exit before the halt if the asset is targeted: curve profit with zero exposure to resolution. Under the lock, every purchase carries collateral into at least one resolution.

The lock is two counters on the position, not a purchase list, so every check is constant cost. With λ=5 000\lambda = 5\,000 basis points and current round ee:

buy(n):ℓ=⌈n λ104⌉,\text{buy}(n): \quad \ell = \left\lceil \frac{n \, \lambda}{10^4} \right\rceil,locked+=ℓ,\mathrm{locked} \mathrel{+}= \ell,free+=n−ℓ,\mathrm{free} \mathrel{+}= n - \ell,lockedEpoch=e\mathrm{lockedEpoch} = e
(11)
release:locked>0 ∧ e>lockedEpoch  ⇒  free+=locked,    locked=0\text{release}: \quad \mathrm{locked} > 0 \,\wedge\, e > \mathrm{lockedEpoch} \;\Rightarrow\; \mathrm{free} \mathrel{+}= \mathrm{locked}, \;\; \mathrm{locked} = 0
(12)

Release runs before every purchase and sale. The ceiling locks a single-base-unit purchase in full. Further purchases in the same round join the locked balance; a purchase in a later round first releases the earlier balance, so a new purchase never extends an older lock.

sell and queue_sell require n≤freen \le \mathrm{free}. No holder instruction sells, transfers or burns locked units. Two protocol paths ignore the lock: a capture takes every unit held in positions on the captured asset, locked units included, and the season-end liquidation pays them out with the rest. The invariant shares=free+locked\mathrm{shares} = \mathrm{free} + \mathrm{locked} holds after every instruction.

The lock releases when the round advances, regardless of whether the round's resolution produced contests or was voided. A voided round had no resolution to protect, and tying release to resolution would freeze the most recent purchase indefinitely if resolutions ever stopped. Supply received by capture or claim is credited to free: it has already been through a resolution.

As a consequence, the committed collateral of a targeted asset includes its most recent buyers.

8Contest Weight and Target Selection

8.1Collateral as contest weight

An asset's weight in a contest is denominated in lamports, not units. The collateral behind nn units of asset XX is their pro-rata share of its reserve, 0 when minted(X)=0\mathrm{minted}(X) = 0:

backing(X,n)=⌊n⋅reserve(X)minted(X)⌋\mathrm{backing}(X, n) = \left\lfloor \frac{n \cdot \mathrm{reserve}(X)}{\mathrm{minted}(X)} \right\rfloor
(13)

An asset's contest weight is exactly the collateral that changes hands if it loses: its reserve, less the collateral behind its units held in other assets' capture pools, plus the collateral behind every capture pool it holds. With cY[X]c_{Y}[X] the units of XX in the pool held by YY, and F(X)F(X) the units of XX held in positions:

F(X)=minted(X)−∑Y≠XcY[X]F(X) = \mathrm{minted}(X) - \sum_{Y \neq X} c_Y[X]
(14)
w(X)=backing(X,F(X))+∑Z≠Xbacking(Z,cX[Z])w(X) = \mathrm{backing}\big(X, F(X)\big) + \sum_{Z \neq X} \mathrm{backing}\big(Z, c_X[Z]\big)
(15)

This is the weight at the launch pool strength of 100%; that setting, if lowered, scales each pool's term down. For an asset with no pools on either side, w=reservew = \mathrm{reserve}. Under this rule every lamport of weight is attributed to exactly one asset and each side's weight is exactly what it would lose, which makes each contest an actuarially fair lottery (see Section 10). The occupied-asset setting described next changes the rule for partly occupied assets.

8.2Occupied assets

The weight rule prices every unit of an asset at the same average backing, whether the unit sits in a position or in another asset's capture pool. On a shared curve this matters for an asset that is partly occupied. A purchase of XX raises reserve(X)\mathrm{reserve}(X), and with it the average backing of every unit of XX, including the units held in its occupiers' pools. Part of the buyer's lamports therefore adds to the occupiers' weight rather than to the weight of XX: in one recorded case, with the setting below at 0, a purchase of 0.988 SOL after fees into a partly occupied asset added 0.716 SOL to its own weight and 0.272 SOL to its occupiers'. Nothing is lost, since the lamports are in the reserve of XX and the occupiers' captured supply is worth more, but an asset's reserve, the lamports it holds, and its contest weight, the lamports it fights with, can differ. The client shows the difference: an occupied asset's card gives, under its weight, what its own holders have in its reserve, and a purchase quote gives how much of the purchase counts in war for the asset.

How much of this effect a contest counts is a setting, occupied_backup_bps, written κ\kappa , from 0 to 10 000 basis points. It changes the weights of partly occupied assets and of the pools that hold their units, and through them the odds of each contest and the split of captured supply among the winners. It does not change which units a capture transfers, the price of any purchase or sale, or the season-end payout, and target selection's exclusion test and the vote floor do not depend on it. At 0 the weights are ww above; at 10 000 a purchase of XX counts its whole cost for XX and nothing for its occupiers; a value between moves each weight κ/10 000\kappa / 10\,000 of the way. Let FF be short for F(X)F(X), P=minted(X)−FP = \mathrm{minted}(X) - F the units in capture pools, and u=min(R(P),reserve(X))u = \mathrm{min}(R(P), \mathrm{reserve}(X)) the bottom of the curve, under the captured supply, with u=0u = 0 when P=0P = 0. With (⋅)+(\cdot )⁺ truncating at zero, the asset's own weight is

own(X)=backing(X,F)+⌊κ⋅(reserve(X)−u−backing(X,F))+104⌋\mathrm{own}(X) = \mathrm{backing}(X, F) + \left\lfloor \frac{\kappa \cdot \big(\mathrm{reserve}(X) - u - \mathrm{backing}(X, F)\big)^{+}}{10^4} \right\rfloor
(16)

with own(X)=0\mathrm{own}(X) = 0 when F=0F = 0. A pool of cc units of ZZ held by XX contributes its average backing moved κ\kappa of the way down to its share of uZu_{Z}, its loss rounded up:

pool(X,Z)=backing(Z,c)−⌈κ⋅(backing(Z,c)−min⁡(⌊c⋅uZ/PZ⌋, uZ))+104⌉\mathrm{pool}(X, Z) = \mathrm{backing}(Z, c) - \left\lceil \frac{\kappa \cdot \Big(\mathrm{backing}(Z, c) - \min\big(\lfloor c \cdot u_Z / P_Z \rfloor,\, u_Z\big)\Big)^{+}}{10^4} \right\rceil
(17)

Here uZu_{Z} and PZP_{Z} are uu and PP computed for ZZ. Both formulas are at the launch pool strength of 100%; the weight of XX is own(X)\mathrm{own}(X) plus the sum of its pools. Because the gain floors and the loss ceils, the parts of one reserve never sum past it. At κ=0\kappa = 0 both formulas reduce to w(X)w(X) above. For the asset's own units the difference has a closed form on a curve of this shape, since the linear term of RR cancels:

reserve(X)−u−backing(X,F)=⌊b F P296⌋+ϵ,\mathrm{reserve}(X) - u - \mathrm{backing}(X, F) = \left\lfloor \frac{b \, F \, P}{2^{96}} \right\rfloor + \epsilon,ϵ∈{0,1,2}\epsilon \in \{0, 1, 2\}
(18)

The gap grows with the product F⋅PF \cdot P: an asset with little supply barely moves, and one near its cap with its supply split between positions and pools moves most. The amount reserve(X)−u\mathrm{reserve}(X) - u is the top of the curve, above the captured supply. It is what the holders of XX would receive, fees apart, by selling all FF units before any captured unit of XX is sold, and exactly what they paid when every unit in positions was bought after the asset's last capture and no captured unit has been sold since; it is the figure an occupied asset's card shows under its weight. The setting therefore chooses where a contest counts an occupied asset's own units between two valuations: at 0 their average backing, at 10 000 the top of the curve.

Neither end of the range is fair under both valuations. Measured at the average backing, which is what the season-end payout pays for every unit and how this paper counts the collateral a capture moves, a contest at 0 is an actuarially fair bet for every side, and above 0 it is not: with challenger weight CC, defender weight DD and stakes stakeC\mathrm{stake}_{C}, stakeD\mathrm{stake}_{D}, the average backing of what each side loses, the challengers' expected gain is

E[ΔC]=δC stakeD−δD stakeCC+D,\mathbb{E}[\Delta_C] = \frac{\delta_C \, \mathrm{stake}_D - \delta_D \, \mathrm{stake}_C}{C + D},δ=weight−stake\delta = \text{weight} - \text{stake}
(19)

Above 0 the own weight of an occupied asset exceeds the backing of its units, and each pool holding its units counts for less than its backing. On this measure a third asset that attacks a partly occupied asset therefore loses in expectation when neither holds other pools, and one contest can move the weight of an asset that takes no part in it. Measured instead at what the holders of XX would receive by selling first, the stake of XX is the top of the curve, its own weight at 10 000: on that measure the setting at 0 undercounts XX in a contest with a third asset, and 10 000 counts it at its stake. Captured units are later sold in an order that no rule fixes, so which measure describes what a contest moves depends on who sells first. On the launch curve, take an asset XX with a reserve of 9 SOL, of which its units in positions are backed by 5 SOL at the average and 6.74 SOL at the top of the curve and its occupier's pool by the other 4 SOL at the average, and a third asset with a reserve of 5 SOL that attacks it. The weight of XX is 5, 5.87 and 6.74 SOL at 0, 5 000 and 10 000, and the attacker's chance is 50%, 46.0% and 42.6%.

The setting also changes what holding captured supply pays. Over 300 simulated rounds, a player who held captured supply finished ahead of an otherwise identical player who sold it by 2.26 ± 0.81 SOL with the setting at 0, 0.60 ± 0.59 SOL at 5 000 and 0.25 ± 0.60 SOL at 10 000. In the simulator's baseline population on the launch curve, with every asset at its cap by round 100 and the setting applied from round 1, the expected value the setting shifts between sides, measured at the average backing, is about 29 SOL per 300-round season at 2 500, 85 SOL at 5 000 and 205 SOL at 10 000, most of it in contests between an asset and its own occupier.

The setting is game state, so changing it needs no program upgrade. set_occupied_backup stages a value, which replaces any value already pending and applies at the first round boundary after the settings notice has elapsed (six rounds, 26 538 slots at launch; see Section 6), but never at a boundary reached at or after the randomness slot LL of the round it would open, so the value a round is played under can never be chosen after that round's slot hash is public. The instruction is refused once a season's end has been announced, and the value carries into the next season. The client shows the current value and marks any pending one before it applies.

8.3Why linear weight

Each non-linear weight function admits a cheap attack. Per-account concavity, such as (∑stake)2(\textstyle\sum \sqrt{\mathrm{stake}})^{2}, is defeated by Sybil splitting, since there is no identity layer. Per-asset concavity, such as reserve\sqrt{\mathrm{reserve}}, is defeated by coalitions, because capital split across allied assets gains combined weight. Convexity rewards concentration directly. Linear weight is the only choice that is neutral to splitting.

The early-entry advantage comes from the curve instead. A late buyer pays the marginal price but adds the average backing, so weight per lamport paid is

average backingmarginal price=ρ+12ρ\frac{\text{average backing}}{\text{marginal price}} = \frac{\rho + 1}{2\rho}
(20)

for a linear ramp of ratio ρ\rho from the bottom of the curve. At the top of a 15× ramp this is 0.53: late capital converts to weight at about half its face value, and the remainder accrues as backing to earlier holders.

8.4Allocation votes

Each position carries one target and a weight equal to its units. An asset's tally is maintained incrementally by every purchase, sale, claim and vote change, so freezing the vote at the halt never iterates over holders. Units in a capture pool do not vote; claiming them is how their holder acquires a vote in that asset. Votes persist across rounds until changed. An asset cannot target itself.

Two floors prevent dust from steering outcomes. A vote counts only if its position is backed by at least a minimum number of lamports, and selling below that floor removes the weight from the tally rather than leaving it standing. A purchase must also be at least a minimum amount, fee included. Both default to the price of 40 initial units, about 0.001 SOL at launch.

8.5Target selection

At the halt, each asset with a non-empty tally selects one target by weighted lottery over its tally, using one draw from the round's random stream (see Section 9). A single voting unit suffices to commit an asset; there is no abstention outcome. Targets with nothing to transfer are excluded and the tally re-weighted over the remainder. The transferable value is measured in lamports, against the minimum prize PminP_{\mathrm{min}}, the price of 40 initial units (1 000 040 lamports at launch):

barren(t)  ⟺  backing(t,F(t))+∑Z≠tbacking(Z,ct[Z])≤Pmin\mathrm{barren}(t) \iff \mathrm{backing}\big(t, F(t)\big) + \sum_{Z \neq t} \mathrm{backing}\big(Z, c_t[Z]\big) \le P_{min}
(21)

With admissible tally weights v1…vkv_{1} \dots v_{k} and V=∑vjV = \textstyle\sum v_{j}, target jj is selected when

∑m<jvm  ≤  (r mod V)  <  ∑m≤jvm\sum_{m < j} v_m \;\le\; (r \bmod V) \;<\; \sum_{m \le j} v_m
(22)

so each target's selection probability equals its share of vote weight. Assets that select the same target contest it jointly as one side. There is no adjacency constraint: any asset may target any other.

9Randomness

All outcomes in a round derive from one 256-bit value Θ\Theta , built from two inputs that no single party controls at the moment it matters: a seed committed long before the round, and the hash Solana records for a slot after trading has stopped.

slot t0​ h L W E trading sealed resolution settlement auction 1 2 3 4 (a) s0​ s1​ · · · si−1​ si​ · · · sN​ sha256 head at genesis head before round i revealed in round i secret Θ HL​ captured at L ‖ sha256(Θ ‖ j) r0​, r1​, r2​, … targets, order, outcomes (b)
Figure 1: Randomness for round ii. (a) Order of events on the slot axis, not to scale: (1) trading and allocation votes halt at hh, so every tally is final; (2) from LL, capture_hash stores HLH_{L}, the hash of the first block at or after LL; (3) the operator reveals sis_{i}, accepted only if it hashes to the stored head; (4) the program computes Θ\Theta and resolves every contest from the draw stream. (b) The seed chain and the derivation of Θ\Theta and the draws. Each arrow in the chain is one application of sha256.

9.1Seed chain

The operator generates a secret terminal seed sNs_{N} and hashes it backwards NN times. The chain head is published on chain before the first round, committing every future seed at once:

sN secret,s_N \text{ secret},si−1=sha256(si),s_{i-1} = \mathrm{sha256}(s_i),head=s0 published at initialisation\text{head} = s_0 \text{ published at initialisation}
(23)

Each reveal discloses the next link; the program accepts it only if it hashes to the stored head, which it then replaces. The launch chain holds 1 000 000 links, one per round: about 30 years at the launch round length and the current slot time.

If a round voids after its slot hash was captured but before a seed was accepted, the required seed may already be public in a failed transaction. The program counts such skips and requires the next reveal to hash k+1k + 1 times to the head, so a possibly exposed seed is never consumed:

sha256(k+1)(s)=head,\mathrm{sha256}^{(k+1)}(s) = \text{head},0≤k≤640 \le k \le 64
(24)

A chain can be replaced only under both the admin and the operator signature, never in the round in which the replacement was staged, and never at a boundary late enough to observe the next round's slot hash. Every replacement is counted publicly.

9.2Capture and reveal

  1. Trading and voting halt at hh, followed by the 138-slot sealed buffer (about 30 seconds at the current slot time).
  2. From slot LL, any account may call capture_hash. It reads from Solana's SlotHashes sysvar the hash of the first slot at or after LL that produced a block, within a 512-slot window (about 111 seconds at the current slot time). The 512 slots are the depth of the SlotHashes sysvar, so this bound is Solana's, not a setting. Falling forward past a skipped slot gives a block producer nothing, because the seed is still secret.
  3. The operator reveals sis_{i} within a deadline of 512 slots after LL at launch. The program refuses a reveal before capture.
  4. The round value is
Θ=sha256(si ∥ HL)\Theta = \mathrm{sha256}\big(s_i \,\|\, H_L\big)
(25)

The producer of slot LL cannot know sis_{i}, so it cannot steer Θ\Theta through block production. The operator knows sis_{i} but cannot influence HLH_{L}. The remaining assumption is an operating rule: the seed holder never runs, controls or pays a Solana block producer. The captured slot is recorded, so an anomalous rate of skipped LL slots would be publicly detectable.

9.3Draw stream

Every random choice in the round is a draw from one stream, each interpreted as a big-endian 256-bit integer:

rj=sha256(Θ ∥ u64le(j)),r_j = \mathrm{sha256}\big(\Theta \,\|\, \mathrm{u64le}(j)\big),j=0,1,2,…j = 0, 1, 2, \dots
(26)

Consumption order is fixed, so the TypeScript reference library and the Rust program reproduce it exactly:

  1. Targets. One draw per active asset in index order, consumed even when the asset has an empty tally.
  2. Contest order. Contests are grouped by defender and listed by defender index, challengers ascending. A Fisher-Yates shuffle runs over the 0-indexed list for i=n−1i = n - 1 down to 1, swapping position ii with position r mod (i+1)r \bmod (i + 1), one draw per swap, n−1n - 1 draws in all.
  3. Outcomes. One draw per contest in shuffled order, consumed even when the contest is void: its defender, or every one of its challengers, was captured earlier in the round, or D+C=0D + C = 0.

Every reduction takes the full 256-bit draw modulo a bound below 2⁶⁶, so every outcome's probability is exact to a relative error below 2⁻¹⁹⁰, an absolute error below 2⁻¹⁹².

9.4Withheld reveals

The operator learns Θ\Theta before revealing and could withhold the reveal. If no reveal lands by the deadline, the round voids with zero transfers: no units move, fees stay where they are, and the next round proceeds. The void counter is on chain and shown on the verification page. Withholding gains the operator nothing, because neither the operator nor the provider holds positions, and it cannot select an outcome: at most it produces a void round, which is public and counted.

10Contest Resolution and Capture

10.1Resolving a contest

All challengers that selected the same defender contest it as one side. With CC the sum of the challengers' weights, DD the defender's weight, and rr the contest's draw:

x=r mod (D+C),x = r \bmod (D + C),defender prevails  ⟺  x<D,\text{defender prevails} \iff x < D,Pr⁡[defender prevails]=DD+C\Pr[\text{defender prevails}] = \frac{D}{D + C}
(27)

The reduction is taken over the full 256-bit draw, limb-wise on chain, with D+C<266D + C < 2^{66}. If the defender prevails it captures every remaining challenger in index order; a failed challenge costs each challenger all of its transferable collateral, just as a successful one costs the defender all of its own.

With the occupied-asset setting at 0, each side's weight equals exactly the collateral it would lose, so every contest is actuarially fair before fees (for other values, see Section 8.2). For the defender:

E[Δ]=DD+C⋅C  −  CD+C⋅D=0\mathbb{E}[\Delta] = \frac{D}{D+C} \cdot C \;-\; \frac{C}{D+C} \cdot D = 0
(28)

and symmetrically for the challengers. Expected value is therefore not created by contest selection; it arises from curve position, from anticipating which assets will be targeted, and from how holders manage captured supply. A contest is void, and still consumes its draw, if its defender or all of its challengers were captured earlier in the round; challengers captured earlier simply drop out of a contest that still has others.

10.2Sequencing within a round

Contests resolve one at a time in shuffled order. An asset captured earlier in the round forfeits its own scheduled challenge, and an asset that prevails in one contest and loses a later one forfeits what it gained. Reciprocal targeting needs no special case: whichever contest is drawn first decides, and the second is void.

10.3Capture

When the winning side captures asset XX:

  1. Each winner's weight is snapshotted before any transfer and reused for every loser in the same contest.
  2. Every unit of XX held in positions, locked units included, becomes captured supply. Units of XX already held in some asset's capture pool remain there; they transfer only when their holding asset is captured.
  3. XX's generation counter increments, which zeroes every position on XX without visiting it (see Section 3).
  4. The captured supply, MM units, is apportioned among winners by weight, with the flooring remainder to the largest contributor, ties to the lowest index:
yi=⌊M⋅wi∑kwk⌋,y_i = \left\lfloor \frac{M \cdot w_i}{\sum_k w_k} \right\rfloor,remainder=M−∑iyi  →  arg⁡max⁡iwi\text{remainder} = M - \sum_i y_i \;\rightarrow\; \arg\max_i w_i
(29)
  1. Every capture pool held by XX transfers to the winners, apportioned the same way.
  2. minted(X)\mathrm{minted}(X) and reserve(X)\mathrm{reserve}(X) are unchanged. Units moved; nothing was burned and no lamport left a reserve.

10.4Capture pools and constant-time accounting

A capture cannot write to every holder of the winning asset, so the program writes one record per source asset on the winner, cA[X]c_{A}[X], with an accumulator from which each entitled holder later derives their share. Each asset holds at most 16 pool slots, one per source.

Each slot keeps a monotone accumulator scaled by 2⁶⁴. When captured supply MM of source XX is credited to asset AA, whose holders' total recorded stake is ΣA\Sigma_A:

accA[X]+=⌊M⋅264ΣA⌋\mathrm{acc}_A[X] \mathrel{+}= \left\lfloor \frac{M \cdot 2^{64}}{\Sigma_A} \right\rfloor
(30)

If ΣA=0\Sigma_A = 0 the supply joins the pool with no entitlement and the accumulator does not move. A holder oo with recorded stake σo\sigma _{o} and bookmark debto[X]\mathrm{debt}_{o}[X] from their last interaction is entitled to

Δzo[X]=σo⋅(accA[X]−debto[X])(×2−64 units of X)\Delta z_o[X] = \sigma_o \cdot \big(\mathrm{acc}_A[X] - \mathrm{debt}_o[X]\big) \quad (\times 2^{-64} \text{ units of } X)
(31)

Entitlements are computed in settle, which runs first in every instruction that touches a position and standalone as the permissionless sync. It iterates over at most 16 slots and their components, so its cost is bounded independently of the holder count; the bookmark then advances to the current accumulator.

Recorded captured supply also earns. A holder's stake is their own units plus stake points for recorded pool entitlements, each pooled unit converted into an equivalent number of the holding asset's units at prices snapshotted when the round's seed was revealed. The points for nn units of source YY pooled on AA are 0 when reserve(A)=0\mathrm{reserve}(A) = 0, and otherwise

pts(Y→A,n)=⌊backing(Y,n)⋅minted(A)reserve(A)⌋\mathrm{pts}(Y \rightarrow A, n) = \left\lfloor \frac{\mathrm{backing}(Y, n) \cdot \mathrm{minted}(A)}{\mathrm{reserve}(A)} \right\rfloor
(32)

A holder who leaves captured supply pooled therefore earns a larger share of the holding asset's next capture than one who claimed it, and loses it if the holding asset is captured. A slot that is retired and re-established clamps stale bookmarks out of the dead interval, so a holder can forfeit one component's accrual over one interval and nothing else.

10.5Claims

claim moves whole units from a holder's recorded entitlement in cA[X]c_{A}[X] into their own position on XX, unlocked. Claimed units are no longer exposed to AA's contests, vote in XX, and can be sold. Claims are permitted only in the settlement auction of a resolved round, and they are irreversible: no instruction moves units into a capture pool. Outside contest resolution, no pool's unit count can increase and no slot can be created.

11The Settlement Auction

After a capture, the receiving holders typically want to exit simultaneously. On a continuous curve the first seller after resolution would extract several times what the last does, converting the opening of trading into a priority-fee race that automated agents win. The settlement auction removes the race: every seller of a captured asset receives the same price.

From WW to EE, each asset captured in the round accepts queue_sell(n). The units move from the seller's free balance into escrow immediately and cannot be withdrawn. The auction accepts no buys; purchasing resumes in the next trading window. At EE, each captured asset's queue settles as a single block down its own curve:

ΔS=∑iqi,\Delta S = \sum_i q_i,G=R(S)−R(S−ΔS)G = R(S) - R(S - \Delta S)
(33)
gi=⌊qi⋅GΔS⌋,g_i = \left\lfloor \frac{q_i \cdot G}{\Delta S} \right\rfloor,fi=⌈gi⋅bs104⌉,f_i = \left\lceil \frac{g_i \cdot b_s}{10^4} \right\rceil,payouti=gi−fi\mathrm{payout}_i = g_i - f_i
(34)

Here qiq_{i} is seller ii's queued units and bsb_{s} the sale rate, 50 basis points. The flooring remainder G−∑giG - \textstyle\sum g_{i} goes to the last seller to queue, less its own fee ⌈(G−∑gi)⋅bs/104⌉\lceil (G - \textstyle\sum g_{i}) \cdot b_{s} / 10^{4}\rceil . Because the gig_{i} and that remainder sum to GG exactly and the reserve decreases by GG, the invariant reserve=R(S−ΔS)\mathrm{reserve} = R(S - \Delta S) holds after settlement and the protocol never pays from its own funds. Each captured asset clears at its own price; the base fee applies without surcharge.

Settlement is resumable. The clearing total, the new supply and the reserve change on the first call, so no later call can price against a different curve; each call, the first included, pays at most 20 sellers and advances a cursor. Any account may continue it. The next round cannot open until every captured asset with a queue has settled, so a large batch can delay the protocol but cannot halt it.

Proceeds are recorded on each seller's position as payout_owed and collected by withdraw_batch, a permissionless instruction whose destination derives from the stored owner. The crank collects on every holder's behalf; a third party can trigger a payout but cannot redirect it.

Participation is optional. Units not sold remain held into the next round, where continuous trading resumes.

12Invariants

The program maintains the following equalities and bounds after every instruction. Each has an executable checker in the reference library and a property test over randomly generated states.

Table 6: Invariants maintained after every instruction.
InvariantStatement
Reserve equalityreserve(X)=R(minted(X))\mathrm{reserve}(X) = R(\mathrm{minted}(X)) for every asset, until a season-end liquidation starts on it
Unit conservationminted(X)\mathrm{minted}(X) equals the units of XX in live positions plus the units of XX in every live capture pool plus the units escrowed in its unsettled settlement auction
Position splitshares=free+locked\mathrm{shares} = \mathrm{free} + \mathrm{locked} for every position
Stake totalseach asset's ΣA\Sigma_A equals its live holders' units plus their recorded stake points
No over-distributionfor every live pool, claimed plus pending entitlements never exceed the supply credited
Retired slots carry no stakea slot that is not live holds zero stake
Pools fill only by captureoutside contest resolution, no pool's units increase and no slot is created
Fair sidesfor each side of a contest, the sum of its weights equals the backing of exactly the units that side would lose, each counted once, when the occupied-asset setting is 0 (see Section 8.2)
Fee separationno lamport moves from a fee vault to a reserve; a reserve pays a fee vault only when a retired asset's residue is swept at a season's end
Arithmetic boundsevery stored accumulator, entitlement and intermediate product is below 2¹²⁸

No floating point appears wherever a lamport, unit, vote weight or multiplier is computed. Multipliers are basis points over 10⁴. Every division rounds against the trader (see Section 4), and each formula is ordered so its intermediates fit in 128 bits; the two exceptions, the product inside RR and the reduction of a 256-bit draw, are evaluated limb-wise.

The reference library is pure: identical inputs produce byte-identical state, with no clock and no randomness beyond the round's committed inputs. That purity is what qualifies it as the oracle against which the on-chain program is tested (see Section 15).

13Liveness and Failure Modes

The protocol is designed so that no single party going offline can strand holder funds.

13.1Permissionless crank

Most of the round loop can be advanced by any account: capture_hash, resolve_battle, void_round, sync and withdraw_batch require no privileged signer. resolve_battle takes no arguments; it resolves the next contest in the drawn order, so a caller can choose neither which contest runs nor how. Independent keeper instances race to submit each step, and duplicates are rejected. Every crank instruction requires the complete set of asset accounts, each verified as the PDA for its index and in index order, so omitting an account to influence an outcome is impossible.

Three instructions are reserved to the operator: reveal_seed, which only the seed holder can produce, and close_window and advance_round, which close the settlement auction and open the next round.

13.2Voided rounds

If the slot hash is not captured within its 512-slot window, or no seed is revealed by the deadline, any account may void the round. A void moves no units and no fees, increments a public counter, and lets the next round proceed. A round whose resolution never completes can always be voided once the stuck limit has passed: 33 180 slots after LL at launch (about 2 hours at the current slot time), and never more than 216 000 slots (about 13 hours at the current slot time).

13.3Dead-man fallback

If the operator stops opening rounds, the protocol continues without it. Once the operator has exceeded the fallback interval since its last on-time opening, any account may close and advance rounds:

fallback=min⁡(4⋅τdeadman,  2⋅τround,max)\text{fallback} = \min\big(4 \cdot \tau_{deadman},\; 2 \cdot \tau_{round,max}\big)
(35)

At launch the dead-man interval is 99 539 slots and the longest permitted round is 216 000 slots, so the fallback opens after the smaller of 4 × 99 539 and 2 × 216 000, that is 398 156 slots: about 24 hours at the current slot time (see Section 6). Until then the round remains where the operator left it; once the fallback opens, every round can be advanced by anyone. The pause instruction itself becomes permissionless after the dead-man interval (99 539 slots at launch, about 6 hours at the current slot time), and a paused protocol still accepts sales: purchases stop at once and contests from the round after next, while exits remain open.

13.4Bounded compute

No instruction iterates over holders. Capture is a counter increment plus a bounded number of pool writes; claims and syncs are bounded by 16 slots per asset; weight computation reads at most 16 asset accounts. The worst single contest measured, pool-carrying challengers defeating a pool-carrying defender, used 447 698 compute units with the occupied-asset setting at 0 and 625 011 above it, within a transaction with a raised compute budget. Holder-signed instructions request compute in proportion to the records they settle.

14Delegated Signing and Custody

14.1Program-controlled balance

A delegated key cannot spend from a wallet; Solana requires the wallet's own signature to move its lamports. A purchase without a wallet prompt therefore requires funds under program control. Each holder has one account at ["player", owner] holding a lamport balance. No keypair controls it, and a holder whose client state is lost reconnects the wallet and finds the balance intact. Sale proceeds, auction proceeds and returned account deposits are credited to this balance, so funds remain inside the protocol until the owner withdraws them.

14.2Session keys

The holder account stores one delegated key and its limits:

Table 7: Fields of the delegated signing key on the holder account.
FieldPurpose
session_keythe delegated public key
session_epochincremented to revoke; a key is valid only at the current epoch
session_expireshard expiry enforced by the program
session_scopecapability bits: buy, sell, vote, claim
session_spent, session_capcumulative cost plus fee under the session, and its ceiling
session_round_ops, session_max_opsoperations this round, and the per-round ceiling
session_positions_opened, session_max_positionspositions created under the session, and the ceiling

Revocation is a single counter increment, independent of how many keys were ever issued; the program never stores a key list, because revoking a list would be an unbounded loop. Every delegated instruction is validated against live account state at execution time, never against data carried in the instruction. The vote and claim capabilities are off by default: claims are irreversible, and a sell-only key could otherwise zero a vote at the last tradable slot.

14.3Derived destinations

Every instruction that moves lamports derives its destination from stored state; none accepts a destination as an argument. Under this rule the caller's identity does not matter for an instruction that only pays its rightful owner, which is why collection and sync can be permissionless.

Table 8: Callers permitted for each lamport-moving instruction.
InstructionOwnerDelegated (capability)Any account
withdraw_war_balance, register_session, revoke_all, revoke_all_and_withdrawyesnono
close_position, close_player_accountyesnono
buy, buy_sharesyesbuyno
sell, queue_sellyessellno
set_voteyesvote, off by defaultno
claimyesclaim, off by defaultno
withdraw_batch, syncyesn/ayes

14.4Emergency exit

revoke_all_and_withdraw is one owner-signed instruction that increments the session epoch and transfers the entire balance to the wallet in the same transaction, leaving a compromised key no window in which to act. It empties the balance; it does not liquidate positions, and the locked portion of the latest purchase remains locked until the round ends.

14.5Custody

No instruction moves lamports out of a holder balance or a position to anyone but its owner, and spending them requires the owner's signature or the owner's scoped delegate. Fee vaults are separate accounts, and protocol-withdrawable revenue is each vault's balance above its rent floor. The operator holds no key that can move a reserve, a holder balance or a position.

15Verification

15.1Outcome proofs

Any participant can open any round's proof and verify its randomness locally, from values already public on chain. The client and the backend run identical checks using the reference library's hashing and draw code:

  1. sis_{i}, hashed k+1k + 1 times, equals the chain head before the reveal, with kk the recorded skip count (0 unless a round was voided after its hash was captured), so the seed was committed in advance.
  2. The slot hash originates from slot LL or later and was captured in a transaction that landed no later than the reveal.
  3. sha256(si∥HL)\mathrm{sha256}(s_{i} \Vert H_{L}) equals the recorded Θ\Theta .
  4. Each asset's target is the weighted selection over its admissible tally at rj mod Vr_{j} \bmod V, one draw per active asset in index order.
  5. The contest order is the Fisher-Yates permutation of the defender-ordered contest list, one draw per swap.
  6. Contest kk in that order consumes the next draw: x=r mod (D+C)x = r \bmod (D + C), and the defender prevails if and only if x<Dx < D, including draws consumed by voided contests.

The round account holds the live round's slot hash, seed and Θ\Theta , and a history account retains the last 64 rounds' seeds and hashes, so verification requires no trusted server. When the backend records a round, it runs the same verification and raises an alert on any mismatch, so a defect reaches an operator before a participant observes it.

15.2Differential testing

The TypeScript reference library implements every rule in this paper. It is pure and deterministic, and each invariant above is a property test over randomly generated states. The Rust program is tested against it by replaying 1 905 recorded cases through both implementations: an accepted operation must leave byte-identical state, and a refused one must be refused with the same error. Scenario suites then run the program on a local validator with real slots, slot hashes and lamports, from a two-round, six-asset configuration to a sixteen-asset joint contest over four rounds, together with delegated signing, the keepers, season termination and the operator fallback.

Adversarial cases are included: a crank instruction given the asset set minus one account, with a duplicate account, or out of order is rejected before any state changes.

15.3Verifiable build

The program interface definition is published, the build is reproducible, and the client is open source, so anyone can confirm that the deployed program is the one specified here.

16Scope of This Version

16.1A deliberately constrained first deployment

The protocol described in this paper is the beta of Despot. Its purpose is empirical: to measure whether demand exists for parimutuel trading as a product category before the design is generalised. The constraints of this version follow from that purpose. Each was chosen because it is simple to specify, to test differentially against the reference library and to audit, not because it is the best mechanism available. The deployment should be read as a prototype of the models that will follow it, not as their final form.

16.2Known limitations and directions

  1. A fixed clock. Every round has four stages of fixed length, so a round with few contests waits through a resolution window sized for many, and a round with many contests compresses their presentation. An elastic clock, in which the resolution window lasts as long as its contests require, a round without contests skips it, and the settlement auction is folded into the next round's opening, preserves every fairness property in this paper: a randomness slot no one can move, one price per captured asset, and no open market while results are public. It requires a program upgrade, not new infrastructure.
  2. A bounded asset set. NMAX=16N_{\mathrm{MAX}} = 16 is compiled into the account layouts as one slot per asset. Keying those slots by asset identifier, bounding them by what an asset actually holds and making per-round state lazy removes the bound, so the asset set and the coalitions within it can grow on demand, with costs proportional to the contests actually fought rather than to the size of the map. The new layout requires a new season.
  3. A synchronised opening. Every curve reopens and every lock releases at the same slot, which rewards the fastest transaction at the boundary. A uniform-price batch at the round's opening, analogous to the settlement auction, removes that race.
  4. An operator-held seed chain. The randomness relies on the seed holder neither steering nor withholding outcomes. Withholding is public and costs holders nothing (see Section 9), but it is still possible. A randomness source with no single seed holder, such as a threshold signature scheme or a verifiable random function, would remove that residual trust.
  5. A single global calendar. Every asset trades and resolves on one schedule. Asynchronous fronts, compressed state for idle assets, contests resolved in an ephemeral rollup or proven off chain, and an application-specific chain were each evaluated. At the scale of this beta each is worse on at least one of cost, trust or latency, and they are kept as options for scales far beyond it.

The invariants, the integer arithmetic and the verification procedure in this paper are intended to carry over unchanged to those later models, which change the clock, the size of the asset set and the source of randomness.