You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit af38af8
Browse filesBrowse the repository at this point in the historyBrowse files
Mike MacCana
committed
Fundraiser: rename close_contributor to close_contribution
The handler closed the account recording one contributor's
contributions, not the contributor. Across the Anchor v2, Anchor v1 and
Quasar copies:
- Contributor -> Contribution (account struct), contributor_account ->
contribution (handler field), PDA seed "contributor" -> "contribution"
- close_contributor -> close_contribution,
CloseContributorAccountConstraints -> CloseContributionAccountConstraints
- Fundraiser.open_contributor_accounts -> open_contributions,
ContributorAccountsOpen -> ContributionsOpen
- tests, READMEs, Kani model and changelogs follow
The contributor account (the contributor's wallet) keeps its name. No
behavior changes.
Claude-Session: https://claude.ai/code/session_01Jzjsod5Bqrr1QHY5hGQYQb
Copy file name to clipboardExpand all lines: finance/fundraiser/anchor-v1/CHANGELOG.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,5 +1,11 @@
1
1
# Changelog
2
2
3
+
## 2026-10-03
4
+
5
+
### Changed
6
+
7
+
-**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.
Copy file name to clipboardExpand all lines: finance/fundraiser/anchor-v1/README.md
+19-19Lines changed: 19 additions & 19 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -25,7 +25,7 @@ pub struct Fundraiser {
25
25
pubtime_started:i64,
26
26
pubduration:u16,
27
27
pubclaimed:bool,
28
-
pubopen_contributor_accounts:u32,
28
+
pubopen_contributions:u32,
29
29
pubbump:u8,
30
30
}
31
31
```
@@ -39,7 +39,7 @@ Fields:
39
39
-`time_started` - when the fundraiser was created.
40
40
-`duration` - fundraising window in days.
41
41
-`claimed` - set by `check_contributions`. A claimed fundraiser accepts no more contributions and no second claim.
42
-
-`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.
42
+
-`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.
43
43
-`bump` - canonical bump for the Fundraiser [PDA](https://solana.com/docs/terminology#program-derived-address-pda).
44
44
45
45
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:
49
49
```rust
50
50
#[account]
51
51
#[derive(InitSpace)]
52
-
pubstructContributor {
52
+
pubstructContribution {
53
53
pubamount:u64,
54
54
pubbump:u8,
55
55
}
56
56
```
57
57
58
58
-`amount` - total amount contributed by this contributor.
59
-
-`bump` - canonical bump for the Contributor PDA.
59
+
-`bump` - canonical bump for the Contribution PDA.
60
60
61
-
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).
61
+
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).
62
62
63
63
### Constants
64
64
@@ -94,7 +94,7 @@ The maker signs and pays for two new accounts:
94
94
-`fundraiser` - the state account, derived from `b"fundraiser"` and the maker's public key. Anchor calculates the canonical bump and the handler stores it.
95
95
-`vault` - the [ATA](https://solana.com/docs/terminology#associated-token-account-ata) that receives contributions, owned by the Fundraiser PDA.
96
96
97
-
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`.
97
+
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`.
98
98
99
99
### `contribute`
100
100
@@ -106,15 +106,15 @@ A contributor signs and the handler performs three checks in order:
106
106
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.
107
107
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`.
108
108
109
-
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`.
109
+
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`.
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.
116
116
117
-
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.
117
+
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.
118
118
119
119
### `refund`
120
120
@@ -125,7 +125,7 @@ Returns a contribution after a failed fundraiser. The contributor does not have
125
125
1. Refunds are allowed only after the fundraiser has ended: `elapsed_days >= duration`, else `FundraiserNotEnded`.
126
126
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`.
127
127
128
-
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.
128
+
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.
129
129
130
130
### `close_fundraiser`
131
131
@@ -139,17 +139,17 @@ For an unclaimed fundraiser, three checks:
139
139
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).
140
140
3. Every contribution has been refunded: `fundraiser.current_amount == 0`, else `RefundsOutstanding` (closing the vault earlier would strand the remaining refunds).
141
141
142
-
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.
142
+
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.
143
143
144
144
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`.
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.
150
+
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.
151
151
152
-
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`.
152
+
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`.
153
153
154
154
## Testing
155
155
@@ -162,21 +162,21 @@ cargo test
162
162
163
163
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.
164
164
165
-
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.
165
+
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.
166
166
167
-
`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.
167
+
`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.
168
168
169
-
`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.
169
+
`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.
170
170
171
171
## FAQ
172
172
173
173
### How do I build crowdfunding on Solana?
174
174
175
175
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.
176
176
177
-
### What happens to the contributor accounts after a successful raise?
177
+
### What happens to the contribution accounts after a successful raise?
178
178
179
-
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.
179
+
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.
180
180
181
181
### What happens if the fundraiser misses its target?
0 commit comments