Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions finance/betting-market/anchor-v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,16 @@ handler that pays it out or closes it: `claim_winnings` and `close_losing_bet` a
`claim_refund` after a cancellation. Once `open_bets` reaches zero the admin closes the accounts the
event created, children first: `close_outcome` for each Outcome account, then `close_event` for the
vault and the Event account. The order matters because each account's address is derived from its
parent's. An Outcome account left open after its event closed would be found again, with its old
`total_amount`, by a later event created with the same `event_id`, and a Bet account left open
after its outcome closed would have nothing to claim against. So `close_outcome` refuses while any
bet is open, and `close_event` refuses while any outcome is. After a settlement the vault holds
only the dust the floored payouts left behind, and `close_event` pays it to the fee recipient with
the fee; after a cancellation and its refunds the vault is empty. Every closed account's rent goes
back to whoever paid it: the bettor for a Bet account, the admin for the outcomes, the vault and
the event.
parent's, and an Event's from the admin-supplied `event_id`. An Outcome account left open after its
event closed would sit at the address a later event created with the same `event_id` needs for its
first outcome, so that event's `add_outcome` would fail. A Bet left open is a different case: no
claim, refund or losing-bet close reads the Outcome account, so keeping an Outcome open for as long
as any Bet names its address is an ordering rule rather than something those handlers need. So
`close_outcome` refuses while any bet is open, and `close_event` refuses while any outcome is. After
a settlement the vault holds only the dust the floored payouts left behind, and `close_event` pays
it to the fee recipient with the fee; after a cancellation and its refunds the vault is empty. Every
closed account's rent goes back to whoever paid it: the bettor for a Bet account, the admin for the
outcomes, the vault and the event.

## Setup

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,12 @@ pub fn handle_close_event(context: Context<CloseEventAccountConstraints>) -> Res
context.accounts.event.open_bets == 0,
BettingError::BetsStillOpen
);
// An Outcome account left behind would be found again, with its old
// `total_amount`, by a later event created with the same `event_id`.
// Every Outcome account of the event must already be closed. Outcome
// addresses derive from the event's, and the event's from the
// admin-supplied `event_id`, so a later event created with the same
// `event_id` reuses this event's address and its outcome addresses. An
// Outcome left behind would make that event's first `add_outcome` fail,
// because `init` refuses an account that already exists.
require!(
context.accounts.event.open_outcomes == 0,
BettingError::OutcomesStillOpen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use anchor_lang::prelude::*;
use crate::{error::BettingError, Config, Event, EventStatus, Outcome};

// Close one Outcome account of a finished event and return its rent to the
// admin, who paid it. Bet accounts derive their address from the outcome's,
// so every bet of the event must already be closed, and the Outcome accounts
// must all close before `close_event` can close the event.
// admin, who paid it. The event's accounts close in the reverse of the order
// they were created: every Bet of the event before any Outcome, and every
// Outcome before `close_event` can close the event.
#[derive(Accounts)]
pub struct CloseOutcomeAccountConstraints<'info> {
#[account(mut)]
Expand Down Expand Up @@ -43,9 +43,13 @@ pub fn handle_close_outcome(context: Context<CloseOutcomeAccountConstraints>) ->
),
BettingError::EventNotFinished
);
// A Bet account derives its address from its outcome's, so an outcome
// closed under an open bet would leave that bet's claim or refund with
// no outcome to check against.
// No claim, refund or losing-bet close reads the Outcome: each re-derives
// the Bet address from the outcome pubkey the Bet stores, which stays
// valid as bytes after the Outcome closes. This check is an ordering
// rule, not something those handlers need: an Outcome stays open for as
// long as any Bet of the event names it by address, so no live position
// ever points at an address the program has emptied, and the event's
// `open_bets` counter is the gate.
require!(
context.accounts.event.open_bets == 0,
BettingError::BetsStillOpen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ pub struct Event {
pub outcome_count: u8,
// How many Outcome accounts of this event are still open. `add_outcome`
// adds one; `close_outcome` subtracts one. `close_event` requires zero,
// because an Outcome left behind would carry its stakes into a later
// event created with the same `event_id`.
// because an Outcome left behind would make `add_outcome` fail for a
// later event created with the same `event_id`, whose outcome
// addresses are the same.
pub open_outcomes: u8,
// How many Bet accounts across every outcome are still open. `place_bet`
// adds one when it creates a Bet account (a top-up reuses the account);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1810,8 +1810,10 @@ fn test_close_event_pays_dust_to_fee_recipient_and_returns_rent() {
);
}

// A settled event keeps its accounts while any Bet account of it is open:
// the claim and the losing-bet close both read the event and the outcome.
// A settled event keeps its Event and Outcome accounts while any Bet account
// of it is open. The claim and the losing-bet close read only the event and
// the bet, so the outcome half is an ordering rule: an Outcome stays open
// for as long as any Bet of the event names it by address.
#[test]
fn test_close_event_refused_while_a_bet_is_open() {
let mut market = setup();
Expand Down
18 changes: 10 additions & 8 deletions finance/betting-market/anchor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,16 @@ handler that pays it out or closes it: `claim_winnings` and `close_losing_bet` a
`claim_refund` after a cancellation. Once `open_bets` reaches zero the admin closes the accounts the
event created, children first: `close_outcome` for each Outcome account, then `close_event` for the
vault and the Event account. The order matters because each account's address is derived from its
parent's. An Outcome account left open after its event closed would be found again, with its old
`total_amount`, by a later event created with the same `event_id`, and a Bet account left open
after its outcome closed would have nothing to claim against. So `close_outcome` refuses while any
bet is open, and `close_event` refuses while any outcome is. After a settlement the vault holds
only the dust the floored payouts left behind, and `close_event` pays it to the fee recipient with
the fee; after a cancellation and its refunds the vault is empty. Every closed account's rent goes
back to whoever paid it: the bettor for a Bet account, the admin for the outcomes, the vault and
the event.
parent's, and an Event's from the admin-supplied `event_id`. An Outcome account left open after its
event closed would sit at the address a later event created with the same `event_id` needs for its
first outcome, so that event's `add_outcome` would fail. A Bet left open is a different case: no
claim, refund or losing-bet close reads the Outcome account, so keeping an Outcome open for as long
as any Bet names its address is an ordering rule rather than something those handlers need. So
`close_outcome` refuses while any bet is open, and `close_event` refuses while any outcome is. After
a settlement the vault holds only the dust the floored payouts left behind, and `close_event` pays
it to the fee recipient with the fee; after a cancellation and its refunds the vault is empty. Every
closed account's rent goes back to whoever paid it: the bettor for a Bet account, the admin for the
outcomes, the vault and the event.

## Setup

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ pub fn handle_close_event(context: &mut Context<CloseEventAccountConstraints>) -
context.accounts.event.open_bets == 0,
BettingError::BetsStillOpen
);
// An Outcome account left behind would be found again, with its old
// `total_amount`, by a later event created with the same `event_id`.
// Every Outcome account of the event must already be closed. Outcome
// addresses derive from the event's, and the event's from the
// admin-supplied `event_id`, so a later event created with the same
// `event_id` reuses this event's address and its outcome addresses. An
// Outcome left behind would make that event's first `add_outcome` fail,
// because `init` refuses an account that already exists.
require!(
context.accounts.event.open_outcomes == 0,
BettingError::OutcomesStillOpen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use crate::state::Event;
use crate::{error::BettingError, Config, EventStatus, Outcome};

// Close one Outcome account of a finished event and return its rent to the
// admin, who paid it. Bet accounts derive their address from the outcome's,
// so every bet of the event must already be closed, and the Outcome accounts
// must all close before `close_event` can close the event.
// admin, who paid it. The event's accounts close in the reverse of the order
// they were created: every Bet of the event before any Outcome, and every
// Outcome before `close_event` can close the event.
#[derive(Accounts)]
pub struct CloseOutcomeAccountConstraints {
#[account(mut, address = config.admin @ BettingError::Unauthorized)]
Expand Down Expand Up @@ -42,9 +42,13 @@ pub fn handle_close_outcome(context: &mut Context<CloseOutcomeAccountConstraints
),
BettingError::EventNotFinished
);
// A Bet account derives its address from its outcome's, so an outcome
// closed under an open bet would leave that bet's claim or refund with
// no outcome to check against.
// No claim, refund or losing-bet close reads the Outcome: each re-derives
// the Bet address from the outcome pubkey the Bet stores, which stays
// valid as bytes after the Outcome closes. This check is an ordering
// rule, not something those handlers need: an Outcome stays open for as
// long as any Bet of the event names it by address, so no live position
// ever points at an address the program has emptied, and the event's
// `open_bets` counter is the gate.
require!(
context.accounts.event.open_bets == 0,
BettingError::BetsStillOpen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ pub struct Event {
pub outcome_count: u8,
// How many Outcome accounts of this event are still open. `add_outcome`
// adds one; `close_outcome` subtracts one. `close_event` requires zero,
// because an Outcome left behind would carry its stakes into a later
// event created with the same `event_id`.
// because an Outcome left behind would make `add_outcome` fail for a
// later event created with the same `event_id`, whose outcome
// addresses are the same.
pub open_outcomes: u8,
// How many Bet accounts across every outcome are still open. `place_bet`
// adds one when it creates a Bet account (a top-up reuses the account);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1808,8 +1808,10 @@ fn test_close_event_pays_dust_to_fee_recipient_and_returns_rent() {
);
}

// A settled event keeps its accounts while any Bet account of it is open:
// the claim and the losing-bet close both read the event and the outcome.
// A settled event keeps its Event and Outcome accounts while any Bet account
// of it is open. The claim and the losing-bet close read only the event and
// the bet, so the outcome half is an ordering rule: an Outcome stays open
// for as long as any Bet of the event names it by address.
#[test]
fn test_close_event_refused_while_a_bet_is_open() {
let mut market = setup();
Expand Down
25 changes: 14 additions & 11 deletions finance/betting-market/quasar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,20 @@ account has a handler that pays it out or closes it: `claim_winnings` and
`open_bets` reaches zero the admin closes the accounts the event created,
children first: `close_outcome` for each Outcome account, then `close_event` for
the vault and the Event account. The order matters because each account's
address is derived from its parent's. An Outcome account left open after its
event closed would be found again, with its old `total_amount`, by a later event
created with the same `event_id`, and a Bet account left open after its outcome
closed would have nothing to claim against. So `close_outcome` refuses while any
bet is open, and `close_event` refuses while any outcome is. After a settlement
the vault holds only the dust the floored payouts left behind, and `close_event`
pays it to the fee recipient with the fee; after a cancellation and its refunds
the vault is empty. Every closed account's rent goes back to whoever paid it:
the bettor for a Bet account, the admin for the outcomes, the vault and the
event. The Config account stays open; it is the admin's state for the whole
deployment.
address is derived from its parent's, and an Event's from the admin-supplied
`event_id`. An Outcome account left open after its event closed would sit at the
address a later event created with the same `event_id` needs for its first
outcome, so that event's `add_outcome` would fail. A Bet left open is a
different case: no claim, refund or losing-bet close reads the Outcome account,
so keeping an Outcome open for as long as any Bet names its address is an
ordering rule rather than something those handlers need. So `close_outcome`
refuses while any bet is open, and `close_event` refuses while any outcome is.
After a settlement the vault holds only the dust the floored payouts left
behind, and `close_event` pays it to the fee recipient with the fee; after a
cancellation and its refunds the vault is empty. Every closed account's rent
goes back to whoever paid it: the bettor for a Bet account, the admin for the
outcomes, the vault and the event. The Config account stays open; it is the
admin's state for the whole deployment.

## Accounts and PDAs

Expand Down
8 changes: 6 additions & 2 deletions finance/betting-market/quasar/src/instructions/close_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,12 @@ pub fn handle_close_event(accounts: &mut CloseEventAccountConstraints) -> Result
u64::from(accounts.event.open_bets) == 0,
BettingError::BetsStillOpen
);
// An Outcome account left behind would be found again, with its old
// `total_amount`, by a later event created with the same `event_id`.
// Every Outcome account of the event must already be closed. Outcome
// addresses derive from the event's, and the event's from the
// admin-supplied `event_id`, so a later event created with the same
// `event_id` reuses this event's address and its outcome addresses. An
// Outcome left behind would make that event's first `add_outcome` fail,
// because `init` refuses an account that already exists.
require!(
accounts.event.open_outcomes == 0,
BettingError::OutcomesStillOpen
Expand Down
16 changes: 10 additions & 6 deletions finance/betting-market/quasar/src/instructions/close_outcome.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ use crate::errors::BettingError;
use crate::state::{snapshot_event, Config, Event, EventStatus, Outcome};

// Close one Outcome account of a finished event and return its rent to the
// admin, who paid it. Bet accounts derive their address from the outcome's,
// so every bet of the event must already be closed, and the Outcome accounts
// must all close before `close_event` can close the event.
// admin, who paid it. The event's accounts close in the reverse of the order
// they were created: every Bet of the event before any Outcome, and every
// Outcome before `close_event` can close the event.
#[derive(Accounts)]
pub struct CloseOutcomeAccountConstraints {
#[account(mut)]
Expand Down Expand Up @@ -36,9 +36,13 @@ pub fn handle_close_outcome(
|| accounts.event.status == EventStatus::Cancelled as u8,
BettingError::EventNotFinished
);
// A Bet account derives its address from its outcome's, so an outcome
// closed under an open bet would leave that bet's claim or refund with
// no outcome to check against.
// No claim, refund or losing-bet close reads the Outcome: each re-derives
// the Bet address from the outcome pubkey the Bet stores, which stays
// valid as bytes after the Outcome closes. This check is an ordering
// rule, not something those handlers need: an Outcome stays open for as
// long as any Bet of the event names it by address, so no live position
// ever points at an address the program has emptied, and the event's
// `open_bets` counter is the gate.
require!(
u64::from(accounts.event.open_bets) == 0,
BettingError::BetsStillOpen
Expand Down
5 changes: 3 additions & 2 deletions finance/betting-market/quasar/src/state/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ pub struct Event {
pub outcome_count: u8,
/// How many Outcome accounts of this event are still open. `add_outcome`
/// adds one; `close_outcome` subtracts one. `close_event` requires zero,
/// because an Outcome left behind would carry its stakes into a later
/// event created with the same `event_id`.
/// because an Outcome left behind would make `add_outcome` fail for a
/// later event created with the same `event_id`, whose outcome
/// addresses are the same.
pub open_outcomes: u8,
/// How many Bet accounts across every outcome are still open. `place_bet`
/// adds one when it creates a Bet account (a top-up reuses the account);
Expand Down
6 changes: 4 additions & 2 deletions finance/betting-market/quasar/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,8 +708,10 @@ fn close_event_pays_dust_to_fee_recipient_and_returns_rent(test: &mut Test) {
assert_eq!(test.tokens(FEE_RECIPIENT_TOKEN), 4);
}

/// A settled event keeps its accounts while any Bet account of it is open:
/// the claim and the losing-bet close both read the event and the outcome.
/// A settled event keeps its Event and Outcome accounts while any Bet account
/// of it is open. The claim and the losing-bet close read only the event and
/// the bet, so the outcome half is an ordering rule: an Outcome stays open
/// for as long as any Bet of the event names it by address.
#[quasar_test]
fn close_event_refused_while_a_bet_is_open(test: &mut Test) {
base_world(test);
Expand Down
2 changes: 1 addition & 1 deletion finance/escrow/anchor-v1/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Unreleased, 2026-10-05

- The tests tell the book's story: token A is TSLAx, minted at 8 decimals, token B is USDC at 6, and Alice offers 1 TSLAx (`TSLAX_OFFERED`, 100,000,000 minor units) for 1,000 USDC (`USDC_WANTED`, 1,000,000,000 minor units). `test_cancel_offer_rejects_non_maker` asserts Anchor's `ConstraintHasOne` (2001), so every refusal test asserts its error code.
- The tests tell the book's story the way the book does: token A is USDC, minted at 6 decimals, token B is TSLAx at 8, and Alice offers 250 USDC (`USDC_OFFERED`, 250,000,000 minor units) for 1 TSLAx (`TSLAX_WANTED`, 100,000,000 minor units), which Bob takes. Both start with the standard wallet of 1 SOL and 1,000 USDC, and Bob also holds 1 TSLAx, so the take leaves him at 1,250 USDC. The switched-offer tests re-make the offer at 1 USDC for the same 1 TSLAx, and at the same 250 USDC for 2 TSLAx. The take test asserts both sides' balances in both tokens. `test_cancel_offer_rejects_non_maker` asserts Anchor's `ConstraintHasOne` (2001), so every refusal test asserts its error code.

## 2026-10-04

Expand Down
Loading
Loading