Skip to content

Add draft state and betting close time to betting market events - #154

Merged
mikemaccana merged 3 commits into
mainfrom
claude/kind-pasteur-2sw8p5
Sep 23, 2026
Merged

mikemaccana merged 3 commits into
mainfrom
claude/kind-pasteur-2sw8p5

Conversation

@mikemaccana

@mikemaccana mikemaccana commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The betting market locked its outcome list implicitly, by refusing add_outcome once total_pool was nonzero. That let anyone freeze a half-built market with a one-minor-unit bet on its only outcome, and the admin's last add_outcome and the first bet could land in either order. place_bet also accepted stakes right up until settle_event ran, so anyone who saw the result before the admin settled could still bet on it.

Changes (Anchor v2 and Quasar)

  • Draft state: events start as Draft. add_outcome works only on a draft (EventNotDraft, which replaces BettingAlreadyStarted). A new admin handler, open_betting, moves a draft with at least two outcomes to Open (NotEnoughOutcomes otherwise). place_bet on a draft fails with the existing EventNotOpen.
  • Close time: initialize_event takes betting_closes_at, in the order event_id, betting_closes_at, description, because Quasar requires fixed-size arguments first. It must be in the future (CloseTimeInPast). place_bet requires now < betting_closes_at (BettingClosed) and settle_event requires now >= betting_closes_at (BettingStillOpen), so the two windows never overlap. The rule lives in the betting_is_open and may_settle helpers.
  • Cancel: cancel_event accepts a draft as well as an open event.
  • Quasar encoding: EventStatus gains Draft = 0 and shifts the other values up so they match Anchor's borsh encoding. open_betting takes discriminator 9, leaving the existing ones in place.
  • Anchor v1: a frozen snapshot per CONTRIBUTING.md, so it doesn't change.

Tests and proofs

  • New tests (both ports): the outcome list locking when betting opens, a bet on a draft being refused, the two-outcome minimum, admin-only open_betting, both edges of the close time, and a close time in the past. The new Anchor tests assert the specific error code, not just is_err().
  • Kani: proof_betting_and_settlement_windows_partition_time, and proof_outcomes_fixed_before_money_arrives, which checks a model of the lifecycle guards over every six-call sequence. The second proof fails when the old empty-pool guard is put back in the model.

Verification

Checked locally:

  • cargo check, clippy --tests and fmt are clean for both ports on the merged head.
  • The Kani proofs pass.

Not run locally: the LiteSVM and quasar-test suites need SBF binaries, and the toolchain download is blocked in the environment these changes were made in. This PR's CI is their first run.

Companion book PR: quicknode/solana-book#165 (merge after this one).

🤖 Generated with Claude Code

https://claude.ai/code/session_01F8Wo5zNLqZ2NQqSjGD7WpK

The outcome list locked implicitly, by refusing add_outcome once total_pool
was nonzero. Anyone could bet one base unit on a half-built market to freeze
it with a single outcome, and the admin's add_outcome and the first bet could
land in either order. And place_bet accepted stakes until settle_event ran,
so anyone who saw the result before the admin settled could still bet on it.

Events now start as Draft. add_outcome works only on a draft, and a new
admin handler, open_betting, moves a draft with at least two outcomes to
Open, so the question is fixed before any money can arrive.
initialize_event takes a betting_closes_at timestamp: bets land only while
now < betting_closes_at and settlement only once now >= betting_closes_at,
so the two windows never overlap. cancel_event also accepts a draft.

Changed in the Anchor v2 and Quasar ports with tests for each rule; two new
Kani proofs cover the time windows and a lifecycle model of the guards. The
Anchor v1 port is a frozen snapshot and does not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8Wo5zNLqZ2NQqSjGD7WpK
Resolved against main's Config.fee_bps -> default_fee_bps rename: new
events copy config.default_fee_bps into their fee_bps, beside the new
betting_closes_at field and the Draft status. Changelogs keep both entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F8Wo5zNLqZ2NQqSjGD7WpK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants