diff --git a/CHANGELOG.md b/CHANGELOG.md index b85d85ae5..991a6103e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this repository are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [2026-10-03] - Fundraiser: `close_contributor` is `close_contribution` + +### Changed + +- `finance/fundraiser` (Anchor v2, Anchor v1, Quasar) renames the account that + records one contributor's contributions from `Contributor` to + `Contribution`, and its closing handler from `close_contributor` to + `close_contribution`: the handler closes that account, not the contributor. + The PDA seed prefix becomes `"contribution"`, the Fundraiser's + `open_contributor_accounts` becomes `open_contributions`, and the + `ContributorAccountsOpen` error becomes `ContributionsOpen`. No behavior + changes. + ## [2026-10-03] - Order book tests cover the costliest cancel `cancel_order` finds an order's place in the tree by walking its side from the diff --git a/finance/fundraiser/anchor-v1/CHANGELOG.md b/finance/fundraiser/anchor-v1/CHANGELOG.md index 912e35d7a..0718dffe9 100644 --- a/finance/fundraiser/anchor-v1/CHANGELOG.md +++ b/finance/fundraiser/anchor-v1/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2026-10-03 + +### Changed + +- **The per-contributor account is `Contribution`, closed by `close_contribution`.** The handler named `close_contributor` closed the account recording one contributor's contributions, not the contributor, so it is now `close_contribution` (account constraints `CloseContributionAccountConstraints`). The account struct `Contributor` is now `Contribution`, its handler field `contributor_account` is `contribution`, and its PDA seed prefix is `"contribution"` instead of `"contributor"`. The Fundraiser's `open_contributor_accounts` count is `open_contributions`, and the error `ContributorAccountsOpen` is `ContributionsOpen`. The `contributor` account (the contributor's wallet) keeps its name. No behavior changes. + ## 2026-10-01 ### Fixed diff --git a/finance/fundraiser/anchor-v1/README.md b/finance/fundraiser/anchor-v1/README.md index 946c0f8a8..2873c324c 100644 --- a/finance/fundraiser/anchor-v1/README.md +++ b/finance/fundraiser/anchor-v1/README.md @@ -25,7 +25,7 @@ pub struct Fundraiser { pub time_started: i64, pub duration: u16, pub claimed: bool, - pub open_contributor_accounts: u32, + pub open_contributions: u32, pub bump: u8, } ``` @@ -39,7 +39,7 @@ Fields: - `time_started` - when the fundraiser was created. - `duration` - fundraising window in days. - `claimed` - set by `check_contributions`. A claimed fundraiser accepts no more contributions and no second claim. -- `open_contributor_accounts` - how many Contributor accounts written for this fundraiser are still open. `contribute` adds one when it creates a Contributor account; `refund` and `close_contributor` each subtract one when they close one. `close_fundraiser` requires zero. +- `open_contributions` - how many Contribution accounts written for this fundraiser are still open. `contribute` adds one when it creates a Contribution account; `refund` and `close_contribution` each subtract one when they close one. `close_fundraiser` requires zero. - `bump` - canonical bump for the Fundraiser [PDA](https://solana.com/docs/terminology#program-derived-address-pda). The `InitSpace` derive macro implements the `Space` trait, which calculates the size of the account (not counting the [Anchor](https://solana.com/docs/terminology#anchor) discriminator). @@ -49,16 +49,16 @@ A per-contributor record: ```rust #[account] #[derive(InitSpace)] -pub struct Contributor { +pub struct Contribution { pub amount: u64, pub bump: u8, } ``` - `amount` - total amount contributed by this contributor. -- `bump` - canonical bump for the Contributor PDA. +- `bump` - canonical bump for the Contribution PDA. -The Contributor PDA uses `init_if_needed`, which only runs the init branch on first call. On first init (when `bump == 0`) the handler stores `bumps.contributor_account` into `bump` and adds one to `open_contributor_accounts`; see [`instructions/contribute.rs`](programs/fundraiser/src/instructions/contribute.rs). +The Contribution PDA uses `init_if_needed`, which only runs the init branch on first call. On first init (when `bump == 0`) the handler stores `bumps.contribution` into `bump` and adds one to `open_contributions`; see [`instructions/contribute.rs`](programs/fundraiser/src/instructions/contribute.rs). ### Constants @@ -94,7 +94,7 @@ The maker signs and pays for two new accounts: - `fundraiser` - the state account, derived from `b"fundraiser"` and the maker's public key. Anchor calculates the canonical bump and the handler stores it. - `vault` - the [ATA](https://solana.com/docs/terminology#associated-token-account-ata) that receives contributions, owned by the Fundraiser PDA. -The handler requires `amount >= MIN_AMOUNT_TO_RAISE * 10^decimals` (the target must be at least 3 major units of the mint, expressed in minor units), then initializes the Fundraiser state with `current_amount = 0`, `claimed = false`, `open_contributor_accounts = 0`, and `time_started` from the `Clock` sysvar. A target below the minimum fails with `InvalidAmount`. +The handler requires `amount >= MIN_AMOUNT_TO_RAISE * 10^decimals` (the target must be at least 3 major units of the mint, expressed in minor units), then initializes the Fundraiser state with `current_amount = 0`, `claimed = false`, `open_contributions = 0`, and `time_started` from the `Clock` sysvar. A target below the minimum fails with `InvalidAmount`. ### `contribute` @@ -106,7 +106,7 @@ A contributor signs and the handler performs three checks in order: 2. Not yet claimed: `!claimed`, else `FundraiserClaimed`. The deadline may still be days away after a claim, and the vault has already been paid out. 3. Time window: contributions are allowed while `elapsed_days < duration`, where `elapsed_days = (now - time_started) / SECONDS_TO_DAYS`. Once `elapsed_days` reaches `duration` the handler fails with `FundraiserEnded`. -If all checks pass, `Fundraiser.current_amount` and `Contributor.amount` are updated (a contributor's later contributions add to the same Contributor account), then `amount` is transferred from `contributor_ata` to `vault` with `transfer_checked`. +If all checks pass, `Fundraiser.current_amount` and `Contribution.amount` are updated (a contributor's later contributions add to the same Contribution account), then `amount` is transferred from `contributor_ata` to `vault` with `transfer_checked`. ### `check_contributions` @@ -114,7 +114,7 @@ If all checks pass, `Fundraiser.current_amount` and `Contributor.amount` are upd Lets the maker claim the funds once the target is met. Requires `!claimed`, else `FundraiserClaimed`, and `fundraiser.current_amount >= amount_to_raise` (the state-tracked total, so direct donations to the vault cannot unlock the claim early), else `TargetNotMet`. The handler sets `claimed` and transfers the entire vault balance (including any direct donations) to `maker_ata` with `transfer_checked`, signed with the Fundraiser PDA's seeds. -The Fundraiser account and the empty vault stay open. Contributor accounts are derived from the Fundraiser's address, so the Fundraiser must outlive every one of them: if it closed here, the maker could initialize a new fundraiser at the same address, and the Contributor accounts left over from this raise would count as contributions to the new one, so `refund` would pay their old amounts out of the new contributors' tokens. `close_contributor` closes the Contributor accounts, then `close_fundraiser` closes the Fundraiser and the vault. +The Fundraiser account and the empty vault stay open. Contribution accounts are derived from the Fundraiser's address, so the Fundraiser must outlive every one of them: if it closed here, the maker could initialize a new fundraiser at the same address, and the Contribution accounts left over from this raise would count as contributions to the new one, so `refund` would pay their old amounts out of the new contributors' tokens. `close_contribution` closes the Contribution accounts, then `close_fundraiser` closes the Fundraiser and the vault. ### `refund` @@ -125,7 +125,7 @@ Returns a contribution after a failed fundraiser. The contributor does not have 1. Refunds are allowed only after the fundraiser has ended: `elapsed_days >= duration`, else `FundraiserNotEnded`. 2. The target was not met: `fundraiser.current_amount < amount_to_raise` (again the state-tracked total, so donated tokens cannot block refunds), else `TargetMet`. -The handler subtracts the contributor's recorded amount from `current_amount` zeroes the Contributor record, and subtracts one from `open_contributor_accounts` before the transfer CPI, then sends the tokens from the vault back to `contributor_ata` with `transfer_checked` (PDA signer). The Contributor account is closed via `close = contributor`, refunding its rent to the contributor. +The handler subtracts the contributor's recorded amount from `current_amount` zeroes the Contribution record, and subtracts one from `open_contributions` before the transfer CPI, then sends the tokens from the vault back to `contributor_ata` with `transfer_checked` (PDA signer). The Contribution account is closed via `close = contributor`, refunding its rent to the contributor. ### `close_fundraiser` @@ -139,17 +139,17 @@ For an unclaimed fundraiser, three checks: 2. The target was not met: `fundraiser.current_amount < amount_to_raise`, else `TargetMet` (a raise that met its target closes after the maker claims it). 3. Every contribution has been refunded: `fundraiser.current_amount == 0`, else `RefundsOutstanding` (closing the vault earlier would strand the remaining refunds). -For every fundraiser, claimed or not: `open_contributor_accounts == 0`, else `ContributorAccountsOpen`. A Contributor account left open would be read as a contribution to the next fundraiser at this address. +For every fundraiser, claimed or not: `open_contributions == 0`, else `ContributionsOpen`. A Contribution account left open would be read as a contribution to the next fundraiser at this address. Anything still in the vault at this point is a direct donation outside the program's accounting; the handler pays it to `maker_ata` with `transfer_checked` rather than burning it, then closes the vault with `close_account` (both CPIs signed with the Fundraiser PDA's seeds). The Fundraiser state account is closed via `close = maker`. -### `close_contributor` +### `close_contribution` -[`programs/fundraiser/src/instructions/close_contributor.rs`](programs/fundraiser/src/instructions/close_contributor.rs), account constraints `CloseContributorAccountConstraints`. +[`programs/fundraiser/src/instructions/close_contribution.rs`](programs/fundraiser/src/instructions/close_contribution.rs), account constraints `CloseContributionAccountConstraints`. -Closes a Contributor account once its fundraiser has been claimed, returning the rent to the contributor. On a successful raise the contribution has been paid out to the maker, so the account holds only rent, and `close_fundraiser` cannot run until every one of them is closed. +Closes a Contribution account once its fundraiser has been claimed, returning the rent to the contributor. On a successful raise the contribution has been paid out to the maker, so the account holds only rent, and `close_fundraiser` cannot run until every one of them is closed. -One check: `fundraiser.claimed`, else `FundraiserNotClaimed`. While the fundraiser is unclaimed the contribution can still be refunded, so `refund` is the way to close it. The contributor does not have to sign: the rent goes to them whoever sends the transaction, so the maker can close every Contributor account and then the fundraiser. The Contributor account's seeds bind it to the fundraiser's address, and the handler subtracts one from `open_contributor_accounts`. The account is closed via `close = contributor`. +One check: `fundraiser.claimed`, else `FundraiserNotClaimed`. While the fundraiser is unclaimed the contribution can still be refunded, so `refund` is the way to close it. The contributor does not have to sign: the rent goes to them whoever sends the transaction, so the maker can close every Contribution account and then the fundraiser. The Contribution account's seeds bind it to the fundraiser's address, and the handler subtracts one from `open_contributions`. The account is closed via `close = contributor`. ## Testing @@ -162,11 +162,11 @@ cargo test The suite uses a nonzero duration and warps the LiteSVM `Clock` sysvar to exercise both sides of every deadline: contributing inside the window succeeds, contributing after the deadline fails, refunding before the deadline fails, and refunding after the deadline succeeds when the target was not met. Every failing case asserts the specific program error. -It checks that the claim pays the maker and marks the fundraiser claimed, that a second claim and a contribution after the claim are refused, that direct vault donations do not unlock the claim, and that anyone can refund a contributor or close their Contributor account after a claim, with the tokens and rent going to the contributor. +It checks that the claim pays the maker and marks the fundraiser claimed, that a second claim and a contribution after the claim are refused, that direct vault donations do not unlock the claim, and that anyone can refund a contributor or close their Contribution account after a claim, with the tokens and rent going to the contributor. -`test_stale_contributor_account_cannot_refund_from_next_raise` runs the attack the open-account count exists to stop: a raise succeeds, its Contributor accounts and the fundraiser are closed, the maker starts a second raise at the same address, and a first-raise contributor's refund from the second raise fails while every second-raise contributor gets back exactly what they put in. `test_reinitialize_with_open_contributor_accounts_fails` and `test_close_fundraiser_with_open_contributor_accounts_fails` check that the second raise cannot start while any first-raise Contributor account is open. +`test_stale_contribution_cannot_refund_from_next_raise` runs the attack the open-account count exists to stop: a raise succeeds, its Contribution accounts and the fundraiser are closed, the maker starts a second raise at the same address, and a first-raise contributor's refund from the second raise fails while every second-raise contributor gets back exactly what they put in. `test_reinitialize_with_open_contributions_fails` and `test_close_fundraiser_with_open_contributions_fails` check that the second raise cannot start while any first-raise Contribution account is open. -`close_fundraiser` is tested on both paths (after a failed raise, only after the deadline, only when the target was missed and refunds are complete; after a claim, only once every Contributor account is closed), including that it pays direct donations to the maker and that the same maker can then initialize a fresh fundraiser. Assertions check token balances and decoded account state rather than just transaction success. +`close_fundraiser` is tested on both paths (after a failed raise, only after the deadline, only when the target was missed and refunds are complete; after a claim, only once every Contribution account is closed), including that it pays direct donations to the maker and that the same maker can then initialize a fresh fundraiser. Assertions check token balances and decoded account state rather than just transaction success. ## FAQ @@ -174,9 +174,9 @@ It checks that the claim pays the maker and marks the fundraiser claimed, that a A maker opens a fundraiser with `initialize_fundraiser`, naming the token, target amount, and duration. Contributors deposit with `contribute` while the window is open, and the funds sit in a program-controlled vault that neither side can raid. When the target is reached, the maker claims the raise with `check_contributions`, which pays out the vault and marks the fundraiser claimed. -### What happens to the contributor accounts after a successful raise? +### What happens to the contribution accounts after a successful raise? -Each stays open with its rent inside until someone calls `close_contributor`, which checks that the fundraiser has been claimed and returns the rent to the contributor. Anyone can send it, so the maker can close them all, then call `close_fundraiser` to close the Fundraiser account and the vault. +Each stays open with its rent inside until someone calls `close_contribution`, which checks that the fundraiser has been claimed and returns the rent to the contributor. Anyone can send it, so the maker can close them all, then call `close_fundraiser` to close the Fundraiser account and the vault. ### What happens if the fundraiser misses its target? diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/error.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/error.rs index f386accc6..1d4d50175 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/error.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/error.rs @@ -20,8 +20,10 @@ pub enum FundraiserError { MathOverflow, #[msg("The fundraiser has already been claimed")] FundraiserClaimed, - #[msg("The fundraiser has not been claimed, so the contributor account closes through refund")] + #[msg( + "The fundraiser has not been claimed, so the contribution account closes through refund" + )] FundraiserNotClaimed, - #[msg("Contributor accounts for this fundraiser are still open, so it cannot close yet")] - ContributorAccountsOpen, + #[msg("Contribution accounts for this fundraiser are still open, so it cannot close yet")] + ContributionsOpen, } diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/checker.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/checker.rs index 3b7c3780f..119913a9e 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/checker.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/checker.rs @@ -47,11 +47,11 @@ pub struct CheckContributionsAccountConstraints<'info> { /// Pays the vault out to the maker once the target is met, and marks the /// fundraiser claimed. /// -/// The fundraiser account and the vault stay open: contributor accounts are +/// The fundraiser account and the vault stay open: contribution accounts are /// derived from the fundraiser's address, so the fundraiser must outlive every /// one of them. Otherwise the maker could initialize a new fundraiser at the -/// same address, and contributor accounts left over from this raise would -/// count as contributions to the new one. `close_contributor` closes them, +/// same address, and contribution accounts left over from this raise would +/// count as contributions to the new one. `close_contribution` closes them, /// then `close_fundraiser` closes the fundraiser and the vault. pub fn handle_check_contributions( accounts: &mut CheckContributionsAccountConstraints, diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close.rs index ab371d28c..07b7c33e7 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close.rs @@ -53,8 +53,8 @@ pub struct CloseFundraiserAccountConstraints<'info> { /// /// The fundraiser PDA is derived from the maker's public key alone, so while /// a fundraiser account exists the maker cannot initialize another one. It -/// closes once no contributor account written for it is still open: after a -/// claim, once `close_contributor` has closed each one; after a failed raise, +/// closes once no contribution account written for it is still open: after a +/// claim, once `close_contribution` has closed each one; after a failed raise, /// once the deadline has passed and `refund` has closed each one. pub fn handle_close_fundraiser(accounts: &mut CloseFundraiserAccountConstraints) -> Result<()> { if !accounts.fundraiser.claimed { @@ -85,11 +85,11 @@ pub fn handle_close_fundraiser(accounts: &mut CloseFundraiserAccountConstraints) ); } - // A contributor account left open would be read as a contribution to the + // A contribution account left open would be read as a contribution to the // next fundraiser at this address. require!( - accounts.fundraiser.open_contributor_accounts == 0, - FundraiserError::ContributorAccountsOpen + accounts.fundraiser.open_contributions == 0, + FundraiserError::ContributionsOpen ); // The vault is owned by the fundraiser PDA, so both CPIs are signed with diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close_contributor.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close_contribution.rs similarity index 58% rename from finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close_contributor.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close_contribution.rs index d1d97c749..5ec5672f7 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close_contributor.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/close_contribution.rs @@ -1,14 +1,14 @@ use anchor_lang::prelude::*; use crate::{ - state::{Contributor, Fundraiser}, + state::{Contribution, Fundraiser}, FundraiserError, }; #[derive(Accounts)] -pub struct CloseContributorAccountConstraints<'info> { +pub struct CloseContributionAccountConstraints<'info> { /// Not a signer: the rent goes to the contributor, whoever sends the - /// transaction. So a maker can close every contributor account and then + /// transaction. So a maker can close every contribution account and then /// the fundraiser without waiting on any contributor. #[account(mut)] pub contributor: SystemAccount<'info>, @@ -21,25 +21,25 @@ pub struct CloseContributorAccountConstraints<'info> { #[account( mut, - seeds = [b"contributor", fundraiser.key().as_ref(), contributor.key().as_ref()], - bump = contributor_account.bump, + seeds = [b"contribution", fundraiser.key().as_ref(), contributor.key().as_ref()], + bump = contribution.bump, close = contributor, )] - pub contributor_account: Account<'info, Contributor>, + pub contribution: Account<'info, Contribution>, } -/// Closes a contributor account once its fundraiser has been claimed, +/// Closes a contribution account once its fundraiser has been claimed, /// returning the rent to the contributor. /// -/// `refund` closes contributor accounts on a failed raise. On a successful +/// `refund` closes contribution accounts on a failed raise. On a successful /// one the contribution has been paid out to the maker, so the account only /// holds rent, and `close_fundraiser` cannot run until every one of them is /// closed. While the fundraiser is unclaimed the contribution can still be /// refunded, so this handler refuses with `FundraiserNotClaimed`. -pub fn handle_close_contributor(accounts: &mut CloseContributorAccountConstraints) -> Result<()> { - accounts.fundraiser.open_contributor_accounts = accounts +pub fn handle_close_contribution(accounts: &mut CloseContributionAccountConstraints) -> Result<()> { + accounts.fundraiser.open_contributions = accounts .fundraiser - .open_contributor_accounts + .open_contributions .checked_sub(1) .ok_or(FundraiserError::MathOverflow)?; diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/contribute.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/contribute.rs index 4f2f76aed..5ab15b306 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/contribute.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/contribute.rs @@ -4,7 +4,7 @@ use anchor_spl::token_interface::{ }; use crate::{ - state::{Contributor, Fundraiser}, + state::{Contribution, Fundraiser}, FundraiserError, SECONDS_TO_DAYS, }; @@ -26,11 +26,11 @@ pub struct ContributeAccountConstraints<'info> { #[account( init_if_needed, payer = contributor, - seeds = [b"contributor", fundraiser.key().as_ref(), contributor.key().as_ref()], + seeds = [b"contribution", fundraiser.key().as_ref(), contributor.key().as_ref()], bump, - space = Contributor::DISCRIMINATOR.len() + Contributor::INIT_SPACE, + space = Contribution::DISCRIMINATOR.len() + Contribution::INIT_SPACE, )] - pub contributor_account: Account<'info, Contributor>, + pub contribution: Account<'info, Contribution>, #[account( mut, @@ -86,7 +86,7 @@ pub fn handle_contribute( ); let cumulative_contribution = accounts - .contributor_account + .contribution .amount .checked_add(amount) .ok_or(FundraiserError::MathOverflow)?; @@ -97,16 +97,16 @@ pub fn handle_contribute( .current_amount .checked_add(amount) .ok_or(FundraiserError::MathOverflow)?; - accounts.contributor_account.amount = cumulative_contribution; + accounts.contribution.amount = cumulative_contribution; // On first init (init_if_needed only runs the init branch once; the // stored bump is zero until set), save the contributor PDA bump and count - // the new contributor account against the fundraiser. - if accounts.contributor_account.bump == 0 { - accounts.contributor_account.bump = bumps.contributor_account; - accounts.fundraiser.open_contributor_accounts = accounts + // the new contribution account against the fundraiser. + if accounts.contribution.bump == 0 { + accounts.contribution.bump = bumps.contribution; + accounts.fundraiser.open_contributions = accounts .fundraiser - .open_contributor_accounts + .open_contributions .checked_add(1) .ok_or(FundraiserError::MathOverflow)?; } diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/initialize_fundraiser.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/initialize_fundraiser.rs index e8d651387..234ef68de 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/initialize_fundraiser.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/initialize_fundraiser.rs @@ -65,7 +65,7 @@ pub fn handle_initialize_fundraiser( time_started: Clock::get()?.unix_timestamp, duration, claimed: false, - open_contributor_accounts: 0, + open_contributions: 0, bump: bumps.fundraiser, }); diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/mod.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/mod.rs index b188ee75d..6a7da59a2 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/mod.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/mod.rs @@ -1,13 +1,13 @@ pub mod checker; pub mod close; -pub mod close_contributor; +pub mod close_contribution; pub mod contribute; pub mod initialize_fundraiser; pub mod refund; pub use checker::*; pub use close::*; -pub use close_contributor::*; +pub use close_contribution::*; pub use contribute::*; pub use initialize_fundraiser::*; pub use refund::*; diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/refund.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/refund.rs index 626afe7d2..57a40df3a 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/refund.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/instructions/refund.rs @@ -4,7 +4,7 @@ use anchor_spl::token_interface::{ }; use crate::{ - state::{Contributor, Fundraiser}, + state::{Contribution, Fundraiser}, FundraiserError, SECONDS_TO_DAYS, }; @@ -31,11 +31,11 @@ pub struct RefundAccountConstraints<'info> { #[account( mut, - seeds = [b"contributor", fundraiser.key().as_ref(), contributor.key().as_ref()], - bump = contributor_account.bump, + seeds = [b"contribution", fundraiser.key().as_ref(), contributor.key().as_ref()], + bump = contribution.bump, close = contributor, )] - pub contributor_account: Account<'info, Contributor>, + pub contribution: Account<'info, Contribution>, #[account( mut, @@ -81,16 +81,16 @@ pub fn handle_refund(accounts: &mut RefundAccountConstraints) -> Result<()> { ); // Checks-effects-interactions: update state before the transfer CPI. - let refund_amount = accounts.contributor_account.amount; + let refund_amount = accounts.contribution.amount; accounts.fundraiser.current_amount = accounts .fundraiser .current_amount .checked_sub(refund_amount) .ok_or(FundraiserError::MathOverflow)?; - accounts.contributor_account.amount = 0; - accounts.fundraiser.open_contributor_accounts = accounts + accounts.contribution.amount = 0; + accounts.fundraiser.open_contributions = accounts .fundraiser - .open_contributor_accounts + .open_contributions .checked_sub(1) .ok_or(FundraiserError::MathOverflow)?; diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/lib.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/lib.rs index 0c01c929f..c753d3eb4 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/lib.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/lib.rs @@ -54,10 +54,10 @@ pub mod fundraiser { Ok(()) } - pub fn close_contributor( - mut context: Context, + pub fn close_contribution( + mut context: Context, ) -> Result<()> { - handle_close_contributor(&mut context.accounts)?; + handle_close_contribution(&mut context.accounts)?; Ok(()) } diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/contributor.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/contribution.rs similarity index 84% rename from finance/fundraiser/anchor-v1/programs/fundraiser/src/state/contributor.rs rename to finance/fundraiser/anchor-v1/programs/fundraiser/src/state/contribution.rs index 1fa309b19..cef124fd7 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/contributor.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/contribution.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; #[account] #[derive(InitSpace)] -pub struct Contributor { +pub struct Contribution { pub amount: u64, /// Canonical bump for this PDA. pub bump: u8, diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/fundraiser.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/fundraiser.rs index bc5a458bf..756570d1d 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/fundraiser.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/fundraiser.rs @@ -11,11 +11,11 @@ pub struct Fundraiser { pub duration: u16, /// Set by `check_contributions`. A claimed fundraiser accepts no more /// contributions and no second claim, and stays open until every - /// contributor account written for it has been closed. + /// contribution account written for it has been closed. pub claimed: bool, - /// How many contributor accounts written for this fundraiser are still + /// How many contribution accounts written for this fundraiser are still /// open. `close_fundraiser` requires zero, so a new fundraiser at the - /// same address never starts with contributor accounts from an old one. - pub open_contributor_accounts: u32, + /// same address never starts with contribution accounts from an old one. + pub open_contributions: u32, pub bump: u8, } diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/mod.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/mod.rs index d4c1b8235..80a262cb3 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/mod.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/src/state/mod.rs @@ -1,5 +1,5 @@ -pub mod contributor; +pub mod contribution; pub mod fundraiser; -pub use contributor::*; +pub use contribution::*; pub use fundraiser::*; diff --git a/finance/fundraiser/anchor-v1/programs/fundraiser/tests/test_fundraiser.rs b/finance/fundraiser/anchor-v1/programs/fundraiser/tests/test_fundraiser.rs index 7022d5106..791617c5b 100644 --- a/finance/fundraiser/anchor-v1/programs/fundraiser/tests/test_fundraiser.rs +++ b/finance/fundraiser/anchor-v1/programs/fundraiser/tests/test_fundraiser.rs @@ -60,13 +60,13 @@ struct FundraiserState { _time_started: i64, duration: u16, claimed: bool, - open_contributor_accounts: u32, + open_contributions: u32, _bump: u8, } -/// Mirror of the onchain Contributor struct. +/// Mirror of the onchain Contribution struct. #[derive(BorshDeserialize)] -struct ContributorState { +struct ContributionState { amount: u64, _bump: u8, } @@ -78,9 +78,9 @@ fn read_fundraiser_state(svm: &LiteSVM, fundraiser_pda: &Pubkey) -> FundraiserSt FundraiserState::try_from_slice(&account.data[ANCHOR_DISCRIMINATOR_LENGTH..]).unwrap() } -fn read_contributor_state(svm: &LiteSVM, contributor_pda: &Pubkey) -> ContributorState { - let account = svm.get_account(contributor_pda).unwrap(); - ContributorState::try_from_slice(&account.data[ANCHOR_DISCRIMINATOR_LENGTH..]).unwrap() +fn read_contribution_state(svm: &LiteSVM, contribution_pda: &Pubkey) -> ContributionState { + let account = svm.get_account(contribution_pda).unwrap(); + ContributionState::try_from_slice(&account.data[ANCHOR_DISCRIMINATOR_LENGTH..]).unwrap() } /// Moves the LiteSVM clock forward by the given number of days. @@ -155,7 +155,7 @@ fn initialize_fundraiser(setup: &mut FundraiserSetup, amount: u64, duration: u16 } /// Creates a contributor wallet with a funded ATA and returns -/// (contributor keypair, contributor ATA, contributor account PDA). +/// (contributor keypair, contributor ATA, contribution PDA). fn create_funded_contributor(setup: &mut FundraiserSetup) -> (Keypair, Pubkey, Pubkey) { let contributor = create_wallet(&mut setup.svm, 10_000_000_000).unwrap(); @@ -176,23 +176,23 @@ fn create_funded_contributor(setup: &mut FundraiserSetup) -> (Keypair, Pubkey, P ) .unwrap(); - let (contributor_account_pda, _bump) = Pubkey::find_program_address( + let (contribution_pda, _bump) = Pubkey::find_program_address( &[ - b"contributor", + b"contribution", setup.fundraiser_pda.as_ref(), contributor.pubkey().as_ref(), ], &setup.program_id, ); - (contributor, contributor_ata, contributor_account_pda) + (contributor, contributor_ata, contribution_pda) } fn build_contribute_instruction( setup: &FundraiserSetup, contributor: &Pubkey, contributor_ata: &Pubkey, - contributor_account_pda: &Pubkey, + contribution_pda: &Pubkey, amount: u64, ) -> Instruction { Instruction::new_with_bytes( @@ -202,7 +202,7 @@ fn build_contribute_instruction( contributor: *contributor, mint_to_raise: setup.mint, fundraiser: setup.fundraiser_pda, - contributor_account: *contributor_account_pda, + contribution: *contribution_pda, contributor_ata: *contributor_ata, vault: setup.vault, token_program: token_program_id(), @@ -216,7 +216,7 @@ fn build_refund_instruction( setup: &FundraiserSetup, contributor: &Pubkey, contributor_ata: &Pubkey, - contributor_account_pda: &Pubkey, + contribution_pda: &Pubkey, ) -> Instruction { Instruction::new_with_bytes( setup.program_id, @@ -226,7 +226,7 @@ fn build_refund_instruction( maker: setup.maker.pubkey(), mint_to_raise: setup.mint, fundraiser: setup.fundraiser_pda, - contributor_account: *contributor_account_pda, + contribution: *contribution_pda, contributor_ata: *contributor_ata, vault: setup.vault, token_program: token_program_id(), @@ -257,18 +257,18 @@ fn build_check_contributions_instruction( ) } -fn build_close_contributor_instruction( +fn build_close_contribution_instruction( setup: &FundraiserSetup, contributor: &Pubkey, - contributor_account_pda: &Pubkey, + contribution_pda: &Pubkey, ) -> Instruction { Instruction::new_with_bytes( setup.program_id, - &fundraiser::instruction::CloseContributor {}.data(), - fundraiser::accounts::CloseContributorAccountConstraints { + &fundraiser::instruction::CloseContribution {}.data(), + fundraiser::accounts::CloseContributionAccountConstraints { contributor: *contributor, fundraiser: setup.fundraiser_pda, - contributor_account: *contributor_account_pda, + contribution: *contribution_pda, } .to_account_metas(None), ) @@ -295,7 +295,7 @@ fn build_close_fundraiser_instruction(setup: &FundraiserSetup, maker_ata: &Pubke struct FundedContributor { keypair: Keypair, ata: Pubkey, - contributor_account_pda: Pubkey, + contribution_pda: Pubkey, } /// Sends `contribute` for the given contributor and amount, signed by the @@ -309,7 +309,7 @@ fn contribute( setup, &contributor.keypair.pubkey(), &contributor.ata, - &contributor.contributor_account_pda, + &contributor.contribution_pda, amount, ); send_transaction_from_instructions( @@ -323,11 +323,11 @@ fn contribute( } fn new_contributor(setup: &mut FundraiserSetup) -> FundedContributor { - let (keypair, ata, contributor_account_pda) = create_funded_contributor(setup); + let (keypair, ata, contribution_pda) = create_funded_contributor(setup); FundedContributor { keypair, ata, - contributor_account_pda, + contribution_pda, } } @@ -353,7 +353,7 @@ fn refund( setup, &contributor.keypair.pubkey(), &contributor.ata, - &contributor.contributor_account_pda, + &contributor.contribution_pda, ); send_transaction_from_instructions( &mut setup.svm, @@ -380,17 +380,17 @@ fn claim(setup: &mut FundraiserSetup) -> Result { .map_err(|error| format!("{error:?}")) } -/// Sends `close_contributor` for `contributor`, signed and paid for by +/// Sends `close_contribution` for `contributor`, signed and paid for by /// `fee_payer`. -fn close_contributor( +fn close_contribution( setup: &mut FundraiserSetup, fee_payer: &Keypair, contributor: &FundedContributor, ) -> Result<(), String> { - let close_instruction = build_close_contributor_instruction( + let close_instruction = build_close_contribution_instruction( setup, &contributor.keypair.pubkey(), - &contributor.contributor_account_pda, + &contributor.contribution_pda, ); send_transaction_from_instructions( &mut setup.svm, @@ -451,7 +451,7 @@ fn test_initialize_fundraiser() { assert_eq!(fundraiser_state.current_amount, 0); assert_eq!(fundraiser_state.duration, DURATION_DAYS); assert!(!fundraiser_state.claimed); - assert_eq!(fundraiser_state.open_contributor_accounts, 0); + assert_eq!(fundraiser_state.open_contributions, 0); assert_eq!( get_token_account_balance(&setup.svm, &setup.vault).unwrap(), @@ -518,15 +518,14 @@ fn test_contribute_inside_window_succeeds() { let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert_eq!(fundraiser_state.current_amount, CONTRIBUTION); - assert_eq!(fundraiser_state.open_contributor_accounts, 1); + assert_eq!(fundraiser_state.open_contributions, 1); - let contributor_state = - read_contributor_state(&setup.svm, &contributor.contributor_account_pda); - assert_eq!(contributor_state.amount, CONTRIBUTION); + let contribution_state = read_contribution_state(&setup.svm, &contributor.contribution_pda); + assert_eq!(contribution_state.amount, CONTRIBUTION); } #[test] -fn test_contributions_accumulate_in_one_contributor_account() { +fn test_contributions_accumulate_in_one_contribution() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); let contributor = new_contributor(&mut setup); @@ -537,10 +536,9 @@ fn test_contributions_accumulate_in_one_contributor_account() { warp_days_forward(&mut setup.svm, 1); contribute(&mut setup, &contributor, second_contribution).unwrap(); - let contributor_state = - read_contributor_state(&setup.svm, &contributor.contributor_account_pda); + let contribution_state = read_contribution_state(&setup.svm, &contributor.contribution_pda); assert_eq!( - contributor_state.amount, + contribution_state.amount, first_contribution + second_contribution ); @@ -551,7 +549,7 @@ fn test_contributions_accumulate_in_one_contributor_account() { fundraiser_state.current_amount, first_contribution + second_contribution ); - assert_eq!(fundraiser_state.open_contributor_accounts, 1); + assert_eq!(fundraiser_state.open_contributions, 1); } #[test] @@ -640,14 +638,14 @@ fn test_refund_after_deadline_target_not_met_succeeds() { let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert_eq!(fundraiser_state.current_amount, 0); - assert_eq!(fundraiser_state.open_contributor_accounts, 0); + assert_eq!(fundraiser_state.open_contributions, 0); assert!( setup .svm - .get_account(&contributor.contributor_account_pda) + .get_account(&contributor.contribution_pda) .is_none(), - "Contributor account must be closed after refund" + "Contribution account must be closed after refund" ); } @@ -661,7 +659,7 @@ fn test_anyone_can_refund_a_contributor() { // The maker sends the refund. The tokens and the rent still go to the // contributor, who signs nothing. - let rent = lamports(&setup, &contributor.contributor_account_pda); + let rent = lamports(&setup, &contributor.contribution_pda); let contributor_lamports_before = lamports(&setup, &contributor.keypair.pubkey()); let maker = setup.maker.insecure_clone(); refund(&mut setup, &maker, &contributor).unwrap(); @@ -717,7 +715,7 @@ fn test_check_contributions_success_pays_maker_and_marks_claimed() { let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert!(fundraiser_state.claimed); assert_eq!( - fundraiser_state.open_contributor_accounts, + fundraiser_state.open_contributions, CONTRIBUTIONS_REACHING_TARGET.len() as u32 ); } @@ -790,25 +788,25 @@ fn test_check_contributions_ignores_direct_vault_donations() { } #[test] -fn test_close_contributor_after_claim_returns_rent() { +fn test_close_contribution_after_claim_returns_rent() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); let contributors = fund_to_target(&mut setup); claim(&mut setup).unwrap(); let contributor = &contributors[0]; - let rent = lamports(&setup, &contributor.contributor_account_pda); + let rent = lamports(&setup, &contributor.contribution_pda); let lamports_before = lamports(&setup, &contributor.keypair.pubkey()); let fee_payer = contributor.keypair.insecure_clone(); - close_contributor(&mut setup, &fee_payer, contributor).unwrap(); + close_contribution(&mut setup, &fee_payer, contributor).unwrap(); assert!( setup .svm - .get_account(&contributor.contributor_account_pda) + .get_account(&contributor.contribution_pda) .is_none(), - "Contributor account must be closed" + "Contribution account must be closed" ); // The contributor paid the transaction fee out of the same balance, so // the rent came back less that fee. @@ -818,25 +816,25 @@ fn test_close_contributor_after_claim_returns_rent() { ); let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert_eq!( - fundraiser_state.open_contributor_accounts, + fundraiser_state.open_contributions, CONTRIBUTIONS_REACHING_TARGET.len() as u32 - 1 ); } #[test] -fn test_anyone_can_close_contributor_accounts_after_claim() { +fn test_anyone_can_close_contribution_accounts_after_claim() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); let contributors = fund_to_target(&mut setup); claim(&mut setup).unwrap(); - // The maker closes every contributor account; each rent deposit goes to + // The maker closes every contribution account; each rent deposit goes to // its contributor, who signs nothing. let maker = setup.maker.insecure_clone(); for contributor in &contributors { - let rent = lamports(&setup, &contributor.contributor_account_pda); + let rent = lamports(&setup, &contributor.contribution_pda); let lamports_before = lamports(&setup, &contributor.keypair.pubkey()); - close_contributor(&mut setup, &maker, contributor).unwrap(); + close_contribution(&mut setup, &maker, contributor).unwrap(); assert_eq!( lamports(&setup, &contributor.keypair.pubkey()), lamports_before + rent @@ -844,11 +842,11 @@ fn test_anyone_can_close_contributor_accounts_after_claim() { } let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); - assert_eq!(fundraiser_state.open_contributor_accounts, 0); + assert_eq!(fundraiser_state.open_contributions, 0); } #[test] -fn test_close_contributor_before_claim_fails() { +fn test_close_contribution_before_claim_fails() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); let contributor = new_contributor(&mut setup); @@ -858,12 +856,11 @@ fn test_close_contributor_before_claim_fails() { // refunded: closing the account now would erase what the vault owes. let fee_payer = contributor.keypair.insecure_clone(); assert_error( - close_contributor(&mut setup, &fee_payer, &contributor), + close_contribution(&mut setup, &fee_payer, &contributor), FundraiserError::FundraiserNotClaimed, ); - let contributor_state = - read_contributor_state(&setup.svm, &contributor.contributor_account_pda); - assert_eq!(contributor_state.amount, CONTRIBUTION); + let contribution_state = read_contribution_state(&setup.svm, &contributor.contribution_pda); + assert_eq!(contribution_state.amount, CONTRIBUTION); } #[test] @@ -959,27 +956,27 @@ fn test_close_fundraiser_when_target_met_but_unclaimed_fails() { } #[test] -fn test_close_fundraiser_with_open_contributor_accounts_fails() { +fn test_close_fundraiser_with_open_contributions_fails() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); let contributors = fund_to_target(&mut setup); claim(&mut setup).unwrap(); - // Close all but one contributor account. + // Close all but one contribution account. let maker = setup.maker.insecure_clone(); for contributor in &contributors[1..] { - close_contributor(&mut setup, &maker, contributor).unwrap(); + close_contribution(&mut setup, &maker, contributor).unwrap(); } assert_error( close_fundraiser(&mut setup), - FundraiserError::ContributorAccountsOpen, + FundraiserError::ContributionsOpen, ); assert!(setup.svm.get_account(&setup.fundraiser_pda).is_some()); } #[test] -fn test_reinitialize_with_open_contributor_accounts_fails() { +fn test_reinitialize_with_open_contributions_fails() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); fund_to_target(&mut setup); @@ -1021,7 +1018,7 @@ fn test_reinitialize_with_open_contributor_accounts_fails() { } #[test] -fn test_stale_contributor_account_cannot_refund_from_next_raise() { +fn test_stale_contribution_cannot_refund_from_next_raise() { let mut setup = full_setup(); // Raise one succeeds and the maker claims it. @@ -1029,11 +1026,11 @@ fn test_stale_contributor_account_cannot_refund_from_next_raise() { let first_raise_contributors = fund_to_target(&mut setup); claim(&mut setup).unwrap(); - // The maker closes every contributor account from raise one, then the + // The maker closes every contribution account from raise one, then the // fundraiser, and starts raise two at the same address. let maker = setup.maker.insecure_clone(); for contributor in &first_raise_contributors { - close_contributor(&mut setup, &maker, contributor).unwrap(); + close_contribution(&mut setup, &maker, contributor).unwrap(); } close_fundraiser(&mut setup).unwrap(); setup.svm.expire_blockhash(); @@ -1050,7 +1047,7 @@ fn test_stale_contributor_account_cannot_refund_from_next_raise() { warp_days_forward(&mut setup.svm, DURATION_DAYS as i64); // A raise-one contributor tries to take a refund from raise two. Their - // contributor account was closed with raise one, so there is nothing to + // contribution account was closed with raise one, so there is nothing to // refund. let stale_contributor = &first_raise_contributors[0]; let fee_payer = stale_contributor.keypair.insecure_clone(); @@ -1071,7 +1068,7 @@ fn test_stale_contributor_account_cannot_refund_from_next_raise() { } let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert_eq!(fundraiser_state.current_amount, 0); - assert_eq!(fundraiser_state.open_contributor_accounts, 0); + assert_eq!(fundraiser_state.open_contributions, 0); } #[test] @@ -1083,7 +1080,7 @@ fn test_close_fundraiser_after_claim_allows_a_new_raise() { let maker = setup.maker.insecure_clone(); for contributor in &contributors { - close_contributor(&mut setup, &maker, contributor).unwrap(); + close_contribution(&mut setup, &maker, contributor).unwrap(); } close_fundraiser(&mut setup).unwrap(); assert!(setup.svm.get_account(&setup.fundraiser_pda).is_none()); diff --git a/finance/fundraiser/anchor/CHANGELOG.md b/finance/fundraiser/anchor/CHANGELOG.md index a5da1b256..e23192193 100644 --- a/finance/fundraiser/anchor/CHANGELOG.md +++ b/finance/fundraiser/anchor/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2026-10-03 + +### Changed + +- **The per-contributor account is `Contribution`, closed by `close_contribution`.** The handler named `close_contributor` closed the account recording one contributor's contributions, not the contributor, so it is now `close_contribution` (account constraints `CloseContributionAccountConstraints`). The account struct `Contributor` is now `Contribution`, its handler field `contributor_account` is `contribution`, and its PDA seed prefix is `"contribution"` instead of `"contributor"`. The Fundraiser's `open_contributor_accounts` count is `open_contributions`, and the error `ContributorAccountsOpen` is `ContributionsOpen`. The `contributor` account (the contributor's wallet) keeps its name. No behavior changes. + ## 2026-10-01 ### Fixed diff --git a/finance/fundraiser/anchor/README.md b/finance/fundraiser/anchor/README.md index 30bdf33de..1c69d7ccf 100644 --- a/finance/fundraiser/anchor/README.md +++ b/finance/fundraiser/anchor/README.md @@ -25,7 +25,7 @@ pub struct Fundraiser { pub time_started: i64, pub duration: u16, pub claimed: bool, - pub open_contributor_accounts: u32, + pub open_contributions: u32, pub bump: u8, } ``` @@ -39,7 +39,7 @@ Fields: - `time_started` - when the fundraiser was created. - `duration` - fundraising window in days. - `claimed` - set by `check_contributions`. A claimed fundraiser accepts no more contributions and no second claim. -- `open_contributor_accounts` - how many Contributor accounts written for this fundraiser are still open. `contribute` adds one when it creates a Contributor account; `refund` and `close_contributor` each subtract one when they close one. `close_fundraiser` requires zero. +- `open_contributions` - how many Contribution accounts written for this fundraiser are still open. `contribute` adds one when it creates a Contribution account; `refund` and `close_contribution` each subtract one when they close one. `close_fundraiser` requires zero. - `bump` - canonical bump for the Fundraiser [PDA](https://solana.com/docs/terminology#program-derived-address-pda). The `InitSpace` derive macro implements the `Space` trait, which calculates the size of the account (not counting the [Anchor](https://solana.com/docs/terminology#anchor) discriminator). @@ -49,7 +49,7 @@ A per-contributor record: ```rust #[account(borsh)] #[derive(InitSpace)] -pub struct Contributor { +pub struct Contribution { pub amount: u64, /// Canonical bump for this PDA. pub bump: u8, @@ -57,9 +57,9 @@ pub struct Contributor { ``` - `amount` - total amount contributed by this contributor. -- `bump` - canonical bump for the Contributor PDA. +- `bump` - canonical bump for the Contribution PDA. -The Contributor PDA uses `init_if_needed`, which only runs the init branch on first call. On first init (when `bump == 0`) the handler stores `bumps.contributor_account` into `bump` and adds one to `open_contributor_accounts`; see [`instructions/contribute.rs`](programs/fundraiser/src/instructions/contribute.rs). +The Contribution PDA uses `init_if_needed`, which only runs the init branch on first call. On first init (when `bump == 0`) the handler stores `bumps.contribution` into `bump` and adds one to `open_contributions`; see [`instructions/contribute.rs`](programs/fundraiser/src/instructions/contribute.rs). ### Constants @@ -95,7 +95,7 @@ The maker signs and pays for two new accounts: - `fundraiser` - the state account, derived from `b"fundraiser"` and the maker's public key. Anchor calculates the canonical bump and the handler stores it. - `vault` - the [ATA](https://solana.com/docs/terminology#associated-token-account-ata) that receives contributions, owned by the Fundraiser PDA. -The handler requires `amount >= MIN_AMOUNT_TO_RAISE * 10^decimals` (the target must be at least 3 major units of the mint, expressed in minor units), then initializes the Fundraiser state with `current_amount = 0`, `claimed = false`, `open_contributor_accounts = 0`, and `time_started` from the `Clock` sysvar. A target below the minimum fails with `InvalidAmount`. +The handler requires `amount >= MIN_AMOUNT_TO_RAISE * 10^decimals` (the target must be at least 3 major units of the mint, expressed in minor units), then initializes the Fundraiser state with `current_amount = 0`, `claimed = false`, `open_contributions = 0`, and `time_started` from the `Clock` sysvar. A target below the minimum fails with `InvalidAmount`. ### `contribute` @@ -107,7 +107,7 @@ A contributor signs and the handler performs three checks in order: 2. Not yet claimed: `!claimed`, else `FundraiserClaimed`. The deadline may still be days away after a claim, and the vault has already been paid out. 3. Time window: contributions are allowed while `elapsed_days < duration`, where `elapsed_days = (now - time_started) / SECONDS_TO_DAYS`. Once `elapsed_days` reaches `duration` the handler fails with `FundraiserEnded`. -If all checks pass, `Fundraiser.current_amount` and `Contributor.amount` are updated (a contributor's later contributions add to the same Contributor account), then `amount` is transferred from `contributor_ata` to `vault` with `transfer_checked`. +If all checks pass, `Fundraiser.current_amount` and `Contribution.amount` are updated (a contributor's later contributions add to the same Contribution account), then `amount` is transferred from `contributor_ata` to `vault` with `transfer_checked`. ### `check_contributions` @@ -115,7 +115,7 @@ If all checks pass, `Fundraiser.current_amount` and `Contributor.amount` are upd Lets the maker claim the funds once the target is met. Requires `!claimed`, else `FundraiserClaimed`, and `fundraiser.current_amount >= amount_to_raise` (the state-tracked total, so direct donations to the vault cannot unlock the claim early), else `TargetNotMet`. The handler sets `claimed` and transfers the entire vault balance (including any direct donations) to `maker_ata` with `transfer_checked`, signed with the Fundraiser PDA's seeds. -The Fundraiser account and the empty vault stay open. Contributor accounts are derived from the Fundraiser's address, so the Fundraiser must outlive every one of them: if it closed here, the maker could initialize a new fundraiser at the same address, and the Contributor accounts left over from this raise would count as contributions to the new one, so `refund` would pay their old amounts out of the new contributors' tokens. `close_contributor` closes the Contributor accounts, then `close_fundraiser` closes the Fundraiser and the vault. +The Fundraiser account and the empty vault stay open. Contribution accounts are derived from the Fundraiser's address, so the Fundraiser must outlive every one of them: if it closed here, the maker could initialize a new fundraiser at the same address, and the Contribution accounts left over from this raise would count as contributions to the new one, so `refund` would pay their old amounts out of the new contributors' tokens. `close_contribution` closes the Contribution accounts, then `close_fundraiser` closes the Fundraiser and the vault. ### `refund` @@ -126,7 +126,7 @@ Returns a contribution after a failed fundraiser. The contributor does not have 1. Refunds are allowed only after the fundraiser has ended: `elapsed_days >= duration`, else `FundraiserNotEnded`. 2. The target was not met: `fundraiser.current_amount < amount_to_raise` (again the state-tracked total, so donated tokens cannot block refunds), else `TargetMet`. -The handler subtracts the contributor's recorded amount from `current_amount` zeroes the Contributor record, and subtracts one from `open_contributor_accounts` before the transfer CPI, then sends the tokens from the vault back to `contributor_ata` with `transfer_checked` (PDA signer). The Contributor account is closed via `close = contributor`, refunding its rent to the contributor. +The handler subtracts the contributor's recorded amount from `current_amount` zeroes the Contribution record, and subtracts one from `open_contributions` before the transfer CPI, then sends the tokens from the vault back to `contributor_ata` with `transfer_checked` (PDA signer). The Contribution account is closed via `close = contributor`, refunding its rent to the contributor. ### `close_fundraiser` @@ -140,17 +140,17 @@ For an unclaimed fundraiser, three checks: 2. The target was not met: `fundraiser.current_amount < amount_to_raise`, else `TargetMet` (a raise that met its target closes after the maker claims it). 3. Every contribution has been refunded: `fundraiser.current_amount == 0`, else `RefundsOutstanding` (closing the vault earlier would strand the remaining refunds). -For every fundraiser, claimed or not: `open_contributor_accounts == 0`, else `ContributorAccountsOpen`. A Contributor account left open would be read as a contribution to the next fundraiser at this address. +For every fundraiser, claimed or not: `open_contributions == 0`, else `ContributionsOpen`. A Contribution account left open would be read as a contribution to the next fundraiser at this address. Anything still in the vault at this point is a direct donation outside the program's accounting; the handler pays it to `maker_ata` with `transfer_checked` rather than burning it, then closes the vault with `close_account` (both CPIs signed with the Fundraiser PDA's seeds). The Fundraiser state account is closed via `close = maker`. -### `close_contributor` +### `close_contribution` -[`programs/fundraiser/src/instructions/close_contributor.rs`](programs/fundraiser/src/instructions/close_contributor.rs), account constraints `CloseContributorAccountConstraints`. +[`programs/fundraiser/src/instructions/close_contribution.rs`](programs/fundraiser/src/instructions/close_contribution.rs), account constraints `CloseContributionAccountConstraints`. -Closes a Contributor account once its fundraiser has been claimed, returning the rent to the contributor. On a successful raise the contribution has been paid out to the maker, so the account holds only rent, and `close_fundraiser` cannot run until every one of them is closed. +Closes a Contribution account once its fundraiser has been claimed, returning the rent to the contributor. On a successful raise the contribution has been paid out to the maker, so the account holds only rent, and `close_fundraiser` cannot run until every one of them is closed. -One check: `fundraiser.claimed`, else `FundraiserNotClaimed`. While the fundraiser is unclaimed the contribution can still be refunded, so `refund` is the way to close it. The contributor does not have to sign: the rent goes to them whoever sends the transaction, so the maker can close every Contributor account and then the fundraiser. The Contributor account's seeds bind it to the fundraiser's address, and the handler subtracts one from `open_contributor_accounts`. The account is closed via `close = contributor`. +One check: `fundraiser.claimed`, else `FundraiserNotClaimed`. While the fundraiser is unclaimed the contribution can still be refunded, so `refund` is the way to close it. The contributor does not have to sign: the rent goes to them whoever sends the transaction, so the maker can close every Contribution account and then the fundraiser. The Contribution account's seeds bind it to the fundraiser's address, and the handler subtracts one from `open_contributions`. The account is closed via `close = contributor`. ## Testing @@ -163,11 +163,11 @@ cargo test The suite uses a nonzero duration and warps the LiteSVM `Clock` sysvar to exercise both sides of every deadline: contributing inside the window succeeds, contributing after the deadline fails, refunding before the deadline fails, and refunding after the deadline succeeds when the target was not met. Every failing case asserts the specific program error. -It checks that the claim pays the maker and marks the fundraiser claimed, that a second claim and a contribution after the claim are refused, that direct vault donations do not unlock the claim, and that anyone can refund a contributor or close their Contributor account after a claim, with the tokens and rent going to the contributor. +It checks that the claim pays the maker and marks the fundraiser claimed, that a second claim and a contribution after the claim are refused, that direct vault donations do not unlock the claim, and that anyone can refund a contributor or close their Contribution account after a claim, with the tokens and rent going to the contributor. -`test_stale_contributor_account_cannot_refund_from_next_raise` runs the attack the open-account count exists to stop: a raise succeeds, its Contributor accounts and the fundraiser are closed, the maker starts a second raise at the same address, and a first-raise contributor's refund from the second raise fails while every second-raise contributor gets back exactly what they put in. `test_reinitialize_with_open_contributor_accounts_fails` and `test_close_fundraiser_with_open_contributor_accounts_fails` check that the second raise cannot start while any first-raise Contributor account is open. +`test_stale_contribution_cannot_refund_from_next_raise` runs the attack the open-account count exists to stop: a raise succeeds, its Contribution accounts and the fundraiser are closed, the maker starts a second raise at the same address, and a first-raise contributor's refund from the second raise fails while every second-raise contributor gets back exactly what they put in. `test_reinitialize_with_open_contributions_fails` and `test_close_fundraiser_with_open_contributions_fails` check that the second raise cannot start while any first-raise Contribution account is open. -`close_fundraiser` is tested on both paths (after a failed raise, only after the deadline, only when the target was missed and refunds are complete; after a claim, only once every Contributor account is closed), including that it pays direct donations to the maker and that the same maker can then initialize a fresh fundraiser. Assertions check token balances and decoded account state rather than just transaction success. +`close_fundraiser` is tested on both paths (after a failed raise, only after the deadline, only when the target was missed and refunds are complete; after a claim, only once every Contribution account is closed), including that it pays direct donations to the maker and that the same maker can then initialize a fresh fundraiser. Assertions check token balances and decoded account state rather than just transaction success. ## FAQ @@ -175,9 +175,9 @@ It checks that the claim pays the maker and marks the fundraiser claimed, that a A maker opens a fundraiser with `initialize_fundraiser`, naming the token, target amount, and duration. Contributors deposit with `contribute` while the window is open, and the funds sit in a program-controlled vault that neither side can raid. When the target is reached, the maker claims the raise with `check_contributions`, which pays out the vault and marks the fundraiser claimed. -### What happens to the contributor accounts after a successful raise? +### What happens to the contribution accounts after a successful raise? -Each stays open with its rent inside until someone calls `close_contributor`, which checks that the fundraiser has been claimed and returns the rent to the contributor. Anyone can send it, so the maker can close them all, then call `close_fundraiser` to close the Fundraiser account and the vault. +Each stays open with its rent inside until someone calls `close_contribution`, which checks that the fundraiser has been claimed and returns the rent to the contributor. Anyone can send it, so the maker can close them all, then call `close_fundraiser` to close the Fundraiser account and the vault. ### What happens if the fundraiser misses its target? diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/error.rs b/finance/fundraiser/anchor/programs/fundraiser/src/error.rs index f386accc6..1d4d50175 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/error.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/error.rs @@ -20,8 +20,10 @@ pub enum FundraiserError { MathOverflow, #[msg("The fundraiser has already been claimed")] FundraiserClaimed, - #[msg("The fundraiser has not been claimed, so the contributor account closes through refund")] + #[msg( + "The fundraiser has not been claimed, so the contribution account closes through refund" + )] FundraiserNotClaimed, - #[msg("Contributor accounts for this fundraiser are still open, so it cannot close yet")] - ContributorAccountsOpen, + #[msg("Contribution accounts for this fundraiser are still open, so it cannot close yet")] + ContributionsOpen, } diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs index 0d58eabc4..79b7ae0da 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/checker.rs @@ -47,11 +47,11 @@ pub struct CheckContributionsAccountConstraints { /// Pays the vault out to the maker once the target is met, and marks the /// fundraiser claimed. /// -/// The fundraiser account and the vault stay open: contributor accounts are +/// The fundraiser account and the vault stay open: contribution accounts are /// derived from the fundraiser's address, so the fundraiser must outlive every /// one of them. Otherwise the maker could initialize a new fundraiser at the -/// same address, and contributor accounts left over from this raise would -/// count as contributions to the new one. `close_contributor` closes them, +/// same address, and contribution accounts left over from this raise would +/// count as contributions to the new one. `close_contribution` closes them, /// then `close_fundraiser` closes the fundraiser and the vault. pub fn handle_check_contributions( accounts: &mut CheckContributionsAccountConstraints, diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/close.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/close.rs index cf7b0ab97..2e452e77f 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/close.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/close.rs @@ -53,8 +53,8 @@ pub struct CloseFundraiserAccountConstraints { /// /// The fundraiser PDA is derived from the maker's public key alone, so while /// a fundraiser account exists the maker cannot initialize another one. It -/// closes once no contributor account written for it is still open: after a -/// claim, once `close_contributor` has closed each one; after a failed raise, +/// closes once no contribution account written for it is still open: after a +/// claim, once `close_contribution` has closed each one; after a failed raise, /// once the deadline has passed and `refund` has closed each one. pub fn handle_close_fundraiser(accounts: &mut CloseFundraiserAccountConstraints) -> Result<()> { if !accounts.fundraiser.claimed { @@ -85,11 +85,11 @@ pub fn handle_close_fundraiser(accounts: &mut CloseFundraiserAccountConstraints) ); } - // A contributor account left open would be read as a contribution to the + // A contribution account left open would be read as a contribution to the // next fundraiser at this address. require!( - accounts.fundraiser.open_contributor_accounts == 0, - FundraiserError::ContributorAccountsOpen + accounts.fundraiser.open_contributions == 0, + FundraiserError::ContributionsOpen ); // Read these before any of the CPI handles below take their borrows. diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/close_contributor.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/close_contribution.rs similarity index 57% rename from finance/fundraiser/anchor/programs/fundraiser/src/instructions/close_contributor.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/instructions/close_contribution.rs index 20121d869..08c48bd25 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/close_contributor.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/close_contribution.rs @@ -1,14 +1,14 @@ use anchor_lang::prelude::*; use crate::{ - state::{Contributor, Fundraiser}, + state::{Contribution, Fundraiser}, FundraiserError, }; #[derive(Accounts)] -pub struct CloseContributorAccountConstraints { +pub struct CloseContributionAccountConstraints { /// Not a signer: the rent goes to the contributor, whoever sends the - /// transaction. So a maker can close every contributor account and then + /// transaction. So a maker can close every contribution account and then /// the fundraiser without waiting on any contributor. #[account(mut)] pub contributor: SystemAccount, @@ -21,25 +21,25 @@ pub struct CloseContributorAccountConstraints { #[account( mut, - seeds = [b"contributor", fundraiser.address().as_ref(), contributor.address().as_ref()], - bump = contributor_account.bump, + seeds = [b"contribution", fundraiser.address().as_ref(), contributor.address().as_ref()], + bump = contribution.bump, close = contributor, )] - pub contributor_account: BorshAccount, + pub contribution: BorshAccount, } -/// Closes a contributor account once its fundraiser has been claimed, +/// Closes a contribution account once its fundraiser has been claimed, /// returning the rent to the contributor. /// -/// `refund` closes contributor accounts on a failed raise. On a successful +/// `refund` closes contribution accounts on a failed raise. On a successful /// one the contribution has been paid out to the maker, so the account only /// holds rent, and `close_fundraiser` cannot run until every one of them is /// closed. While the fundraiser is unclaimed the contribution can still be /// refunded, so this handler refuses with `FundraiserNotClaimed`. -pub fn handle_close_contributor(accounts: &mut CloseContributorAccountConstraints) -> Result<()> { - accounts.fundraiser.open_contributor_accounts = accounts +pub fn handle_close_contribution(accounts: &mut CloseContributionAccountConstraints) -> Result<()> { + accounts.fundraiser.open_contributions = accounts .fundraiser - .open_contributor_accounts + .open_contributions .checked_sub(1) .ok_or(FundraiserError::MathOverflow)?; diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs index 6713235d0..8a6cdd251 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/contribute.rs @@ -4,7 +4,7 @@ use anchor_spl::token_interface::{ }; use crate::{ - state::{Contributor, Fundraiser}, + state::{Contribution, Fundraiser}, FundraiserError, SECONDS_TO_DAYS, }; @@ -26,11 +26,11 @@ pub struct ContributeAccountConstraints { #[account( init_if_needed, payer = contributor, - seeds = [b"contributor", fundraiser.address().as_ref(), contributor.address().as_ref()], + seeds = [b"contribution", fundraiser.address().as_ref(), contributor.address().as_ref()], bump, - space = Contributor::DISCRIMINATOR.len() + Contributor::INIT_SPACE, + space = Contribution::DISCRIMINATOR.len() + Contribution::INIT_SPACE, )] - pub contributor_account: BorshAccount, + pub contribution: BorshAccount, #[account( mut, @@ -86,7 +86,7 @@ pub fn handle_contribute( ); let cumulative_contribution = accounts - .contributor_account + .contribution .amount .checked_add(amount) .ok_or(FundraiserError::MathOverflow)?; @@ -97,16 +97,16 @@ pub fn handle_contribute( .current_amount .checked_add(amount) .ok_or(FundraiserError::MathOverflow)?; - accounts.contributor_account.amount = cumulative_contribution; + accounts.contribution.amount = cumulative_contribution; // On first init (init_if_needed only runs the init branch once; the // stored bump is zero until set), save the contributor PDA bump and count - // the new contributor account against the fundraiser. - if accounts.contributor_account.bump == 0 { - accounts.contributor_account.bump = bumps.contributor_account; - accounts.fundraiser.open_contributor_accounts = accounts + // the new contribution account against the fundraiser. + if accounts.contribution.bump == 0 { + accounts.contribution.bump = bumps.contribution; + accounts.fundraiser.open_contributions = accounts .fundraiser - .open_contributor_accounts + .open_contributions .checked_add(1) .ok_or(FundraiserError::MathOverflow)?; } diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs index 17a35dae9..de2fe5ad4 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/initialize_fundraiser.rs @@ -65,7 +65,7 @@ pub fn handle_initialize_fundraiser( time_started: Clock::get()?.unix_timestamp, duration, claimed: false, - open_contributor_accounts: 0, + open_contributions: 0, bump: bumps.fundraiser, }; diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs index b188ee75d..6a7da59a2 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/mod.rs @@ -1,13 +1,13 @@ pub mod checker; pub mod close; -pub mod close_contributor; +pub mod close_contribution; pub mod contribute; pub mod initialize_fundraiser; pub mod refund; pub use checker::*; pub use close::*; -pub use close_contributor::*; +pub use close_contribution::*; pub use contribute::*; pub use initialize_fundraiser::*; pub use refund::*; diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs index 42f4150a6..6d16e6fed 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/instructions/refund.rs @@ -4,7 +4,7 @@ use anchor_spl::token_interface::{ }; use crate::{ - state::{Contributor, Fundraiser}, + state::{Contribution, Fundraiser}, FundraiserError, SECONDS_TO_DAYS, }; @@ -31,11 +31,11 @@ pub struct RefundAccountConstraints { #[account( mut, - seeds = [b"contributor", fundraiser.address().as_ref(), contributor.address().as_ref()], - bump = contributor_account.bump, + seeds = [b"contribution", fundraiser.address().as_ref(), contributor.address().as_ref()], + bump = contribution.bump, close = contributor, )] - pub contributor_account: BorshAccount, + pub contribution: BorshAccount, #[account( mut, @@ -81,16 +81,16 @@ pub fn handle_refund(accounts: &mut RefundAccountConstraints) -> Result<()> { ); // Checks-effects-interactions: update state before the transfer CPI. - let refund_amount = accounts.contributor_account.amount; + let refund_amount = accounts.contribution.amount; accounts.fundraiser.current_amount = accounts .fundraiser .current_amount .checked_sub(refund_amount) .ok_or(FundraiserError::MathOverflow)?; - accounts.contributor_account.amount = 0; - accounts.fundraiser.open_contributor_accounts = accounts + accounts.contribution.amount = 0; + accounts.fundraiser.open_contributions = accounts .fundraiser - .open_contributor_accounts + .open_contributions .checked_sub(1) .ok_or(FundraiserError::MathOverflow)?; diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/lib.rs b/finance/fundraiser/anchor/programs/fundraiser/src/lib.rs index bd0c8c5c4..34616a238 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/lib.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/lib.rs @@ -56,10 +56,10 @@ pub mod fundraiser { Ok(()) } - pub fn close_contributor( - context: &mut Context, + pub fn close_contribution( + context: &mut Context, ) -> Result<()> { - handle_close_contributor(&mut context.accounts)?; + handle_close_contribution(&mut context.accounts)?; Ok(()) } diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/state/contributor.rs b/finance/fundraiser/anchor/programs/fundraiser/src/state/contribution.rs similarity index 84% rename from finance/fundraiser/anchor/programs/fundraiser/src/state/contributor.rs rename to finance/fundraiser/anchor/programs/fundraiser/src/state/contribution.rs index 1003a50ba..a9fa4f399 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/state/contributor.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/state/contribution.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; #[account(borsh)] #[derive(InitSpace)] -pub struct Contributor { +pub struct Contribution { pub amount: u64, /// Canonical bump for this PDA. pub bump: u8, diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs b/finance/fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs index 34e818392..a145a5698 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/state/fundraiser.rs @@ -11,11 +11,11 @@ pub struct Fundraiser { pub duration: u16, /// Set by `check_contributions`. A claimed fundraiser accepts no more /// contributions and no second claim, and stays open until every - /// contributor account written for it has been closed. + /// contribution account written for it has been closed. pub claimed: bool, - /// How many contributor accounts written for this fundraiser are still + /// How many contribution accounts written for this fundraiser are still /// open. `close_fundraiser` requires zero, so a new fundraiser at the - /// same address never starts with contributor accounts from an old one. - pub open_contributor_accounts: u32, + /// same address never starts with contribution accounts from an old one. + pub open_contributions: u32, pub bump: u8, } diff --git a/finance/fundraiser/anchor/programs/fundraiser/src/state/mod.rs b/finance/fundraiser/anchor/programs/fundraiser/src/state/mod.rs index d4c1b8235..80a262cb3 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/src/state/mod.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/src/state/mod.rs @@ -1,5 +1,5 @@ -pub mod contributor; +pub mod contribution; pub mod fundraiser; -pub use contributor::*; +pub use contribution::*; pub use fundraiser::*; diff --git a/finance/fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs b/finance/fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs index f9fd00e0e..a298d012e 100644 --- a/finance/fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs +++ b/finance/fundraiser/anchor/programs/fundraiser/tests/test_fundraiser.rs @@ -60,13 +60,13 @@ struct FundraiserState { _time_started: i64, duration: u16, claimed: bool, - open_contributor_accounts: u32, + open_contributions: u32, _bump: u8, } -/// Mirror of the onchain Contributor struct. +/// Mirror of the onchain Contribution struct. #[derive(BorshDeserialize)] -struct ContributorState { +struct ContributionState { amount: u64, _bump: u8, } @@ -78,9 +78,9 @@ fn read_fundraiser_state(svm: &LiteSVM, fundraiser_pda: &Address) -> FundraiserS FundraiserState::try_from_slice(&account.data[ANCHOR_DISCRIMINATOR_LENGTH..]).unwrap() } -fn read_contributor_state(svm: &LiteSVM, contributor_pda: &Address) -> ContributorState { - let account = svm.get_account(contributor_pda).unwrap(); - ContributorState::try_from_slice(&account.data[ANCHOR_DISCRIMINATOR_LENGTH..]).unwrap() +fn read_contribution_state(svm: &LiteSVM, contribution_pda: &Address) -> ContributionState { + let account = svm.get_account(contribution_pda).unwrap(); + ContributionState::try_from_slice(&account.data[ANCHOR_DISCRIMINATOR_LENGTH..]).unwrap() } /// Moves the LiteSVM clock forward by the given number of days. @@ -155,7 +155,7 @@ fn initialize_fundraiser(setup: &mut FundraiserSetup, amount: u64, duration: u16 } /// Creates a contributor wallet with a funded ATA and returns -/// (contributor keypair, contributor ATA, contributor account PDA). +/// (contributor keypair, contributor ATA, contribution PDA). fn create_funded_contributor(setup: &mut FundraiserSetup) -> (Keypair, Address, Address) { let contributor = create_wallet(&mut setup.svm, 10_000_000_000).unwrap(); @@ -176,23 +176,23 @@ fn create_funded_contributor(setup: &mut FundraiserSetup) -> (Keypair, Address, ) .unwrap(); - let (contributor_account_pda, _bump) = Address::find_program_address( + let (contribution_pda, _bump) = Address::find_program_address( &[ - b"contributor", + b"contribution", setup.fundraiser_pda.as_ref(), contributor.pubkey().as_ref(), ], &setup.program_id, ); - (contributor, contributor_ata, contributor_account_pda) + (contributor, contributor_ata, contribution_pda) } fn build_contribute_instruction( setup: &FundraiserSetup, contributor: &Address, contributor_ata: &Address, - contributor_account_pda: &Address, + contribution_pda: &Address, amount: u64, ) -> Instruction { Instruction::new_with_bytes( @@ -202,7 +202,7 @@ fn build_contribute_instruction( contributor: *contributor, mint_to_raise: setup.mint, fundraiser: setup.fundraiser_pda, - contributor_account: *contributor_account_pda, + contribution: *contribution_pda, contributor_ata: *contributor_ata, vault: setup.vault, token_program: token_program_id(), @@ -216,7 +216,7 @@ fn build_refund_instruction( setup: &FundraiserSetup, contributor: &Address, contributor_ata: &Address, - contributor_account_pda: &Address, + contribution_pda: &Address, ) -> Instruction { Instruction::new_with_bytes( setup.program_id, @@ -226,7 +226,7 @@ fn build_refund_instruction( maker: setup.maker.pubkey(), mint_to_raise: setup.mint, fundraiser: setup.fundraiser_pda, - contributor_account: *contributor_account_pda, + contribution: *contribution_pda, contributor_ata: *contributor_ata, vault: setup.vault, token_program: token_program_id(), @@ -257,18 +257,18 @@ fn build_check_contributions_instruction( ) } -fn build_close_contributor_instruction( +fn build_close_contribution_instruction( setup: &FundraiserSetup, contributor: &Address, - contributor_account_pda: &Address, + contribution_pda: &Address, ) -> Instruction { Instruction::new_with_bytes( setup.program_id, - &fundraiser::instruction::CloseContributor {}.data(), - fundraiser::accounts::CloseContributorAccountConstraints { + &fundraiser::instruction::CloseContribution {}.data(), + fundraiser::accounts::CloseContributionAccountConstraints { contributor: *contributor, fundraiser: setup.fundraiser_pda, - contributor_account: *contributor_account_pda, + contribution: *contribution_pda, } .to_account_metas(None), ) @@ -295,7 +295,7 @@ fn build_close_fundraiser_instruction(setup: &FundraiserSetup, maker_ata: &Addre struct FundedContributor { keypair: Keypair, ata: Address, - contributor_account_pda: Address, + contribution_pda: Address, } /// Sends `contribute` for the given contributor and amount, signed by the @@ -309,7 +309,7 @@ fn contribute( setup, &contributor.keypair.pubkey(), &contributor.ata, - &contributor.contributor_account_pda, + &contributor.contribution_pda, amount, ); send_transaction_from_instructions( @@ -323,11 +323,11 @@ fn contribute( } fn new_contributor(setup: &mut FundraiserSetup) -> FundedContributor { - let (keypair, ata, contributor_account_pda) = create_funded_contributor(setup); + let (keypair, ata, contribution_pda) = create_funded_contributor(setup); FundedContributor { keypair, ata, - contributor_account_pda, + contribution_pda, } } @@ -353,7 +353,7 @@ fn refund( setup, &contributor.keypair.pubkey(), &contributor.ata, - &contributor.contributor_account_pda, + &contributor.contribution_pda, ); send_transaction_from_instructions( &mut setup.svm, @@ -380,17 +380,17 @@ fn claim(setup: &mut FundraiserSetup) -> Result { .map_err(|error| format!("{error:?}")) } -/// Sends `close_contributor` for `contributor`, signed and paid for by +/// Sends `close_contribution` for `contributor`, signed and paid for by /// `fee_payer`. -fn close_contributor( +fn close_contribution( setup: &mut FundraiserSetup, fee_payer: &Keypair, contributor: &FundedContributor, ) -> Result<(), String> { - let close_instruction = build_close_contributor_instruction( + let close_instruction = build_close_contribution_instruction( setup, &contributor.keypair.pubkey(), - &contributor.contributor_account_pda, + &contributor.contribution_pda, ); send_transaction_from_instructions( &mut setup.svm, @@ -451,7 +451,7 @@ fn test_initialize_fundraiser() { assert_eq!(fundraiser_state.current_amount, 0); assert_eq!(fundraiser_state.duration, DURATION_DAYS); assert!(!fundraiser_state.claimed); - assert_eq!(fundraiser_state.open_contributor_accounts, 0); + assert_eq!(fundraiser_state.open_contributions, 0); assert_eq!( get_token_account_balance(&setup.svm, &setup.vault).unwrap(), @@ -518,15 +518,14 @@ fn test_contribute_inside_window_succeeds() { let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert_eq!(fundraiser_state.current_amount, CONTRIBUTION); - assert_eq!(fundraiser_state.open_contributor_accounts, 1); + assert_eq!(fundraiser_state.open_contributions, 1); - let contributor_state = - read_contributor_state(&setup.svm, &contributor.contributor_account_pda); - assert_eq!(contributor_state.amount, CONTRIBUTION); + let contribution_state = read_contribution_state(&setup.svm, &contributor.contribution_pda); + assert_eq!(contribution_state.amount, CONTRIBUTION); } #[test] -fn test_contributions_accumulate_in_one_contributor_account() { +fn test_contributions_accumulate_in_one_contribution() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); let contributor = new_contributor(&mut setup); @@ -537,10 +536,9 @@ fn test_contributions_accumulate_in_one_contributor_account() { warp_days_forward(&mut setup.svm, 1); contribute(&mut setup, &contributor, second_contribution).unwrap(); - let contributor_state = - read_contributor_state(&setup.svm, &contributor.contributor_account_pda); + let contribution_state = read_contribution_state(&setup.svm, &contributor.contribution_pda); assert_eq!( - contributor_state.amount, + contribution_state.amount, first_contribution + second_contribution ); @@ -551,7 +549,7 @@ fn test_contributions_accumulate_in_one_contributor_account() { fundraiser_state.current_amount, first_contribution + second_contribution ); - assert_eq!(fundraiser_state.open_contributor_accounts, 1); + assert_eq!(fundraiser_state.open_contributions, 1); } #[test] @@ -640,14 +638,14 @@ fn test_refund_after_deadline_target_not_met_succeeds() { let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert_eq!(fundraiser_state.current_amount, 0); - assert_eq!(fundraiser_state.open_contributor_accounts, 0); + assert_eq!(fundraiser_state.open_contributions, 0); assert!( setup .svm - .get_account(&contributor.contributor_account_pda) + .get_account(&contributor.contribution_pda) .is_none(), - "Contributor account must be closed after refund" + "Contribution account must be closed after refund" ); } @@ -661,7 +659,7 @@ fn test_anyone_can_refund_a_contributor() { // The maker sends the refund. The tokens and the rent still go to the // contributor, who signs nothing. - let rent = lamports(&setup, &contributor.contributor_account_pda); + let rent = lamports(&setup, &contributor.contribution_pda); let contributor_lamports_before = lamports(&setup, &contributor.keypair.pubkey()); let maker = setup.maker.insecure_clone(); refund(&mut setup, &maker, &contributor).unwrap(); @@ -717,7 +715,7 @@ fn test_check_contributions_success_pays_maker_and_marks_claimed() { let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert!(fundraiser_state.claimed); assert_eq!( - fundraiser_state.open_contributor_accounts, + fundraiser_state.open_contributions, CONTRIBUTIONS_REACHING_TARGET.len() as u32 ); } @@ -790,25 +788,25 @@ fn test_check_contributions_ignores_direct_vault_donations() { } #[test] -fn test_close_contributor_after_claim_returns_rent() { +fn test_close_contribution_after_claim_returns_rent() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); let contributors = fund_to_target(&mut setup); claim(&mut setup).unwrap(); let contributor = &contributors[0]; - let rent = lamports(&setup, &contributor.contributor_account_pda); + let rent = lamports(&setup, &contributor.contribution_pda); let lamports_before = lamports(&setup, &contributor.keypair.pubkey()); let fee_payer = contributor.keypair.insecure_clone(); - close_contributor(&mut setup, &fee_payer, contributor).unwrap(); + close_contribution(&mut setup, &fee_payer, contributor).unwrap(); assert!( setup .svm - .get_account(&contributor.contributor_account_pda) + .get_account(&contributor.contribution_pda) .is_none(), - "Contributor account must be closed" + "Contribution account must be closed" ); // The contributor paid the transaction fee out of the same balance, so // the rent came back less that fee. @@ -818,25 +816,25 @@ fn test_close_contributor_after_claim_returns_rent() { ); let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert_eq!( - fundraiser_state.open_contributor_accounts, + fundraiser_state.open_contributions, CONTRIBUTIONS_REACHING_TARGET.len() as u32 - 1 ); } #[test] -fn test_anyone_can_close_contributor_accounts_after_claim() { +fn test_anyone_can_close_contribution_accounts_after_claim() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); let contributors = fund_to_target(&mut setup); claim(&mut setup).unwrap(); - // The maker closes every contributor account; each rent deposit goes to + // The maker closes every contribution account; each rent deposit goes to // its contributor, who signs nothing. let maker = setup.maker.insecure_clone(); for contributor in &contributors { - let rent = lamports(&setup, &contributor.contributor_account_pda); + let rent = lamports(&setup, &contributor.contribution_pda); let lamports_before = lamports(&setup, &contributor.keypair.pubkey()); - close_contributor(&mut setup, &maker, contributor).unwrap(); + close_contribution(&mut setup, &maker, contributor).unwrap(); assert_eq!( lamports(&setup, &contributor.keypair.pubkey()), lamports_before + rent @@ -844,11 +842,11 @@ fn test_anyone_can_close_contributor_accounts_after_claim() { } let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); - assert_eq!(fundraiser_state.open_contributor_accounts, 0); + assert_eq!(fundraiser_state.open_contributions, 0); } #[test] -fn test_close_contributor_before_claim_fails() { +fn test_close_contribution_before_claim_fails() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); let contributor = new_contributor(&mut setup); @@ -858,12 +856,11 @@ fn test_close_contributor_before_claim_fails() { // refunded: closing the account now would erase what the vault owes. let fee_payer = contributor.keypair.insecure_clone(); assert_error( - close_contributor(&mut setup, &fee_payer, &contributor), + close_contribution(&mut setup, &fee_payer, &contributor), FundraiserError::FundraiserNotClaimed, ); - let contributor_state = - read_contributor_state(&setup.svm, &contributor.contributor_account_pda); - assert_eq!(contributor_state.amount, CONTRIBUTION); + let contribution_state = read_contribution_state(&setup.svm, &contributor.contribution_pda); + assert_eq!(contribution_state.amount, CONTRIBUTION); } #[test] @@ -959,27 +956,27 @@ fn test_close_fundraiser_when_target_met_but_unclaimed_fails() { } #[test] -fn test_close_fundraiser_with_open_contributor_accounts_fails() { +fn test_close_fundraiser_with_open_contributions_fails() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); let contributors = fund_to_target(&mut setup); claim(&mut setup).unwrap(); - // Close all but one contributor account. + // Close all but one contribution account. let maker = setup.maker.insecure_clone(); for contributor in &contributors[1..] { - close_contributor(&mut setup, &maker, contributor).unwrap(); + close_contribution(&mut setup, &maker, contributor).unwrap(); } assert_error( close_fundraiser(&mut setup), - FundraiserError::ContributorAccountsOpen, + FundraiserError::ContributionsOpen, ); assert!(setup.svm.get_account(&setup.fundraiser_pda).is_some()); } #[test] -fn test_reinitialize_with_open_contributor_accounts_fails() { +fn test_reinitialize_with_open_contributions_fails() { let mut setup = full_setup(); initialize_fundraiser(&mut setup, AMOUNT_TO_RAISE, DURATION_DAYS); fund_to_target(&mut setup); @@ -1021,7 +1018,7 @@ fn test_reinitialize_with_open_contributor_accounts_fails() { } #[test] -fn test_stale_contributor_account_cannot_refund_from_next_raise() { +fn test_stale_contribution_cannot_refund_from_next_raise() { let mut setup = full_setup(); // Raise one succeeds and the maker claims it. @@ -1029,11 +1026,11 @@ fn test_stale_contributor_account_cannot_refund_from_next_raise() { let first_raise_contributors = fund_to_target(&mut setup); claim(&mut setup).unwrap(); - // The maker closes every contributor account from raise one, then the + // The maker closes every contribution account from raise one, then the // fundraiser, and starts raise two at the same address. let maker = setup.maker.insecure_clone(); for contributor in &first_raise_contributors { - close_contributor(&mut setup, &maker, contributor).unwrap(); + close_contribution(&mut setup, &maker, contributor).unwrap(); } close_fundraiser(&mut setup).unwrap(); setup.svm.expire_blockhash(); @@ -1050,7 +1047,7 @@ fn test_stale_contributor_account_cannot_refund_from_next_raise() { warp_days_forward(&mut setup.svm, DURATION_DAYS as i64); // A raise-one contributor tries to take a refund from raise two. Their - // contributor account was closed with raise one, so there is nothing to + // contribution account was closed with raise one, so there is nothing to // refund. let stale_contributor = &first_raise_contributors[0]; let fee_payer = stale_contributor.keypair.insecure_clone(); @@ -1071,7 +1068,7 @@ fn test_stale_contributor_account_cannot_refund_from_next_raise() { } let fundraiser_state = read_fundraiser_state(&setup.svm, &setup.fundraiser_pda); assert_eq!(fundraiser_state.current_amount, 0); - assert_eq!(fundraiser_state.open_contributor_accounts, 0); + assert_eq!(fundraiser_state.open_contributions, 0); } #[test] @@ -1083,7 +1080,7 @@ fn test_close_fundraiser_after_claim_allows_a_new_raise() { let maker = setup.maker.insecure_clone(); for contributor in &contributors { - close_contributor(&mut setup, &maker, contributor).unwrap(); + close_contribution(&mut setup, &maker, contributor).unwrap(); } close_fundraiser(&mut setup).unwrap(); assert!(setup.svm.get_account(&setup.fundraiser_pda).is_none()); diff --git a/finance/fundraiser/kani-proofs/README.md b/finance/fundraiser/kani-proofs/README.md index ffa786926..4d239b8d0 100644 --- a/finance/fundraiser/kani-proofs/README.md +++ b/finance/fundraiser/kani-proofs/README.md @@ -14,10 +14,10 @@ one. The program collects contributions toward a goal; if the goal is not met by the deadline, every contributor reclaims their exact stake. Token movement is via SPL CPIs Kani cannot symbolically execute, but the accounting (`contribute`, -`refund`, `close_contributor`) is pure integer arithmetic, and the harnesses check it for every input in the +`refund`, `close_contribution`) is pure integer arithmetic, and the harnesses check it for every input in the declared ranges: -- `proof_open_contributor_accounts_counts_open_accounts`: `open_contributor_accounts` always equals the number of contributor accounts that exist, over every sequence of eight contributions, refunds and closes among three contributors. `close_fundraiser` requires it to be zero, so no contributor account carries over into the next raise at the same address. +- `proof_open_contributions_counts_open_accounts`: `open_contributions` always equals the number of contribution accounts that exist, over every sequence of eight contributions, refunds and closes among three contributors. `close_fundraiser` requires it to be zero, so no contribution account carries over into the next raise at the same address. - `proof_current_amount_is_sum_of_contributions`: `current_amount` always equals the sum of the contributions added to it, no accounting drift. - `proof_refunds_sum_to_current_amount`: On a failed raise, refunds sum back to `current_amount`; no contributor reclaims more than they put in. diff --git a/finance/fundraiser/kani-proofs/src/lib.rs b/finance/fundraiser/kani-proofs/src/lib.rs index 26130e57e..ee6a1383e 100644 --- a/finance/fundraiser/kani-proofs/src/lib.rs +++ b/finance/fundraiser/kani-proofs/src/lib.rs @@ -10,7 +10,7 @@ //! The program collects contributions into a vault toward a goal; if the goal //! is not met by the deadline, every contributor reclaims their exact stake. //! Token movement is via SPL CPIs Kani cannot symbolically execute, but the -//! accounting (`contribute`, `refund`, `close_contributor`) is pure integer +//! accounting (`contribute`, `refund`, `close_contribution`) is pure integer //! arithmetic. This crate reproduces it faithfully and checks the contributor //! account counter, the running-total accounting, and refund conservation. @@ -19,74 +19,74 @@ /// How many contributors the counter harness tracks. pub const CONTRIBUTORS: usize = 3; -/// One step of a fundraiser's life, as it affects contributor accounts. +/// One step of a fundraiser's life, as it affects contribution accounts. #[derive(Clone, Copy)] -pub enum ContributorAccountStep { +pub enum ContributionAccountStep { /// `contribute` from this contributor: creates their account on the first /// call and adds to it on later ones. Contribute(usize), - /// `refund` or `close_contributor` for this contributor: both close the + /// `refund` or `close_contribution` for this contributor: both close the /// account, and both fail if it does not exist. Close(usize), } -/// Replays `contribute`, `refund` and `close_contributor`'s bookkeeping on -/// `open_contributor_accounts`. Returns `None` where the program would reject +/// Replays `contribute`, `refund` and `close_contribution`'s bookkeeping on +/// `open_contributions`. Returns `None` where the program would reject /// the step, as it does when the account to close does not exist. pub fn apply_step( open_accounts: &mut [bool; CONTRIBUTORS], - open_contributor_accounts: u32, - step: ContributorAccountStep, + open_contributions: u32, + step: ContributionAccountStep, ) -> Option { match step { - ContributorAccountStep::Contribute(contributor) => { + ContributionAccountStep::Contribute(contributor) => { if open_accounts[contributor] { - Some(open_contributor_accounts) + Some(open_contributions) } else { open_accounts[contributor] = true; - open_contributor_accounts.checked_add(1) + open_contributions.checked_add(1) } } - ContributorAccountStep::Close(contributor) => { + ContributionAccountStep::Close(contributor) => { if !open_accounts[contributor] { return None; } open_accounts[contributor] = false; - open_contributor_accounts.checked_sub(1) + open_contributions.checked_sub(1) } } } // =========================================================================== -// 1. Contributor account counter +// 1. Contribution account counter // =========================================================================== -/// `fundraiser.open_contributor_accounts` always equals the number of -/// contributor accounts that exist for the fundraiser, whatever order +/// `fundraiser.open_contributions` always equals the number of +/// contribution accounts that exist for the fundraiser, whatever order /// contributions, refunds and closes arrive in. `close_fundraiser` requires -/// the counter to be zero, so this is what guarantees no contributor account +/// the counter to be zero, so this is what guarantees no contribution account /// outlives its fundraiser and carries over into the next raise at the same /// address. #[cfg(kani)] #[kani::proof] #[kani::unwind(9)] -fn proof_open_contributor_accounts_counts_open_accounts() { +fn proof_open_contributions_counts_open_accounts() { let mut open_accounts = [false; CONTRIBUTORS]; - let mut open_contributor_accounts: u32 = 0; + let mut open_contributions: u32 = 0; for _ in 0..8 { let contributor: usize = kani::any(); kani::assume(contributor < CONTRIBUTORS); let step = if kani::any() { - ContributorAccountStep::Contribute(contributor) + ContributionAccountStep::Contribute(contributor) } else { - ContributorAccountStep::Close(contributor) + ContributionAccountStep::Close(contributor) }; - if let Some(updated) = apply_step(&mut open_accounts, open_contributor_accounts, step) { - open_contributor_accounts = updated; + if let Some(updated) = apply_step(&mut open_accounts, open_contributions, step) { + open_contributions = updated; } let actually_open = open_accounts.iter().filter(|open| **open).count() as u32; - assert_eq!(open_contributor_accounts, actually_open); + assert_eq!(open_contributions, actually_open); } } @@ -156,14 +156,14 @@ mod tests { use super::*; #[test] - fn counter_tracks_contributor_accounts() { + fn counter_tracks_contributions() { let mut open_accounts = [false; CONTRIBUTORS]; let mut counter = 0u32; for step in [ - ContributorAccountStep::Contribute(0), - ContributorAccountStep::Contribute(0), - ContributorAccountStep::Contribute(2), - ContributorAccountStep::Close(0), + ContributionAccountStep::Contribute(0), + ContributionAccountStep::Contribute(0), + ContributionAccountStep::Contribute(2), + ContributionAccountStep::Close(0), ] { counter = apply_step(&mut open_accounts, counter, step).unwrap(); } @@ -171,7 +171,7 @@ mod tests { assert!(apply_step( &mut open_accounts, counter, - ContributorAccountStep::Close(1) + ContributionAccountStep::Close(1) ) .is_none()); } diff --git a/finance/fundraiser/quasar/CHANGELOG.md b/finance/fundraiser/quasar/CHANGELOG.md index 911c26f43..5113b82c2 100644 --- a/finance/fundraiser/quasar/CHANGELOG.md +++ b/finance/fundraiser/quasar/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [2026-10-03] + +### Changed + +- The per-contributor account is `Contribution`, closed by + `close_contribution`. The handler named `close_contributor` closed the + account recording one contributor's contributions, not the contributor, so + it is now `close_contribution` (account constraints + `CloseContributionAccountConstraints`). The account struct `Contributor` is + now `Contribution`, its handler field `contributor_account` is + `contribution`, and its PDA seed prefix is `"contribution"` instead of + `"contributor"`. The Fundraiser's `open_contributor_accounts` count is + `open_contributions`, and the error `ContributorAccountsOpen` is + `ContributionsOpen`. No behavior changes. + ## [2026-10-01] ### Fixed diff --git a/finance/fundraiser/quasar/README.md b/finance/fundraiser/quasar/README.md index 0bced1c48..7596b2fe7 100644 --- a/finance/fundraiser/quasar/README.md +++ b/finance/fundraiser/quasar/README.md @@ -8,20 +8,20 @@ See also: the [repository catalog](../../../README.md) and the [Anchor variant]( ## Major concepts -- The **Fundraiser** account is a PDA at `["fundraiser", maker]`. It stores the maker, the token's mint, the vault address, the target (`amount_to_raise`), the running total (`current_amount`), the Clock timestamp captured at creation (`time_started`), the window length in days (`duration`), whether the maker has claimed it (`claimed`), how many Contributor accounts written for it are still open (`open_contributor_accounts`), and the PDA bump. Storing the vault address lets every later instruction bind the passed vault to this fundraiser with a `has_one(vault)` constraint. -- A **Contributor** account is a PDA at `["contributor", fundraiser, contributor]`. It records how much that contributor has given to that fundraiser, plus its bump. The seeds bind the record to one (fundraiser, contributor) pair, so one contributor's record can never be paid to anyone else or against another fundraiser. +- The **Fundraiser** account is a PDA at `["fundraiser", maker]`. It stores the maker, the token's mint, the vault address, the target (`amount_to_raise`), the running total (`current_amount`), the Clock timestamp captured at creation (`time_started`), the window length in days (`duration`), whether the maker has claimed it (`claimed`), how many Contribution accounts written for it are still open (`open_contributions`), and the PDA bump. Storing the vault address lets every later instruction bind the passed vault to this fundraiser with a `has_one(vault)` constraint. +- A **Contribution** account is a PDA at `["contribution", fundraiser, contributor]`. It records how much that contributor has given to that fundraiser, plus its bump. The seeds bind the record to one (fundraiser, contributor) pair, so one contributor's record can never be paid to anyone else or against another fundraiser. - The **vault** is a token account whose authority is the Fundraiser PDA. All deposits, the maker payout, and refunds flow through it, with the PDA signing outbound transfers via its seeds. -- **The Fundraiser outlives every Contributor account.** A Contributor PDA is derived from the Fundraiser's address, and the Fundraiser's address from the maker alone. If the Fundraiser could close while a Contributor account was open, the maker could initialize a new fundraiser at the same address and that Contributor account would count as a contribution to it, so `refund` would pay its old amount out of the new contributors' tokens. `open_contributor_accounts` counts them, and `close_fundraiser` requires zero. +- **The Fundraiser outlives every Contribution account.** A Contribution PDA is derived from the Fundraiser's address, and the Fundraiser's address from the maker alone. If the Fundraiser could close while a Contribution account was open, the maker could initialize a new fundraiser at the same address and that Contribution account would count as a contribution to it, so `refund` would pay its old amount out of the new contributors' tokens. `open_contributions` counts them, and `close_fundraiser` requires zero. - The **fundraising window** runs from `time_started` for `duration` days. Contributions are allowed while `now < time_started + duration`; refunds are allowed once `now >= time_started + duration` and only if the target was not met. `now` is the Clock sysvar's unix timestamp. ## Lifecycle -- `initialize_fundraiser` (maker signs): rejects a zero target (`InvalidAmount`) or zero duration (`InvalidDuration`), creates the Fundraiser PDA and the vault, and records the current Clock time as `time_started`, with `claimed` false and `open_contributor_accounts` zero. -- `contribute` (contributor signs): rejects a zero amount, fails with `FundraiserClaimed` once the maker has claimed, and fails with `FundraiserEnded` after the deadline. Creates the contributor's Contributor PDA on first use (idempotent init, contributor pays the rent) and adds one to `open_contributor_accounts`, adds the amount to both `current_amount` and the contributor's record with checked arithmetic, transfers tokens from the contributor's token account into the vault, then verifies the vault gained exactly the contributed amount (`BalanceMismatch` otherwise). -- `check_contributions` (maker signs): fails with `FundraiserClaimed` if already claimed and with `TargetNotMet` unless `current_amount >= amount_to_raise`. Sets `claimed` and transfers the whole vault balance to the maker's token account with the Fundraiser PDA signing. The Fundraiser and the empty vault stay open until every Contributor account is closed. -- `refund` (anyone may send it; the contributor does not sign): fails with `FundraiserNotEnded` before the deadline and with `TargetMet` if the fundraiser succeeded. Pays the contributor's recorded amount back from the vault to the contributor's own token account (checked against the contributor's address) with the PDA signing, subtracts it from `current_amount` and one from `open_contributor_accounts`, verifies the vault lost exactly that amount, and closes the Contributor account, returning its rent to the contributor. The tokens and rent can only reach the contributor, so the maker can refund everyone without waiting on them. -- `close_contributor` (anyone may send it; the contributor does not sign): fails with `FundraiserNotClaimed` until the maker has claimed, because before then the contribution can still be refunded. Closes the Contributor account, returning its rent to the contributor, and subtracts one from `open_contributor_accounts`. The Contributor account's seeds bind it to the fundraiser address, so no other fundraiser can be substituted. -- `close_fundraiser` (maker signs): for an unclaimed fundraiser, fails with `FundraiserNotEnded` before the deadline, `TargetMet` if the target was met, and `RefundsOutstanding` while `current_amount` is above zero. For any fundraiser, fails with `ContributorAccountsOpen` while `open_contributor_accounts` is above zero. Pays any tokens left in the vault (direct transfers outside `contribute`) to the maker's token account, then closes the vault and the Fundraiser account, returning their rent to the maker. +- `initialize_fundraiser` (maker signs): rejects a zero target (`InvalidAmount`) or zero duration (`InvalidDuration`), creates the Fundraiser PDA and the vault, and records the current Clock time as `time_started`, with `claimed` false and `open_contributions` zero. +- `contribute` (contributor signs): rejects a zero amount, fails with `FundraiserClaimed` once the maker has claimed, and fails with `FundraiserEnded` after the deadline. Creates the contributor's Contribution PDA on first use (idempotent init, contributor pays the rent) and adds one to `open_contributions`, adds the amount to both `current_amount` and the contributor's record with checked arithmetic, transfers tokens from the contributor's token account into the vault, then verifies the vault gained exactly the contributed amount (`BalanceMismatch` otherwise). +- `check_contributions` (maker signs): fails with `FundraiserClaimed` if already claimed and with `TargetNotMet` unless `current_amount >= amount_to_raise`. Sets `claimed` and transfers the whole vault balance to the maker's token account with the Fundraiser PDA signing. The Fundraiser and the empty vault stay open until every Contribution account is closed. +- `refund` (anyone may send it; the contributor does not sign): fails with `FundraiserNotEnded` before the deadline and with `TargetMet` if the fundraiser succeeded. Pays the contributor's recorded amount back from the vault to the contributor's own token account (checked against the contributor's address) with the PDA signing, subtracts it from `current_amount` and one from `open_contributions`, verifies the vault lost exactly that amount, and closes the Contribution account, returning its rent to the contributor. The tokens and rent can only reach the contributor, so the maker can refund everyone without waiting on them. +- `close_contribution` (anyone may send it; the contributor does not sign): fails with `FundraiserNotClaimed` until the maker has claimed, because before then the contribution can still be refunded. Closes the Contribution account, returning its rent to the contributor, and subtracts one from `open_contributions`. The Contribution account's seeds bind it to the fundraiser address, so no other fundraiser can be substituted. +- `close_fundraiser` (maker signs): for an unclaimed fundraiser, fails with `FundraiserNotEnded` before the deadline, `TargetMet` if the target was met, and `RefundsOutstanding` while `current_amount` is above zero. For any fundraiser, fails with `ContributionsOpen` while `open_contributions` is above zero. Pays any tokens left in the vault (direct transfers outside `contribute`) to the maker's token account, then closes the vault and the Fundraiser account, returning their rent to the maker. Errors are defined in `src/error.rs` as a `#[error_code]` enum starting at code 6000. @@ -45,4 +45,4 @@ In-process tests via **Quasar SVM** (`quasar-svm` in `Quasar.toml`): quasar test ``` -The tests in `src/tests.rs` drive the real instruction handlers end to end (initialize_fundraiser, contribute, check_contributions, refund, close_contributor, close_fundraiser), assert vault and contributor token balances plus account state after every step, and use `QuasarSvm::warp_to_timestamp` to test both sides of the deadline. They also cover the rejection paths: contributing after the deadline, refunding early or after a successful raise, paying out below target, passing a vault not bound to the fundraiser, refunding against another contributor's record, closing a contributor account before the claim, and closing the fundraiser early, with contributions unrefunded, or with Contributor accounts open. `stale_contributor_account_cannot_refund_from_next_raise` (see `src/tests.rs`) runs two raises at the same address and checks that a first-raise contributor cannot take a refund from the second. No local validator is needed. +The tests in `src/tests.rs` drive the real instruction handlers end to end (initialize_fundraiser, contribute, check_contributions, refund, close_contribution, close_fundraiser), assert vault and contributor token balances plus account state after every step, and use `QuasarSvm::warp_to_timestamp` to test both sides of the deadline. They also cover the rejection paths: contributing after the deadline, refunding early or after a successful raise, paying out below target, passing a vault not bound to the fundraiser, refunding against another contributor's record, closing a contribution account before the claim, and closing the fundraiser early, with contributions unrefunded, or with Contribution accounts open. `stale_contribution_cannot_refund_from_next_raise` (see `src/tests.rs`) runs two raises at the same address and checks that a first-raise contributor cannot take a refund from the second. No local validator is needed. diff --git a/finance/fundraiser/quasar/src/error.rs b/finance/fundraiser/quasar/src/error.rs index 1e2762aac..41ec58d2a 100644 --- a/finance/fundraiser/quasar/src/error.rs +++ b/finance/fundraiser/quasar/src/error.rs @@ -24,12 +24,12 @@ pub enum FundraiserError { BalanceMismatch, /// The fundraiser has already been claimed. FundraiserClaimed, - /// The fundraiser has not been claimed, so the contributor account closes + /// The fundraiser has not been claimed, so the contribution account closes /// through refund. FundraiserNotClaimed, - /// Contributor accounts for this fundraiser are still open, so it cannot + /// Contribution accounts for this fundraiser are still open, so it cannot /// close yet. - ContributorAccountsOpen, + ContributionsOpen, /// Contributions to an unclaimed fundraiser have not all been refunded, /// so closing its vault would strand them. RefundsOutstanding, diff --git a/finance/fundraiser/quasar/src/instructions/check_contributions.rs b/finance/fundraiser/quasar/src/instructions/check_contributions.rs index 633fa628b..ee5dae34e 100644 --- a/finance/fundraiser/quasar/src/instructions/check_contributions.rs +++ b/finance/fundraiser/quasar/src/instructions/check_contributions.rs @@ -34,11 +34,11 @@ pub struct CheckContributionsAccountConstraints { /// Pays the vault out to the maker once the target is met, and marks the /// fundraiser claimed. /// -/// The fundraiser account and the vault stay open: contributor accounts are +/// The fundraiser account and the vault stay open: contribution accounts are /// derived from the fundraiser's address, so the fundraiser must outlive every /// one of them. Otherwise the maker could initialize a new fundraiser at the -/// same address, and contributor accounts left over from this raise would -/// count as contributions to the new one. `close_contributor` closes them, +/// same address, and contribution accounts left over from this raise would +/// count as contributions to the new one. `close_contribution` closes them, /// then `close_fundraiser` closes the fundraiser and the vault. #[inline(always)] pub fn handle_check_contributions( diff --git a/finance/fundraiser/quasar/src/instructions/close_contributor.rs b/finance/fundraiser/quasar/src/instructions/close_contribution.rs similarity index 55% rename from finance/fundraiser/quasar/src/instructions/close_contributor.rs rename to finance/fundraiser/quasar/src/instructions/close_contribution.rs index 2e9ec8ca1..0d02866dc 100644 --- a/finance/fundraiser/quasar/src/instructions/close_contributor.rs +++ b/finance/fundraiser/quasar/src/instructions/close_contribution.rs @@ -1,21 +1,21 @@ use { crate::{ error::FundraiserError, - state::{Contributor, Fundraiser}, + state::{Contribution, Fundraiser}, }, quasar_lang::prelude::*, }; #[derive(Accounts)] -pub struct CloseContributorAccountConstraints { +pub struct CloseContributionAccountConstraints { /// Not a signer: the rent goes to the contributor, whoever sends the - /// transaction. So a maker can close every contributor account and then + /// transaction. So a maker can close every contribution account and then /// the fundraiser without waiting on any contributor. #[account(mut)] pub contributor: SystemAccount, - /// The fundraiser this contributor account was written for. The - /// contributor account's seeds bind it to this address, so no other + /// The fundraiser this contribution account was written for. The + /// contribution account's seeds bind it to this address, so no other /// fundraiser can be substituted. #[account(mut)] pub fundraiser: Account, @@ -23,31 +23,31 @@ pub struct CloseContributorAccountConstraints { #[account( mut, close(dest = contributor), - address = Contributor::seeds(fundraiser.address(), contributor.address()), + address = Contribution::seeds(fundraiser.address(), contributor.address()), )] - pub contributor_account: Account, + pub contribution: Account, } -/// Closes a contributor account once its fundraiser has been claimed, +/// Closes a contribution account once its fundraiser has been claimed, /// returning the rent to the contributor. /// -/// `refund` closes contributor accounts on a failed raise. On a successful +/// `refund` closes contribution accounts on a failed raise. On a successful /// one the contribution has been paid out to the maker, so the account only /// holds rent, and `close_fundraiser` cannot run until every one of them is /// closed. While the fundraiser is unclaimed the contribution can still be /// refunded, so this handler refuses with `FundraiserNotClaimed`. #[inline(always)] -pub fn handle_close_contributor( - accounts: &mut CloseContributorAccountConstraints, +pub fn handle_close_contribution( + accounts: &mut CloseContributionAccountConstraints, ) -> Result<(), ProgramError> { require!( bool::from(accounts.fundraiser.claimed), FundraiserError::FundraiserNotClaimed ); - let open_contributor_accounts: u32 = accounts.fundraiser.open_contributor_accounts.into(); - accounts.fundraiser.open_contributor_accounts = PodU32::from( - open_contributor_accounts + let open_contributions: u32 = accounts.fundraiser.open_contributions.into(); + accounts.fundraiser.open_contributions = PodU32::from( + open_contributions .checked_sub(1) .ok_or(FundraiserError::MathOverflow)?, ); diff --git a/finance/fundraiser/quasar/src/instructions/close_fundraiser.rs b/finance/fundraiser/quasar/src/instructions/close_fundraiser.rs index 6f6604835..84ac6e1ef 100644 --- a/finance/fundraiser/quasar/src/instructions/close_fundraiser.rs +++ b/finance/fundraiser/quasar/src/instructions/close_fundraiser.rs @@ -40,8 +40,8 @@ pub struct CloseFundraiserAccountConstraints { /// /// The fundraiser PDA is derived from the maker's address alone, so while a /// fundraiser account exists the maker cannot initialize another one. It -/// closes once no contributor account written for it is still open: after a -/// claim, once `close_contributor` has closed each one; after a failed raise, +/// closes once no contribution account written for it is still open: after a +/// claim, once `close_contribution` has closed each one; after a failed raise, /// once the deadline has passed and `refund` has closed each one. #[inline(always)] pub fn handle_close_fundraiser( @@ -68,13 +68,10 @@ pub fn handle_close_fundraiser( require!(current_amount == 0, FundraiserError::RefundsOutstanding); } - // A contributor account left open would be read as a contribution to the + // A contribution account left open would be read as a contribution to the // next fundraiser at this address. - let open_contributor_accounts: u32 = accounts.fundraiser.open_contributor_accounts.into(); - require!( - open_contributor_accounts == 0, - FundraiserError::ContributorAccountsOpen - ); + let open_contributions: u32 = accounts.fundraiser.open_contributions.into(); + require!(open_contributions == 0, FundraiserError::ContributionsOpen); // Fundraiser PDA signer seeds: ["fundraiser", maker, bump]. let bump = [bumps.fundraiser]; diff --git a/finance/fundraiser/quasar/src/instructions/contribute.rs b/finance/fundraiser/quasar/src/instructions/contribute.rs index 6681cbab4..c9c6bd886 100644 --- a/finance/fundraiser/quasar/src/instructions/contribute.rs +++ b/finance/fundraiser/quasar/src/instructions/contribute.rs @@ -1,7 +1,7 @@ use { crate::{ error::FundraiserError, - state::{fundraiser_deadline, Contributor, Fundraiser}, + state::{fundraiser_deadline, Contribution, Fundraiser}, }, quasar_lang::{prelude::*, sysvars::Sysvar as _}, quasar_spl::prelude::*, @@ -27,9 +27,9 @@ pub struct ContributeAccountConstraints { mut, init(idempotent), payer = contributor, - address = Contributor::seeds(fundraiser.address(), contributor.address()), + address = Contribution::seeds(fundraiser.address(), contributor.address()), )] - pub contributor_account: Account, + pub contribution: Account, #[account(mut)] pub contributor_ta: Account, @@ -77,22 +77,22 @@ pub fn handle_contribute( .ok_or(FundraiserError::MathOverflow)?, ); - let contributed_so_far: u64 = accounts.contributor_account.amount.into(); - accounts.contributor_account.amount = PodU64::from( + let contributed_so_far: u64 = accounts.contribution.amount.into(); + accounts.contribution.amount = PodU64::from( contributed_so_far .checked_add(amount) .ok_or(FundraiserError::MathOverflow)?, ); - // `init(idempotent)` creates the contributor account zeroed and reuses it + // `init(idempotent)` creates the contribution account zeroed and reuses it // on later contributions. Every contribution is nonzero, so a recorded // amount of zero means the account was created by this instruction: save // its bump and count it against the fundraiser. if contributed_so_far == 0 { - accounts.contributor_account.bump = bumps.contributor_account; - let open_contributor_accounts: u32 = accounts.fundraiser.open_contributor_accounts.into(); - accounts.fundraiser.open_contributor_accounts = PodU32::from( - open_contributor_accounts + accounts.contribution.bump = bumps.contribution; + let open_contributions: u32 = accounts.fundraiser.open_contributions.into(); + accounts.fundraiser.open_contributions = PodU32::from( + open_contributions .checked_add(1) .ok_or(FundraiserError::MathOverflow)?, ); diff --git a/finance/fundraiser/quasar/src/instructions/initialize_fundraiser.rs b/finance/fundraiser/quasar/src/instructions/initialize_fundraiser.rs index 263b33224..ab490c4a3 100644 --- a/finance/fundraiser/quasar/src/instructions/initialize_fundraiser.rs +++ b/finance/fundraiser/quasar/src/instructions/initialize_fundraiser.rs @@ -54,7 +54,7 @@ pub fn handle_initialize_fundraiser( time_started, duration, claimed: PodBool::from(false), - open_contributor_accounts: 0, + open_contributions: 0, bump, }); Ok(()) diff --git a/finance/fundraiser/quasar/src/instructions/mod.rs b/finance/fundraiser/quasar/src/instructions/mod.rs index 932e6663e..200fa491d 100644 --- a/finance/fundraiser/quasar/src/instructions/mod.rs +++ b/finance/fundraiser/quasar/src/instructions/mod.rs @@ -10,8 +10,8 @@ pub use check_contributions::*; pub mod refund; pub use refund::*; -pub mod close_contributor; -pub use close_contributor::*; +pub mod close_contribution; +pub use close_contribution::*; pub mod close_fundraiser; pub use close_fundraiser::*; diff --git a/finance/fundraiser/quasar/src/instructions/refund.rs b/finance/fundraiser/quasar/src/instructions/refund.rs index c548df182..c065bc6ec 100644 --- a/finance/fundraiser/quasar/src/instructions/refund.rs +++ b/finance/fundraiser/quasar/src/instructions/refund.rs @@ -2,7 +2,7 @@ use quasar_lang::cpi::Seed; use { crate::{ error::FundraiserError, - state::{fundraiser_deadline, Contributor, Fundraiser}, + state::{fundraiser_deadline, Contribution, Fundraiser}, }, quasar_lang::{prelude::*, sysvars::Sysvar as _}, quasar_spl::prelude::*, @@ -31,9 +31,9 @@ pub struct RefundAccountConstraints { #[account( mut, close(dest = contributor), - address = Contributor::seeds(fundraiser.address(), contributor.address()), + address = Contribution::seeds(fundraiser.address(), contributor.address()), )] - pub contributor_account: Account, + pub contribution: Account, // Bound to the contributor: since anyone may send a refund, the tokens // must go to a token account the contributor owns, in the raised mint. @@ -72,7 +72,7 @@ pub fn handle_refund( let amount_to_raise: u64 = accounts.fundraiser.amount_to_raise.into(); require!(current_amount < amount_to_raise, FundraiserError::TargetMet); - let refund_amount: u64 = accounts.contributor_account.amount.into(); + let refund_amount: u64 = accounts.contribution.amount.into(); // Update state before the transfer CPI (checks-effects-interactions). accounts.fundraiser.current_amount = PodU64::from( @@ -80,10 +80,10 @@ pub fn handle_refund( .checked_sub(refund_amount) .ok_or(FundraiserError::MathOverflow)?, ); - accounts.contributor_account.amount = PodU64::from(0); - let open_contributor_accounts: u32 = accounts.fundraiser.open_contributor_accounts.into(); - accounts.fundraiser.open_contributor_accounts = PodU32::from( - open_contributor_accounts + accounts.contribution.amount = PodU64::from(0); + let open_contributions: u32 = accounts.fundraiser.open_contributions.into(); + accounts.fundraiser.open_contributions = PodU32::from( + open_contributions .checked_sub(1) .ok_or(FundraiserError::MathOverflow)?, ); diff --git a/finance/fundraiser/quasar/src/lib.rs b/finance/fundraiser/quasar/src/lib.rs index f5106752b..479b8b55a 100644 --- a/finance/fundraiser/quasar/src/lib.rs +++ b/finance/fundraiser/quasar/src/lib.rs @@ -14,7 +14,7 @@ declare_id!("Eoiuq1dXvHxh6dLx3wh9gj8kSAUpga11krTrbfF5XYsC"); /// Token crowdfunding program: a maker creates a fundraiser targeting a specific /// SPL token. Contributors deposit tokens into a vault. If the target is met, /// the maker withdraws everything. If not, contributors can reclaim their funds. -/// Once every contributor account is closed, the maker closes the fundraiser. +/// Once every contribution account is closed, the maker closes the fundraiser. #[program] mod quasar_fundraiser { use super::*; @@ -46,7 +46,7 @@ mod quasar_fundraiser { /// Maker withdraws all funds once the target is met, marking the /// fundraiser claimed. The fundraiser and the vault stay open until every - /// contributor account is closed. + /// contribution account is closed. #[instruction(discriminator = 2)] pub fn check_contributions( ctx: Ctx, @@ -61,13 +61,13 @@ mod quasar_fundraiser { instructions::handle_refund(&mut ctx.accounts, &ctx.bumps) } - /// Close a contributor account once its fundraiser has been claimed, + /// Close a contribution account once its fundraiser has been claimed, /// returning the rent to the contributor. Anyone may send it. #[instruction(discriminator = 4)] - pub fn close_contributor( - ctx: Ctx, + pub fn close_contribution( + ctx: Ctx, ) -> Result<(), ProgramError> { - instructions::handle_close_contributor(&mut ctx.accounts) + instructions::handle_close_contribution(&mut ctx.accounts) } /// Maker closes a finished fundraiser and its vault once no contributor diff --git a/finance/fundraiser/quasar/src/state.rs b/finance/fundraiser/quasar/src/state.rs index a96f7d291..49d3d44e2 100644 --- a/finance/fundraiser/quasar/src/state.rs +++ b/finance/fundraiser/quasar/src/state.rs @@ -23,12 +23,12 @@ pub struct Fundraiser { pub duration: u16, /// Set by `check_contributions`. A claimed fundraiser accepts no more /// contributions and no second claim, and stays open until every - /// contributor account written for it has been closed. + /// contribution account written for it has been closed. pub claimed: PodBool, - /// How many contributor accounts written for this fundraiser are still + /// How many contribution accounts written for this fundraiser are still /// open. `close_fundraiser` requires zero, so a new fundraiser at the - /// same address never starts with contributor accounts from an old one. - pub open_contributor_accounts: u32, + /// same address never starts with contribution accounts from an old one. + pub open_contributions: u32, pub bump: u8, } @@ -36,8 +36,8 @@ pub struct Fundraiser { /// The seeds bind this record to one (fundraiser, contributor) pair, so it /// can never be spent by another signer or against another fundraiser. #[account(discriminator = 2, set_inner)] -#[seeds(b"contributor", fundraiser: Address, contributor: Address)] -pub struct Contributor { +#[seeds(b"contribution", fundraiser: Address, contributor: Address)] +pub struct Contribution { pub amount: u64, pub bump: u8, } diff --git a/finance/fundraiser/quasar/src/tests.rs b/finance/fundraiser/quasar/src/tests.rs index 7d55d8fa0..7849aa78e 100644 --- a/finance/fundraiser/quasar/src/tests.rs +++ b/finance/fundraiser/quasar/src/tests.rs @@ -1,17 +1,18 @@ //! quasar-test integration tests: create a fundraiser, contribute inside the //! window, refund after a failed raise, pay the maker after a successful one, -//! close every contributor account and then the fundraiser, and raise again +//! close every contribution account and then the fundraiser, and raise again //! at the same address — plus the deadline, target, claim, and //! account-binding guard rails. use { crate::{ cpi::{ - CheckContributionsInstruction, CloseContributorInstruction, CloseFundraiserInstruction, - ContributeInstruction, InitializeFundraiserInstruction, RefundInstruction, + CheckContributionsInstruction, CloseContributionInstruction, + CloseFundraiserInstruction, ContributeInstruction, InitializeFundraiserInstruction, + RefundInstruction, }, error::FundraiserError, - state::{Contributor, Fundraiser, SECONDS_PER_DAY}, + state::{Contribution, Fundraiser, SECONDS_PER_DAY}, }, quasar_lang::error::QuasarError, quasar_test::prelude::*, @@ -128,8 +129,8 @@ fn initialized_world(test: &mut Test) -> Pubkey { test.derive_pda(Fundraiser::seeds(&MAKER)) } -fn contributor_account(test: &Test, fundraiser: Pubkey, contributor: ContributorKeys) -> Pubkey { - test.derive_pda(Contributor::seeds(&fundraiser, &contributor.wallet)) +fn contribution(test: &Test, fundraiser: Pubkey, contributor: ContributorKeys) -> Pubkey { + test.derive_pda(Contribution::seeds(&fundraiser, &contributor.wallet)) } fn contribute_from(test: &mut Test, contributor: ContributorKeys, amount: u64) -> Outcome { @@ -188,24 +189,24 @@ fn check_contributions(test: &mut Test) -> Outcome { }) } -fn close_contributor_instruction(contributor: ContributorKeys, fundraiser: Pubkey) -> Instruction { - CloseContributorInstruction { +fn close_contribution_instruction(contributor: ContributorKeys, fundraiser: Pubkey) -> Instruction { + CloseContributionInstruction { contributor: contributor.wallet, fundraiser, } .into() } -fn close_contributor_for( +fn close_contribution_for( test: &mut Test, contributor: ContributorKeys, fundraiser: Pubkey, ) -> Outcome { - test.send(close_contributor_instruction(contributor, fundraiser)) + test.send(close_contribution_instruction(contributor, fundraiser)) } -fn close_contributor(test: &mut Test, fundraiser: Pubkey) -> Outcome { - close_contributor_for(test, FIRST_CONTRIBUTOR, fundraiser) +fn close_contribution(test: &mut Test, fundraiser: Pubkey) -> Outcome { + close_contribution_for(test, FIRST_CONTRIBUTOR, fundraiser) } fn close_fundraiser(test: &mut Test) -> Outcome { @@ -228,11 +229,8 @@ fn donate_to_vault(test: &mut Test, fundraiser: Pubkey, amount: u64) { ); } -fn open_contributor_accounts(test: &Test, fundraiser: Pubkey) -> u32 { - u32::from( - test.read::(fundraiser) - .open_contributor_accounts, - ) +fn open_contributions(test: &Test, fundraiser: Pubkey) -> u32 { + u32::from(test.read::(fundraiser).open_contributions) } fn is_claimed(test: &Test, fundraiser: Pubkey) -> bool { @@ -256,7 +254,7 @@ fn initialize_records_state_and_clock_time(test: &mut Test) { assert_eq!(i64::from(state.time_started), START_TIME); assert_eq!(u16::from(state.duration), DURATION_DAYS); assert!(!bool::from(state.claimed)); - assert_eq!(u32::from(state.open_contributor_accounts), 0); + assert_eq!(u32::from(state.open_contributions), 0); assert_eq!(state.bump, expected_bump); } @@ -273,7 +271,7 @@ fn initialize_rejects_zero_duration(test: &mut Test) { } #[quasar_test] -fn contribute_creates_contributor_account_and_moves_tokens(test: &mut Test) { +fn contribute_creates_contribution_and_moves_tokens(test: &mut Test) { let fundraiser = initialized_world(test); contribute(test, PARTIAL_CONTRIBUTION) @@ -289,37 +287,37 @@ fn contribute_creates_contributor_account_and_moves_tokens(test: &mut Test) { u64::from(fundraiser_state.current_amount), PARTIAL_CONTRIBUTION ); - assert_eq!(u32::from(fundraiser_state.open_contributor_accounts), 1); + assert_eq!(u32::from(fundraiser_state.open_contributions), 1); - let (contributor_account, expected_bump) = - test.derive_pda_with_bump(Contributor::seeds(&fundraiser, &CONTRIBUTOR)); - let contributor_state = test.read::(contributor_account); - assert_eq!(u64::from(contributor_state.amount), PARTIAL_CONTRIBUTION); - assert_eq!(contributor_state.bump, expected_bump); + let (contribution, expected_bump) = + test.derive_pda_with_bump(Contribution::seeds(&fundraiser, &CONTRIBUTOR)); + let contribution_state = test.read::(contribution); + assert_eq!(u64::from(contribution_state.amount), PARTIAL_CONTRIBUTION); + assert_eq!(contribution_state.bump, expected_bump); } #[quasar_test] -fn contributions_accumulate_in_one_contributor_account(test: &mut Test) { +fn contributions_accumulate_in_one_contribution(test: &mut Test) { let fundraiser = initialized_world(test); contribute(test, PARTIAL_CONTRIBUTION).succeeds(); - // The second contribution reuses the contributor account created by the - // first, so the fundraiser still counts one open contributor account. + // The second contribution reuses the contribution account created by the + // first, so the fundraiser still counts one open contribution account. let expected_total = PARTIAL_CONTRIBUTION + SECOND_PARTIAL_CONTRIBUTION; contribute(test, SECOND_PARTIAL_CONTRIBUTION) .succeeds() .has_tokens(VAULT, expected_total); - let contributor_account = contributor_account(test, fundraiser, FIRST_CONTRIBUTOR); + let contribution = contribution(test, fundraiser, FIRST_CONTRIBUTOR); assert_eq!( - u64::from(test.read::(contributor_account).amount), + u64::from(test.read::(contribution).amount), expected_total ); assert_eq!( u64::from(test.read::(fundraiser).current_amount), expected_total ); - assert_eq!(open_contributor_accounts(test, fundraiser), 1); + assert_eq!(open_contributions(test, fundraiser), 1); } #[quasar_test] @@ -380,7 +378,7 @@ fn contribute_after_claim_fails(test: &mut Test) { CONTRIBUTOR_STARTING_BALANCE ); assert_eq!( - open_contributor_accounts(test, fundraiser), + open_contributions(test, fundraiser), TARGET_CONTRIBUTORS.len() as u32 ); } @@ -392,17 +390,17 @@ fn refund_returns_tokens_after_failed_fundraiser(test: &mut Test) { test.warp_to_timestamp(DEADLINE); - let contributor_account = contributor_account(test, fundraiser, FIRST_CONTRIBUTOR); + let contribution = contribution(test, fundraiser, FIRST_CONTRIBUTOR); refund(test) .succeeds() .has_tokens(VAULT, 0) .has_tokens(CONTRIBUTOR_TA, CONTRIBUTOR_STARTING_BALANCE) - // The contributor account was closed and its rent returned. - .is_closed(contributor_account); + // The contribution account was closed and its rent returned. + .is_closed(contribution); let fundraiser_state = test.read::(fundraiser); assert_eq!(u64::from(fundraiser_state.current_amount), 0); - assert_eq!(u32::from(fundraiser_state.open_contributor_accounts), 0); + assert_eq!(u32::from(fundraiser_state.open_contributions), 0); } #[quasar_test] @@ -419,14 +417,14 @@ fn anyone_can_refund_a_contributor(test: &mut Test) { "refund must not require any signature" ); - let contributor_account = contributor_account(test, fundraiser, FIRST_CONTRIBUTOR); - let rent = test.lamports(contributor_account); + let contribution = contribution(test, fundraiser, FIRST_CONTRIBUTOR); + let rent = test.lamports(contribution); let contributor_lamports_before = test.lamports(CONTRIBUTOR); test.send(instruction) .succeeds() .has_tokens(CONTRIBUTOR_TA, CONTRIBUTOR_STARTING_BALANCE) - .is_closed(contributor_account); + .is_closed(contribution); assert_eq!( test.lamports(CONTRIBUTOR), contributor_lamports_before + rent @@ -467,14 +465,14 @@ fn refund_rejects_another_contributors_account(test: &mut Test) { // destination must be owned by the contributor. let mut instruction = refund_instruction(FIRST_CONTRIBUTOR); // Account indices follow the accounts-struct field order: - // 0 contributor, 3 contributor_account, 4 contributor_ta. + // 0 contributor, 3 contribution, 4 contributor_ta. instruction.accounts[4].pubkey = ATTACKER_TA; test.send(instruction) .fails(ProgramError::InvalidAccountData); // The attacker names themselves as the contributor, with their own token // account, against the victim's contributor record. The record's PDA is - // derived from ["contributor", fundraiser, attacker], which does not + // derived from ["contribution", fundraiser, attacker], which does not // match. let mut instruction = refund_instruction(FIRST_CONTRIBUTOR); instruction.accounts[0].pubkey = ATTACKER; @@ -498,14 +496,14 @@ fn check_contributions_pays_maker_and_marks_claimed(test: &mut Test) { .has_tokens(VAULT, 0); // The fundraiser and the vault stay open, the fundraiser marked claimed, - // until every contributor account written for it is closed. + // until every contribution account written for it is closed. assert!( test.account(VAULT).is_some(), "the vault survives the claim" ); assert!(is_claimed(test, fundraiser)); assert_eq!( - open_contributor_accounts(test, fundraiser), + open_contributions(test, fundraiser), TARGET_CONTRIBUTORS.len() as u32 ); } @@ -546,33 +544,33 @@ fn second_claim_fails(test: &mut Test) { } #[quasar_test] -fn close_contributor_returns_rent_after_successful_raise(test: &mut Test) { +fn close_contribution_returns_rent_after_successful_raise(test: &mut Test) { let fundraiser = initialized_world(test); fund_to_target(test); check_contributions(test).succeeds(); - // The claim leaves the contributor account open with its rent inside. - let contributor_account = contributor_account(test, fundraiser, FIRST_CONTRIBUTOR); - let rent = test.lamports(contributor_account); - assert!(rent > 0, "the contributor account survives the claim"); + // The claim leaves the contribution account open with its rent inside. + let contribution = contribution(test, fundraiser, FIRST_CONTRIBUTOR); + let rent = test.lamports(contribution); + assert!(rent > 0, "the contribution account survives the claim"); let lamports_before = test.lamports(CONTRIBUTOR); - close_contributor(test, fundraiser) + close_contribution(test, fundraiser) .succeeds() - .is_closed(contributor_account); + .is_closed(contribution); assert_eq!( test.lamports(CONTRIBUTOR), lamports_before + rent, - "the contributor account's rent returns to the contributor" + "the contribution account's rent returns to the contributor" ); assert_eq!( - open_contributor_accounts(test, fundraiser), + open_contributions(test, fundraiser), TARGET_CONTRIBUTORS.len() as u32 - 1 ); } #[quasar_test] -fn anyone_can_close_contributor_accounts_after_claim(test: &mut Test) { +fn anyone_can_close_contribution_accounts_after_claim(test: &mut Test) { let fundraiser = initialized_world(test); fund_to_target(test); check_contributions(test).succeeds(); @@ -580,39 +578,37 @@ fn anyone_can_close_contributor_accounts_after_claim(test: &mut Test) { // Whoever sends it, each rent deposit goes to its contributor, who signs // nothing. for contributor in TARGET_CONTRIBUTORS { - let instruction = close_contributor_instruction(contributor, fundraiser); + let instruction = close_contribution_instruction(contributor, fundraiser); assert!( instruction.accounts.iter().all(|meta| !meta.is_signer), - "close_contributor must not require any signature" + "close_contribution must not require any signature" ); - let contributor_account = contributor_account(test, fundraiser, contributor); - let rent = test.lamports(contributor_account); + let contribution = contribution(test, fundraiser, contributor); + let rent = test.lamports(contribution); let lamports_before = test.lamports(contributor.wallet); - test.send(instruction) - .succeeds() - .is_closed(contributor_account); + test.send(instruction).succeeds().is_closed(contribution); assert_eq!(test.lamports(contributor.wallet), lamports_before + rent); } - assert_eq!(open_contributor_accounts(test, fundraiser), 0); + assert_eq!(open_contributions(test, fundraiser), 0); } #[quasar_test] -fn close_contributor_before_claim_fails(test: &mut Test) { +fn close_contribution_before_claim_fails(test: &mut Test) { let fundraiser = initialized_world(test); contribute(test, PARTIAL_CONTRIBUTION).succeeds(); // The fundraiser is unclaimed, so the contribution can still be // refunded: closing the record now would erase what the vault owes. - close_contributor(test, fundraiser).fails_with(FundraiserError::FundraiserNotClaimed); + close_contribution(test, fundraiser).fails_with(FundraiserError::FundraiserNotClaimed); - let contributor_account = contributor_account(test, fundraiser, FIRST_CONTRIBUTOR); + let contribution = contribution(test, fundraiser, FIRST_CONTRIBUTOR); assert_eq!( - u64::from(test.read::(contributor_account).amount), + u64::from(test.read::(contribution).amount), PARTIAL_CONTRIBUTION ); - assert_eq!(open_contributor_accounts(test, fundraiser), 1); + assert_eq!(open_contributions(test, fundraiser), 1); } #[quasar_test] @@ -645,7 +641,7 @@ fn close_fundraiser_after_claim_allows_a_new_raise(test: &mut Test) { check_contributions(test).succeeds(); for contributor in TARGET_CONTRIBUTORS { - close_contributor_for(test, contributor, fundraiser).succeeds(); + close_contribution_for(test, contributor, fundraiser).succeeds(); } // A claimed fundraiser closes without waiting for its deadline. close_fundraiser(test) @@ -698,23 +694,23 @@ fn close_fundraiser_when_target_met_but_unclaimed_fails(test: &mut Test) { } #[quasar_test] -fn close_fundraiser_with_open_contributor_accounts_fails(test: &mut Test) { +fn close_fundraiser_with_open_contributions_fails(test: &mut Test) { let fundraiser = initialized_world(test); fund_to_target(test); check_contributions(test).succeeds(); - // Close all but the first contributor account. + // Close all but the first contribution account. for contributor in &TARGET_CONTRIBUTORS[1..] { - close_contributor_for(test, *contributor, fundraiser).succeeds(); + close_contribution_for(test, *contributor, fundraiser).succeeds(); } - close_fundraiser(test).fails_with(FundraiserError::ContributorAccountsOpen); + close_fundraiser(test).fails_with(FundraiserError::ContributionsOpen); assert!(test.account(fundraiser).is_some()); - assert_eq!(open_contributor_accounts(test, fundraiser), 1); + assert_eq!(open_contributions(test, fundraiser), 1); } #[quasar_test] -fn reinitialize_with_open_contributor_accounts_fails(test: &mut Test) { +fn reinitialize_with_open_contributions_fails(test: &mut Test) { let fundraiser = initialized_world(test); fund_to_target(test); check_contributions(test).succeeds(); @@ -725,23 +721,23 @@ fn reinitialize_with_open_contributor_accounts_fails(test: &mut Test) { .fails(ProgramError::AccountAlreadyInitialized); assert!(is_claimed(test, fundraiser)); assert_eq!( - open_contributor_accounts(test, fundraiser), + open_contributions(test, fundraiser), TARGET_CONTRIBUTORS.len() as u32 ); } #[quasar_test] -fn stale_contributor_account_cannot_refund_from_next_raise(test: &mut Test) { +fn stale_contribution_cannot_refund_from_next_raise(test: &mut Test) { let fundraiser = initialized_world(test); // Raise one succeeds and the maker claims it. fund_to_target(test); check_contributions(test).succeeds(); - // The maker closes every contributor account from raise one, then the + // The maker closes every contribution account from raise one, then the // fundraiser, and starts raise two at the same address. for contributor in TARGET_CONTRIBUTORS { - close_contributor_for(test, contributor, fundraiser).succeeds(); + close_contribution_for(test, contributor, fundraiser).succeeds(); } close_fundraiser(test).succeeds(); initialize_fundraiser(test, TARGET_AMOUNT, DURATION_DAYS).succeeds(); @@ -753,11 +749,11 @@ fn stale_contributor_account_cannot_refund_from_next_raise(test: &mut Test) { contribute_from(test, *contributor, amount).succeeds(); } let raise_two_total = PARTIAL_CONTRIBUTION + SECOND_PARTIAL_CONTRIBUTION; - assert_eq!(open_contributor_accounts(test, fundraiser), 2); + assert_eq!(open_contributions(test, fundraiser), 2); test.warp_to_timestamp(DEADLINE); // A raise-one contributor tries to take a refund from raise two. Their - // contributor account was closed with raise one, so the address is a + // contribution account was closed with raise one, so the address is a // system-owned empty account, not a contributor record: there is nothing // to refund. let stale_contributor = FIRST_CONTRIBUTOR; @@ -776,7 +772,7 @@ fn stale_contributor_account_cannot_refund_from_next_raise(test: &mut Test) { } let state = test.read::(fundraiser); assert_eq!(u64::from(state.current_amount), 0); - assert_eq!(u32::from(state.open_contributor_accounts), 0); + assert_eq!(u32::from(state.open_contributions), 0); assert_eq!(test.tokens(VAULT), 0); }