Developers
How settlement works
How a Game program settles a Pot through tesoro_core.
A Match is settled by the Game program, not by a Studio server. The Game keeps its state in a rollup, works out the result itself, and then calls tesoro_core, signing as the authority registered for that Game. tesoro_core moves the Gemas.
- Playertesoro_coreopen_match / join_matchEntries leave each Player's Holdings. Fee basis points are snapshotted.
- GameRollupdelegate state accountsPinned to the one approved TEE validator.
- PlayersRollupplayHidden state. The program enforces the rules and its own clock.
- RollupSolanacommit and undelegateHidden state is wiped, the final state returns to the base layer.
- AnyoneGamesettleThe Game works out the winner itself.
- Gametesoro_coresettle_match(result) by CPISigned by the Game's authority PDA with invoke_signed.
- tesoro_corePlayerscredit payout lines, take FeesLeaderboard Fee goes to the Pool. A MatchWon event is emitted.
Match lifecycle in tesoro_core
| Instruction | Who | Effect |
|---|---|---|
open_match(match_id, game, entry) | A Player | Creates the Match, draws the opener's entry from their Holdings, and snapshots the Platform and Studio Fee basis points. Entries: 500, 1,000 or 2,500. |
join_match | A Player | Draws the joiner's entry, sets status Active, sets the refund deadline to one hour later. |
open_match_backed, join_match_backed | A backed Player | The same, funded by an accepted Offer's reserved Gemas. The Player's own Gemas are never touched. |
extend_match_deadline(new_deadline) | Game authority | Pushes an Active, unexpired Match's refund deadline later, never earlier and at most 7 days from now, so a slow Game cannot be refunded out from under itself. |
settle_match(result) | Game authority only | Settles once, from Open or Active to Settled. Result is Opener, Joiner or Draw. |
refund_match | Game authority any time; the opener of an unjoined Match; anyone after the deadline | Returns every entry to its funder, unchanged. Liveness fallback. |
What a Game must provide
- A program id and an authority: a PDA of the Game program is the norm (seeds
["authority"]), signing byinvoke_signed. A server key also works but is the weaker trust boundary. - Registration by the admin:
register_game(game, authority, studio_bps). Up to 8 Games can be registered. Platform plus Studio Fee together are capped at 5,000 bps. - A way to compute the winner from state the Game controls. Block Puzzle replays moves against the Seed; Hold'em plays out the chips; Trading Duel marks both books at a pinned final price.
- Liveness: timeouts and forfeits enforced by the program, with
extend_match_deadlinewhere the Game's own clock exceeds the one-hour default.
// Inside a Game program (block_puzzle's finalize, abridged): settle by CPI into tesoro_core.
// game_authority is the Game program's own ["authority"] PDA, so it signs with invoke_signed.
let signer = &[&[AUTHORITY_SEED, &[session.authority_bump]][..]];
tesoro_core::cpi::settle_match(
CpiContext::new_with_signer(
tesoro_core_program,
tesoro_core::cpi::accounts::SettleMatch { game_authority, config, match_state },
signer,
)
.with_remaining_accounts(payout_accounts), // per payout: recipient TesoroAccount, then its Holding of that Source
result, // MatchResult::Opener | Joiner | Draw
)?;No change to tesoro_core was needed to support PDA authorities: settle_match only requires that the signer equals the registered authority.
The Pot is a list of lines
A Pot is a list of (source, amount) lines per side. The winner receives the loser's actual lines, each keeping its Source, plus their own entry back. The recipient's Holding of each Source is credited; it counts as own-sourced only if they own that Source's Position. Holdings for a payout must exist before settling; anyone can create them with open_holding, so a recipient cannot block a settlement.
Fees
- Fees are
platform_bps(on Config, 200) plus the Game'sstudio_bps(300 for all three launch Games), both snapshotted when the Match opens, plus the Leaderboard Feeleaderboard_bps(on Config, 100), read from Config when the Match settles. Together they are capped at 5,000 bps of the Pot. - They are taken in Gemas from the loser's lines: Platform Fee first, then Studio Fee, then Leaderboard Fee, then (for a backed win) the Player's share of what remains. Lines are merged by Source and drained in ascending Source-address order.
- The Leaderboard Fee is paid to the Leaderboard Pool, a Tesoro Account owned by the address
["leaderboard"], as one more(recipient, source)payout line. Each decisive settlement also emitsMatchWon(Player, entry, points, season). - Fees are floored, so the winner gets the dust. A Draw, abandonment or no-show takes none.
Entry draw and bounds
- An entry draws
own = min(entry, own_free)from own-sourced Gemas first, the rest from won Gemas. The client supplies Holdings, own class first then won, each strictly ascending by Source address; a non-canonical draw is rejected. MAX_ENTRY_SOURCES = 8Holdings per entry. A settlement therefore pays at most 19(recipient, source)lines (20 for a backed win).- Measured worst case on devnet before the Leaderboard Fee: a Block Puzzle
settlewith 18 payouts is 135,734 compute units and 1,091 bytes of the 1,232 limit as a legacy transaction, so no lookup table is needed.
Backing in the ledger
create_request, create_offer, accept_directed, apply_offer, accept_applicant, cancel_offer, expire_offer. Offers reserve the Backer's entry on their Holdings. Requests and Offers live 24 hours; an accepted Offer has 24 hours to be used and is consumed when the backed Match opens or is joined. Win: the Backer's entry first, then the Player's share of the surplus after Fees, the rest to the Backer. Loss: the Backer loses the entry, nothing more.
Errors
Ledger errors surface as custom program error 6100 + CoreError::code(). Program errors start at 6000 (Unauthorized = 6000, GameNotRegistered = 6001, InvalidAccount = 6003).