Skip to content

Fundraiser: stop receipts carrying into the next raise; drop the per-backer cap - #175

Merged
mikemaccana merged 5 commits into
mainfrom
claude/jolly-sagan-u8rgr1
Oct 2, 2026
Merged

mikemaccana merged 5 commits into
mainfrom
claude/jolly-sagan-u8rgr1

Conversation

@mikemaccana

@mikemaccana mikemaccana commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

From Noah Prince's review of the book's Fundraiser chapter. All three implementations (Anchor v2, Anchor v1, Quasar). The perpetual-futures changes that were here have moved to their own pull request.

Bug: a receipt could outlive its fundraiser and count toward the next one

check_contributions closed the Fundraiser account while the Contributor accounts derived from its address stayed open. The maker could then initialize a new fundraiser at the same address, and refund would pay a leftover receipt's old amount out of the new backers' tokens.

  • check_contributions pays out the vault and sets claimed; the Fundraiser and vault stay open.
  • The Fundraiser counts open_contributor_accounts; close_fundraiser requires zero on both paths (ContributorAccountsOpen). Quasar gains close_fundraiser, which it lacked.
  • close_contributor requires claimed (FundraiserNotClaimed, replacing FundraiserStillOpen).
  • refund and close_contributor no longer need the contributor's signature: tokens and rent go only to the contributor, so the maker can close every receipt without waiting on anyone.
  • contribute and a second claim fail with FundraiserClaimed.
  • test_stale_contributor_account_cannot_refund_from_next_raise runs the attack; Kani gains proof_open_contributor_accounts_counts_open_accounts.

Removed: the per-contributor cap

It limited wallets, and wallets cost nothing to create, so it did not stop one person funding most of a raise.

Tests

Anchor v2: 26, Anchor v1: 26, Quasar: 31, all passing; fundraiser Kani harnesses pass under cargo kani.

🤖 Generated with Claude Code

https://claude.ai/code/session_019G9tytYrS3Qp42fZ1hBnDu

Mike MacCana added 5 commits October 1, 2026 19:35
…r-backer cap

A successful claim closed the Fundraiser account while the Contributor
accounts derived from its address stayed open. The maker could then
initialize a new fundraiser at the same address, and a leftover
Contributor account would count as a contribution to it: refund() would
pay its old amount out of the new contributors' tokens.

check_contributions() now pays out the vault and sets a claimed flag.
The Fundraiser counts open Contributor accounts, and close_fundraiser()
requires that count to be zero on both paths. refund() and
close_contributor() no longer need the contributor's signature, so the
maker can close every receipt without waiting on anyone.

The per-contributor cap is removed: it limited wallets, and wallets cost
nothing to create. The Kani cap harness is replaced with one that checks
the open-account counter.

Anchor v2 only; the Anchor v1 and Quasar ports follow.

Claude-Session: https://claude.ai/code/session_019G9tytYrS3Qp42fZ1hBnDu
…d close_fundraiser

Port of the Anchor v2 fix. check_contributions sets claimed instead of
closing the Fundraiser and vault; the Fundraiser counts open Contributor
accounts; the new close_fundraiser handler requires that count to be zero.
refund and close_contributor no longer need the contributor's signature,
and refund checks the destination token account belongs to the
contributor. 31 tests, including two raises at the same address.

Claude-Session: https://claude.ai/code/session_019G9tytYrS3Qp42fZ1hBnDu
… drop the per-backer cap

Port of the Anchor v2 fix, same semantics, errors and 26 tests. The
README now names the handler initialize_fundraiser, which it is.

Claude-Session: https://claude.ai/code/session_019G9tytYrS3Qp42fZ1hBnDu
…e with a trading band

Initial margin: max_leverage becomes initial_margin_bps, which must exceed
maintenance_margin_bps (InitialMarginNotAboveMaintenance). open_position
requires net collateral of at least initial_margin_bps of the size
(InitialMarginNotMet); the separate maintenance check at open is implied.

Price band: the pool keeps a time-weighted average of the oracle price
(average_price, last_oracle_price, average_price_timestamp, ten-minute
window). Each fold credits the elapsed time to the price seen at the
previous read, so one manipulated read after an idle spell moves nothing.
open_position, close_position, add_liquidity and remove_liquidity refuse
an oracle price more than max_price_deviation_bps from the average
(PriceOutsideBand). liquidate_position and the new permissionless
update_price_average() fold without the check, so liquidations run
through a genuine move and the average can catch up with it.

All three implementations; anchor 37, anchor-v1 37, quasar 28 tests.

Claude-Session: https://claude.ai/code/session_019G9tytYrS3Qp42fZ1hBnDu
This pull request is the fundraiser fix only. The perpetual-futures
initial margin and price band are on claude/perps-margin-price-band.

This reverts commit 1f1a46e.

Claude-Session: https://claude.ai/code/session_019G9tytYrS3Qp42fZ1hBnDu
@mikemaccana mikemaccana changed the title Fundraiser: fix receipts carrying into the next raise; perps: initial margin and a price band Fundraiser: stop receipts carrying into the next raise; drop the per-backer cap Oct 1, 2026
@mikemaccana
mikemaccana merged commit 479b003 into main Oct 2, 2026
33 checks passed
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.

1 participant