What the program already does
A maker opens a fundraiser: "I want to raise 30,000 USDC, and I have 14 days." Anyone can contribute that token, and every contribution lands in a vault the program controls. Nobody, including the maker, can touch it while the clock is running.
When the window closes there are exactly two outcomes. If the vault reached the target,
the maker calls check_contributions and takes the whole pot. If it did not,
every contributor calls refund and takes their own money back. The program
is the thing that makes "all or nothing" true rather than a promise.
What is different about this assignment
The first three assignments had one correct answer. This one does not. You are being asked to design something and then defend it — which means the interesting work happens before you write any Rust.
The rules your feature has to satisfy
- Enforced on-chain. A rule that only your TypeScript obeys is not a rule. If a contributor can get the reward by crafting their own transaction, you have not built anything.
- Provably present. At least one test that passes with your feature and fails without it.
- Nothing else breaks. All four original instructions still work, and the original tests still pass.
- Money math is checked. No bare
+,-or*on token amounts.checked_add,checked_mul, or you explain why it cannot overflow. - Written down. A README section covering what it does, what it costs in rent and compute, and how you would attack it.
What you will be working with
Every step has hints you can open when you want them. The repo has a genuinely good README that walks through every account and constraint — read it before checkpoint 1.
Fork & set up
1 · Fork on GitHub
Open github.com/JavierBonill4/anchor-fundraiser and click Fork.
You now own github.com/<you>/anchor-fundraiser.
2 · Clone your fork — not the original
git clone https://github.com/<you>/anchor-fundraiser.git
cd anchor-fundraiser
3 · Add the original as upstream
git remote add upstream https://github.com/JavierBonill4/anchor-fundraiser.git
git remote -v
# origin https://github.com/<you>/anchor-fundraiser.git (yours — you push here)
# upstream https://github.com/JavierBonill4/anchor-fundraiser.git (theirs — you pull from here)
4 · Branch, and name it after your feature
git switch -c feat/milestone-unlocks
Everyone's branch will be different this time. Name yours for what you are actually building — it is the first thing a reviewer reads.
5 · Check your toolchain
This repo is pinned. If your versions do not match, fix that before anything else — a version mismatch here produces errors that look like bugs in your code.
anchor --version # anchor-cli 1.1.2
solana --version # 3.1 or newer
node --version # 20 or newer
If Anchor is wrong: avm install 1.1.2 && avm use 1.1.2. The program
pins anchor-lang and anchor-spl to exactly
=1.1.2, so the CLI has to match.
6 · Install, build, sync, test
yarn install
anchor build
anchor keys sync # first time only
anchor build
anchor test
anchor keys sync
The repo declares a program id whose keypair it does not ship, so your first
anchor build generates a different one and anchor test fails
with DeclaredProgramIdMismatch. keys sync rewrites
declare_id! and Anchor.toml to match the keypair you just
generated. Run it once, rebuild, and forget about it.
That id change is yours alone — leave it out of your pull request.
tests/fundraiser.ts before you trust it. Every test is a
console.log, and the two that could fail are wrapped in
try/catch that swallows the error and prints it. The suite goes green
whether the program works or not.
That is normal for an example repo and it is a problem for you, because it means "the tests pass" tells you nothing. Checkpoint 6 is where you fix that for your own code.
Hint If anchor test fails before it starts
anchor test builds, boots a local validator, deploys, and runs Mocha. Any
of those four can be what broke:
ANCHOR_PROVIDER_URL is not defined— you rants-mochadirectly instead of throughanchor test. Useanchor test, or exportANCHOR_PROVIDER_URLandANCHOR_WALLETyourself.Address already in use— a validator is still running from last time. Kill it, or useanchor test --skip-local-validatoragainst one you started yourself.- A missing
target/types/fundraiser.ts—anchor buildgenerates it. Build before you test, every time.
anchor build && anchor test runs to the end and prints a vault
balance. Do not start writing until it does; you cannot tell your breakage from theirs
otherwise.
Read the fundraiser
You are about to graft something onto this program. Graft it onto the wrong place and you will fight it for a day.
The files
| File | Holds |
|---|---|
| state/fundraiser.rs | The campaign: maker, mint, target, running total, clock, bump |
| state/contributor.rs | One u64. How much this person has put in. |
| instructions/initialize.rs | Opens a campaign and creates the vault |
| instructions/contribute.rs | The only instruction money flows in through |
| instructions/checker.rs | Success path: maker takes the pot, campaign closes |
| instructions/refund.rs | Failure path: one contributor takes their money back |
| constants.rs | The magic numbers, already named |
| error.rs | Eight errors. You will add to this. |
The two accounts and their seeds
// one per maker — a maker runs one campaign at a time
[b"fundraiser", maker]
// one per person per campaign
[b"contributor", fundraiser, contributor]
The vault is an ordinary associated token account whose authority is the fundraiser PDA. That is the whole trick: a program-derived address can own a token account, so the tokens are held by code rather than by a person.
What each instruction already enforces
| Instruction | Refuses when |
|---|---|
| initialize | the target is not more than 3 whole tokens |
| contribute | the amount is under one whole token; above 10% of the target in one go; the cumulative total for this contributor would pass 10%; the window has closed |
| check_contributions | the vault is below the target |
| refund | the window is still open; the target was met |
Note the symmetry: contribute works while the window is open,
refund works once it has closed, and they can never both be legal at the
same moment. Whatever you build, keep that property — a feature that lets someone
contribute and refund in the same breath is a feature that lets them do it a thousand
times.
Where the money can move
Three transfers exist in the whole program, and it is worth knowing which are signed by a human and which by the PDA:
| Where | From → To | Signed by |
|---|---|---|
| contribute | contributor → vault | the contributor's keypair |
| check_contributions | vault → maker | the fundraiser PDA, by seeds |
| refund | vault → contributor | the fundraiser PDA, by seeds |
If your feature moves or mints anything, it will be a fourth row in that table, and it will almost certainly be signed by the PDA. Checkpoint 5 has the pattern.
Choose your feature
Pick one. Not two. A single feature built properly — validated, tested, documented, with its failure modes understood — is worth more than three half-features, and it is what you will be graded on.
Four worked options are below, each with the state it needs, where it hooks in, and the specific thing that will go wrong. Read all four even if you already know which one you want; the traps generalise.
Option A Milestones at a quarter, a half, three quarters
The idea. Something happens the moment the vault crosses 25%, 50% and 75% of the target. What "something" is, is up to you: release a tranche of the money to the maker early, raise or lower the per-contributor cap, unlock a bonus for everyone who got in before the halfway mark, or simply emit an event a front end can listen for.
Difficulty: moderate. Best for: most people — it is the only option here with no external dependency and no clock problem.
State you need. One byte on Fundraiser. A bitmask is
the tidy version:
pub milestones_fired: u8, // bit 0 = 25%, bit 1 = 50%, bit 2 = 75%
Where it hooks. The end of contribute, after
current_amount has been updated. Nothing else changes the total upward.
if
statements that each assume they run alone.
Worth knowing:
contribute caps any one person at 10% of the target, so a
single contribution moves the total by at most 10 percentage points and can never skip
a whole quarter. Write the loop anyway — it is the correct shape, and it stays correct
if someone changes the cap.
Second trap. refund reduces current_amount.
Decide deliberately whether a milestone can un-fire, and write your decision in the
README. ("Milestones are one-way; a refund does not retract one" is a perfectly good
answer, and it is a better answer than not having thought about it.)
Option B A lottery ticket for every contribution
The idea. Contributing buys tickets in proportion to the amount. When the fundraiser succeeds, one ticket wins — a slice of the pot, a special NFT, bragging rights.
Difficulty: hard, and the hard part is not the code.
State you need. A running total_tickets on
Fundraiser, and on Contributor the range of ticket
numbers they own:
pub ticket_start: u64,
pub ticket_end: u64, // exclusive
Contributor PDA on-chain and pick one. Ranges invert the problem: you
draw a number, and the winner proves they won by passing their own account, which
you check contains the number. The program never needs to know who is playing.
The real problem: randomness. Solana has none. Every value a program
can read is a value the caller could read first, off-chain, before deciding whether to
send the transaction at all. A naive draw from Clock means anyone can
simulate, see that they lose, and simply not submit — then try again next slot.
Three answers, in increasing order of honesty:
- SlotHashes sysvar. Readable on-chain, contains recent block hashes, much better than
Clock. Still influenced by whoever is producing blocks. Cheap and standard for low-stakes draws. - Commit–reveal. The maker commits
hash(secret)atinitializeand revealssecretat draw time; the winner is derived from the secret combined with something the maker cannot control. The maker can refuse to reveal, so pair it with a deadline. - A VRF — Switchboard or ORAO. This is what production does, and integrating one is a whole assignment by itself.
Pick one, implement it, and write down in your README exactly how it can be gamed. A correct description of your own scheme's weakness will score better than a scheme presented as secure that is not.
Option C An NFT receipt for contributing
The idea. Your first contribution mints you a one-of-one token — proof you backed this campaign, held in your wallet, transferable or not as you choose.
Difficulty: moderate if you keep it simple, hard if you reach for Metaplex.
The simple version, and the one to start with: an NFT is just a mint with 0 decimals and a supply of exactly 1. Derive the mint as a PDA so you never have to store its address:
[b"receipt", fundraiser.key(), contributor.key()]
Mint authority is the fundraiser PDA. On a contributor's first contribution, create the
mint and their ATA with init_if_needed, mint 1, and — if you want it to be
a true one-of-one — set the mint authority to None afterwards so no more
can ever exist.
The better version. You already did Token-2022 metadata in the last
assignment. A receipt with a metadata_pointer back to itself, carrying a
name and a URI, is a genuinely nice artifact and reuses work you have already done.
The ambitious version. Metaplex Token Metadata via CPI. Be warned that
mpl-token-metadata and anchor-spl have version opinions
about each other, and the program's dependencies are pinned to exactly
=1.1.2 — so you may spend your afternoon in Cargo.toml
rather than in your program.
Option D A reward token, minted as you contribute
The idea. Contribute 100 USDC, receive 100 (or 10, or 1,000) CAMPAIGN tokens. A governance token, a points balance, a claim on something later.
Difficulty: moderate, and the gentlest introduction to a PDA acting as a mint authority.
State you need. A reward_mint: Pubkey on
Fundraiser, chosen at initialize, whose mint authority is the
fundraiser PDA. Optionally a reward_rate if you do not want to hardcode it.
Where it hooks. contribute, right after the transfer
into the vault succeeds.
Then: if someone contributes, receives rewards, and refunds, they have been paid for nothing. Burn on refund, or make rewards claimable only after the campaign succeeds. Both are defensible; minting eagerly and ignoring the problem is not.
Option E Something else entirely
Encouraged, and it has to clear the same bar. Ideas that have worked well:
- Matching funds. A sponsor deposits a pool at the start that matches contributions 1:1 up to a limit.
- Refund insurance. A small fee on each contribution funds a pool that pays contributors a little extra if the campaign fails.
- Tiers. Contribute above a threshold and you are a "founding backer" — recorded on-chain and unforgeable.
- An early-bird curve. Rewards that decay with time or with how full the vault already is.
- Multiple campaigns per maker. Add a campaign id to the seeds and work out everything that breaks.
Run it past your TA before you start. Not for permission — to catch the case where the idea is secretly three assignments, which is the most common way this goes wrong.
Design the state
Almost every feature needs to remember something. Where you put it decides how much rent it costs, who pays, and how painful it is to change later.
Three places it can live
| Where | Good for | Costs |
|---|---|---|
A field on Fundraiser | Anything campaign-wide: milestone flags, total tickets, a reward mint | Rent paid once by the maker |
A field on Contributor | Anything per-person: ticket ranges, tier, rewards claimed | Rent paid by each contributor |
| A brand-new PDA | Things with their own lifecycle — a receipt mint, a milestone record, a claim ticket | Another account, another set of seeds, another thing to validate |
Start with a field. Reach for a new PDA only when the thing genuinely has a life of its own — it gets created and closed at different times from its parent, or there can be many of them per parent.
Adding a field
#[derive(InitSpace)] recalculates the account size for you, and
space = ANCHOR_DISCRIMINATOR + Fundraiser::INIT_SPACE picks it up with no
further edits. Add the field, rebuild, done.
New fields go at the end. Always. Even when the middle is where they logically belong.
Hint Choosing seeds for a new PDA
Seeds are how your program answers "which one of these is this?". The existing ones are a good model to copy:
[b"fundraiser", maker] // one per maker
[b"contributor", fundraiser, contributor] // one per person per campaign
Read them as a sentence. "One X per Y per Z" — and the
seeds are literally [b"x", y, z]. If you want one receipt per contributor
per campaign, your seeds write themselves.
Include a literal prefix so two different kinds of account can never collide, and
store the bump in the account so you are not paying for
find_program_address on every instruction.
Hint init_if_needed is already enabled
The feature is on in Cargo.toml, and contribute already uses
it for the contributor account — which is how a second contribution from the same
person reuses the first account instead of failing.
Use it, and remember what it does not do: it will not re-run your initialisation. An account that already exists arrives with its old contents, so any "set this up" logic you write has to be safe to skip, or guarded by a flag you check yourself. This is the single most common source of "it works the first time" bugs in Anchor.
anchor build is clean and the original tests still pass. You have changed the
shape of an account and nothing else; if something broke, it broke now, while the change
is one line and you remember making it.
Pick your moment
Your feature fires at some moment. There are only three kinds of moment on Solana, and choosing the wrong one is the most expensive mistake available to you here.
| Pattern | What it means | Use when |
|---|---|---|
| Inline | Extra logic at the end of an existing instruction | The trigger is something that instruction just did |
| A new instruction | Someone calls it deliberately | The user is claiming something, or the maker is settling up |
| A crank | A new instruction anyone may call, that checks a condition and acts | The trigger is time, or nobody in particular owns it |
The worked example: a milestone, inline
Milestones are the easiest case because the trigger is a number that only
contribute changes. So the logic goes at the end of
contribute, after the total has moved:
// what fraction are we at now, in quarters? 0, 1, 2, 3 or 4
let quarters = self.fundraiser.current_amount
.checked_mul(4).ok_or(FundraiserError::Overflow)?
/ self.fundraiser.amount_to_raise;
for i in 0..quarters.min(3) {
let flag = 1u8 << i;
if self.fundraiser.milestones_fired & flag == 0 {
self.fundraiser.milestones_fired |= flag;
// ... whatever the milestone actually does ...
}
}
Read what that does and does not do. It computes where you are, not what changed — so a contribution that leaps from 20% to 60% fires both the quarter and the half. And the flag check means each one fires once, ever, no matter how many times the loop runs.
Integer arithmetic, since there is no other kind
- There are no floats in a Solana program.
0.25 * targetis not available to you;target / 4is. - Multiply before you divide.
current * 4 / targetkeeps precision;current / target * 4is zero until the very end. - Multiply-before-divide is also where overflow lives.
checked_mul, and a real error variant, notunwrap(). overflow-checks = trueis already set in this repo's release profile, so a bare+that overflows panics rather than wrapping. A panic is a failed transaction with an unhelpful log — better than silent corruption, worse than a named error.
Hint Making something happen exactly once
Anything triggered by a condition rather than by a caller needs a "has this already
happened" flag, because the condition stays true after the first time. A
bool per event, or one u8 used as a bitmask for up to eight
of them.
Set the flag before you do the work, not after. If the work is a CPI that fails, the whole instruction unwinds and the flag goes back with it — so ordering costs you nothing and protects you from the case where the work partially succeeds in some future version of your code.
Hint If your trigger is time, read this before checkpoint 6
Time-based features are harder to test than amount-based ones, and the
difficulty has nothing to do with your program. anchor test runs against
a real local validator whose clock is the real clock, and there is no RPC call that
moves it. You cannot wait out a 14-day campaign in CI.
Your options are to trigger on amounts instead, to use a duration short enough to sit
through, or to switch your tests to solana-bankrun, which can set the
clock directly. Checkpoint 6 covers the third one. Decide now, because it is much
cheaper than discovering it at the end.
Build it
You know what it is, where the state lives and when it fires. This checkpoint is the mechanics you are most likely to need, in the order you will need them.
Adding an instruction
Four edits, the same four every time:
- A file in
instructions/with an#[derive(Accounts)]struct and animplblock. pub modandpub uselines ininstructions/mod.rs.- A handler in the
#[program]module inlib.rs. - An error variant or two in
error.rs, with a#[msg]a human can read.
Making the PDA sign
If your feature mints, transfers or closes anything owned by the fundraiser, the PDA has
to authorise it. checker.rs already does this and is worth copying from:
let signer_seeds: [&[&[u8]]; 1] = [&[
b"fundraiser".as_ref(),
self.maker.to_account_info().key.as_ref(),
&[self.fundraiser.bump],
]];
let cpi_ctx = CpiContext::new_with_signer(
self.token_program.key(), // an address, not an AccountInfo
cpi_accounts,
&signer_seeds,
);
The seeds must be exactly the derivation, in order, bump last. Get one element wrong and you do not get an error explaining that — you get a different address, and the runtime declines to sign for an account you never asked about.
CpiContext::new and new_with_signer take the program's
address — self.token_program.key() — where older code passed
self.token_program.to_account_info(). Every tutorial and StackOverflow answer
written before Anchor 1.0 has the old form, and the compiler error it produces
(a type mismatch on Pubkey) does not explain why.
Hint Minting tokens from the program (options C and D)
Create the mint in initialize (for a campaign-wide reward token) or with
init_if_needed in contribute (for a per-contributor receipt),
with mint::authority = fundraiser. Then:
use anchor_spl::token::{mint_to, MintTo};
mint_to(
CpiContext::new_with_signer(
self.token_program.key(),
MintTo {
mint: self.reward_mint.to_account_info(),
to: self.contributor_reward_ata.to_account_info(),
authority: self.fundraiser.to_account_info(),
},
&signer_seeds,
),
reward_amount,
)?;
For a true one-of-one, follow the mint with set_authority to
None. An NFT whose mint authority still exists is a promise, not a
guarantee.
Hint Reading SlotHashes for a draw (option B)
The sysvar is too large for Anchor to deserialize, so you take it as an unchecked account pinned to the right address and read the bytes yourself:
/// CHECK: address-checked below; read as raw bytes
#[account(address = anchor_lang::solana_program::sysvar::slot_hashes::ID)]
pub slot_hashes: UncheckedAccount<'info>,
The first 8 bytes are the entry count; each entry after that is a slot
(u64) followed by a 32-byte hash, most recent first. Take the bytes you
want, hash them with something else, reduce modulo total_tickets.
And then write the paragraph in your README explaining that a block producer can influence this. The paragraph is the deliverable.
Hint Emitting an event so a front end can react
Milestones are much more useful if something can listen for them:
#[event]
pub struct MilestoneReached {
pub fundraiser: Pubkey,
pub quarter: u8,
pub amount: u64,
}
emit!(MilestoneReached { fundraiser: self.fundraiser.key(), quarter, amount });
Events are program logs, not state. They are cheap, they show up in the transaction, and they are gone once nobody is indexing them — so emit them as well as recording what matters on-chain, never instead of it.
anchor build is clean, the original tests still run, and you can describe out
loud what happens if your new instruction is called twice in a row. If you cannot answer
that last one, go back to the idempotency hint in checkpoint 4.
Prove it
The suite you inherited logs and never asserts. Yours will not, because an assertion is the only part of a test that can tell you that you are wrong.
Three tests, minimum
- The happy path. Do the thing that should trigger your feature. Assert on the resulting on-chain state — a balance, a flag, a new account — not on the transaction succeeding.
- The boundary. One unit below the trigger, nothing happens. Exactly at it, it does. This is the test that distinguishes
>from>=, and it is the one that catches real bugs. - The abuse case. Try to get the reward twice, or without qualifying, or as the wrong person. Assert that it fails with your error.
try { ... } catch { console.log(e) } passes whatever happens, which is why
the shipped suite is green. Even expect(tx).to.be.rejected passes when your
transaction fails because you mistyped an account name.
Assert on the error code Anchor gives you:
try {
await program.methods.claimReward()/* ... */.rpc();
assert.fail("expected the second claim to be rejected");
} catch (err) {
assert.strictEqual(err.error.errorCode.code, "RewardAlreadyClaimed");
}
Note the assert.fail inside the try. Without it, a test that
should have thrown and did not still passes — the most common broken test in the world.
Hint Testing anything that depends on time
anchor test cannot move the clock. solana-bankrun can — it
runs the program against an in-process bank instead of a validator, and lets you set
the sysvar directly.
Both packages are already in package.json, so
yarn install is all you need.
import { startAnchor, BankrunProvider } from "anchor-bankrun";
import { Clock } from "solana-bankrun";
const context = await startAnchor("", [], []);
const provider = new BankrunProvider(context);
anchor.setProvider(provider);
// ... run initialize and a contribution ...
const clock = await context.banksClient.getClock();
context.setClock(new Clock(
clock.slot,
clock.epochStartTimestamp,
clock.epoch,
clock.leaderScheduleEpoch,
clock.unixTimestamp + 15n * 86_400n, // fifteen days later
));
Every field is a bigint. Changing the clock affects everything from that
point on, so create the campaign first and then move time — moving it first only
changes what gets stamped into time_started.
Hint Reading your new state from TypeScript
Anchor regenerates the IDL on every build, so a field you added in Rust is available in TypeScript with no client work at all:
const account = await program.account.fundraiser.fetch(fundraiser);
assert.strictEqual(account.milestonesFired, 3); // 25% and 50%
Anchor camel-cases field names on the way out. If the field is missing or
undefined, you are running against a stale IDL — rebuild.
Open the PR
Everyone's submission is different this time, so the description does more work than usual. A reviewer opening your PR has no idea what you decided to build.
1 · Write the README section first
This is graded, and it is where most of the thinking shows. Four short sections:
- What it does — in plain language, for someone who has not read your code.
- How it works — the state you added, where it fires, the accounts involved.
- What it costs — extra rent, extra accounts, roughly what it added to compute.
- How you would attack it — the honest one. Every design has a soft spot; naming yours is the point of the exercise.
2 · Look before you stage
git status
git diff
.gitignore covers target/, node_modules/ and
.anchor/, so git add . will not drown your pull request. It will
still happily include things you did not mean to send — the program id
anchor keys sync rewrote, a scratch test, a commented-out experiment.
Name your files explicitly. It takes ten seconds and it is the difference between a reviewable diff and one nobody wants to open.
3 · Commit in readable pieces
git add programs/fundraiser/src/state programs/fundraiser/src/error.rs
git commit -m "Add milestone tracking to the fundraiser account"
git add programs/fundraiser/src/instructions programs/fundraiser/src/lib.rs
git commit -m "Fire milestone events at the quarter marks"
git add tests readme.MD
git commit -m "Test milestone firing and document the design"
Imperative mood — "Add", not "Added". State, behaviour, tests and docs are separate ideas, and a reviewer should be able to read any one of them alone.
4 · Sync, then push
git fetch upstream
git rebase upstream/main
git push -u origin feat/milestone-unlocks
5 · Open it in the right direction
| Field | Value |
|---|---|
| base repository | JavierBonill4/anchor-fundraiser |
| base branch | main |
| head repository | <you>/anchor-fundraiser |
| compare branch | feat/<your-feature> |
6 · Write a description worth reading
## What I built
Milestone events at 25%, 50% and 75% of the target. Each fires once,
even when a single contribution crosses two of them, and a refund does
not retract one.
## Why
A campaign at 60% looks very different to a prospective backer than one
at 10%, and right now nothing on-chain marks the difference. A front end
can subscribe to the event instead of polling the vault balance.
## How
- `milestones_fired: u8` appended to `Fundraiser` — one bit per mark
- computed at the end of `contribute` as `current * 4 / target`
- `emit!(MilestoneReached { .. })` guarded by the bit, so it is idempotent
- new error `Overflow` for the checked multiplication
## Where it is weak
`current_amount` can fall after a refund, so `milestones_fired` can claim
a mark the vault no longer justifies. I chose one-way milestones on
purpose — a mark that flickers is worse than one that is generous — but a
front end should read the vault, not the flags, for a live percentage.
## Testing
`anchor test` — the original tests still pass, plus three new ones: a
contribution that crosses two marks at once, the exact boundary at 25%,
and a repeat contribution that must not fire anything twice.
7 · After you submit
- Leave "Allow edits by maintainers" checked.
- Respond to review by pushing more commits to the same branch — the PR updates itself. Never open a second one.
- Do not force-push once review has started unless asked; it invalidates comments people already left.
- Reply to every comment, even just to say you have done it. Silence reads as disagreement.
Troubleshooting
Error ANCHOR_PROVIDER_URL is not defined
AnchorProvider.env() reads two environment variables that
anchor test sets for you. Run anchor test rather than
ts-mocha directly, or export them yourself:
export ANCHOR_PROVIDER_URL=http://127.0.0.1:8899
export ANCHOR_WALLET=~/.config/solana/id.json
Error does not exist in type ResolvedAccounts<…>
Anchor resolves some accounts for you and refuses to let .accounts()
set them. Use .accountsPartial({ ... }), which the shipped tests already
do. If you copied a snippet from pre-0.30 docs, this is why it does not compile.
Error A field you added is undefined in TypeScript
The IDL is regenerated by anchor build and the TypeScript types come from
it. Rebuild, and check target/types/fundraiser.ts actually contains your
field. Remember Anchor camel-cases on the way out:
milestones_fired becomes milestonesFired.
Error AccountDidNotDeserialize, or garbage in your new field
You inserted a field in the middle of an account struct rather than appending it, and you are reading an account created before the change. Every byte after the insertion point has shifted.
Append the field instead. In tests, a fresh validator makes this invisible; on devnet it is permanent.
Error Cross-program invocation with unauthorized signer
The seeds you passed do not derive the account you are signing for. Compare them
against the derivation one element at a time —
b"fundraiser", the maker's key, the bump — in that order, bump last,
nothing extra.
If you are minting, check the mint's authority is actually the fundraiser PDA and not the maker.
Error ContributionTooSmall or InvalidAmount in your own tests
Both minimums are in whole tokens, scaled by the mint's decimals. On a 6-decimal
mint that is 1_000_000 raw units per token — so a contribution must be
at least 1_000_000, and a target must be more than
3_000_000.
Contributions are also capped at 10% of the target per contributor, so a target of 30 tokens allows at most 3 tokens from any one person. If you are writing a test that needs to fill the vault, use several contributors.
Error Your feature fires twice
The condition that triggered it is still true afterwards. Conditions do not consume themselves — you need a flag, checked and set in the same instruction. Checkpoint 4.
Error Your test passes with the feature commented out
Then it is testing the program you already had. Usually one of: asserting the
transaction succeeded rather than asserting on state; a try/catch with no
assert.fail in the try; or reading a value that would have
been that number anyway.
Deleting your own logic and watching the suite go red is the only way to know. Do it before you open the PR, not after someone asks.
Error The clock will not move
It will not — anchor test uses a real validator and there is no RPC to
set the time. Either make your trigger amount-based, use a duration short enough to
wait out, or move the suite to solana-bankrun. Checkpoint 6 has the
setup.
Solana Summer · fundraiser · program Eoiuq1dXvHxh6dLx3wh9gj8kSAUpga11krTrbfF5XYsC · submit by pull request