Sample audits · Open-source projects and documentation, eight more

solana-foundation/program-examples

Example Solana programs that developers copy as starting points.

Auditedsolana-foundation/program-examples at commit de090195e4de698e150572c4f886c333323a64a5
Date11 October 2026
How it rancloud session, full audit, Standard review
Verdict after reviewFail (rule: Fail if a High finding remains after review, otherwise Pass with notes)
2
High after review
6
Medium after review
43
Low after review
3
Info after review
0
excluded on review
How to read this page

Each finding keeps the number it has in the audit report. The rating shown first is the one after review; the first automated rating is listed with it. 32 findings were first rated Medium or High; 8 of them are Medium or High after review. Text marked "From the report" is quoted from the audit report; fixes are suggestions and were not tested. Code locations link to the file and line at the audited commit.

High after review (2)

31. High cNFT vault: anyone can withdraw the vault's NFTs to any address
First rating: High · Reviewed rating: High · Review: confirmed
From the report
Evidence
/// CHECK: This account is neither written to nor read from.
pub new_leaf_owner: UncheckedAccount<'info>,
Why it matters

Withdraw and WithdrawTwo have no Signer and no stored authority. The vault PDA signs the Bubblegum transfer for every caller, and the caller picks the recipient, so any wallet can take every cNFT in the vault.

Suggested fix, not tested

Store an owner or admin key at setup and require that signer (has_one or address). Pin bubblegum_program to the Bubblegum program ID.

Review: Withdraw and WithdrawTwo (lib.rs:269-330) contain no Signer and no stored authority; leaf_owner is only the cNFT-vault PDA, which invoke_signed at lines 141-145 and 225/262 signs for whoever calls. new_leaf_owner is an arbitrary UncheckedAccount ("neither written to nor read from"), so the caller picks the recipient. Textbook missing authorization on a program-signed asset transfer; the example's whole point is a vault, so a copy inherits it.

Upstream: No matching upstream report found (11 October 2026).

32. High Fundraiser: the maker can fake reaching the goal and take contributors' funds
First rating: High · Reviewed rating: High · Review: confirmed
From the report
Evidence
self.vault.amount >= self.fundraiser.amount_to_raise,
...
transfer(cpi_ctx, self.vault.amount)?;
Why it matters

The goal check uses the vault's token balance, which anyone can raise by sending tokens straight to it. The maker can top up the vault temporarily, pass the check and sweep everything, including the contributions of a campaign that never reached its target. Contributors then cannot get a refund. The refund check in refund.rs:66 also reads vault.amount.

Suggested fix, not tested

Compare fundraiser.current_amount, which only contribute and refund change, against the target in both places.

Review: checker.rs:53-56 requires vault.amount >= amount_to_raise, then sweeps vault.amount (line 80) and closes fundraiser (close = maker, line 28). The vault is a plain ATA, so anyone, including the maker inside the same transaction, can top it up, pass the check, and take the entire vault; the top-up comes back in the sweep, so the maker risks nothing. After fundraiser is closed contributors cannot refund. refund.rs:66 reads vault.amount too, so a donation can also block refunds. current_amount exists and is the right counter. Upstream: no match found (related: #725, #674 are different problems).

Upstream: No exact upstream report; related items: #725 (issue, open), #685 (PR, merged).

Medium after review (6)

9. Medium Realloc (Pinocchio): any caller can overwrite and resize any record owned by the program
First rating: High · Reviewed rating: Medium · Review: rated too high
From the report
Evidence
let [target_account] = accounts else { ... };
target_account.resize(account_span)?;
target_account_data.copy_from_slice(data);
Why it matters

There is no signer, no ownership relation and no record-type tag. Anyone can pass another user's account and replace its contents with a different record type, which destroys that user's data. Two of the three record types are 25 bytes long, so size alone cannot tell them apart.

Suggested fix, not tested

Start every record with a one-byte type tag and reject any type other than the one expected. Before resizing or writing, require an authority signature or a PDA derived from the owner.

Review: Pinocchio reallocate_zero_init (reallocate.rs:31-44) and native (37-48) take one account, no signer, no owner field, and resize/overwrite it; true. But there is no stored authority concept anywhere in this example: the Anchor sibling (Update in basics/realloc/anchor) only requires a payer signer and no relation between payer and account, so it is equally open, and the records are address/work demo data with no funds. Not HIGH. The report's type-confusion remark is accurate for same-size records. Upstream: no match found.

Upstream: No matching upstream report found (11 October 2026).

10. Medium Realloc (native): the same overwrite, plus records of one type read as another
First rating: High · Reviewed rating: Medium · Review: rated too high
From the report
Evidence
let target_account = next_account_info(accounts_iter)?;
target_account.resize(account_span)?;
data.serialize(&mut &mut target_account.data.borrow_mut()[..])?;
Why it matters

reallocate_zero_init overwrites any account the program owns. reallocate_without_zero_init (line 18) parses any 25-byte account as an address record, including a work record.

Suggested fix, not tested

As in finding 9, add a type tag, verify it, and require an owner or authority before any change.

Review: Pinocchio reallocate_zero_init (reallocate.rs:31-44) and native (37-48) take one account, no signer, no owner field, and resize/overwrite it; true. But there is no stored authority concept anywhere in this example: the Anchor sibling (Update in basics/realloc/anchor) only requires a payer signer and no relation between payer and account, so it is equally open, and the records are address/work demo data with no funds. Not HIGH. The report's type-confusion remark is accurate for same-size records. Upstream: no match found.

Upstream: No matching upstream report found (11 October 2026).

29. Medium PDA rent payer (native): anyone can drain the rent vault
First rating: High · Reviewed rating: Medium · Review: rated too high
From the report
Evidence
**rent_vault.lamports.borrow_mut() -= lamports_required_for_rent;
**new_account.lamports.borrow_mut() += lamports_required_for_rent;
Why it matters

There is no signer check and no account is ever created. Each call moves the rent minimum from the vault to any account the caller names, so repeated calls empty the vault.

Suggested fix, not tested

Require new_account to sign and create it with a system create_account CPI signed by the vault PDA, as the Anchor variant does. Use checked arithmetic, and return an error instead of assert!.

Review: Native lines 25-26 and Pinocchio 29-30 move lamports out of the rent_vault PDA to any account with no signer check; true. But the Anchor sibling is also open: new_account there is any fresh keypair that signs, and the CPI signed by the vault funds it, so any caller can still drain rent_vault (the report's fix "as the Anchor variant does" would not close it). The drain is bounded to funds above the vault's rent minimum (the runtime rejects an under-funded program-owned account). Upstream: issue #671 (open, "create_new_account has no caller check - worth a note for people copying it?") and PR #732 (closed, docs caution note) show maintainers know and treat it as a documentation matter.

Upstream: Already reported upstream: #671 (issue, open), #732 (PR, closed unmerged).

30. Medium PDA rent payer (Pinocchio): the same vault drain
First rating: High · Reviewed rating: Medium · Review: rated too high
From the report
Evidence
rent_vault.set_lamports(rent_vault.lamports() - lamports_required_for_rent);
new_account.set_lamports(new_account.lamports() + lamports_required_for_rent);
Why it matters

Same as finding 29. In addition, instruction_data[0] at line 18 panics on empty input.

Suggested fix, not tested

Same as finding 29. Also check the instruction data length.

Review: Native lines 25-26 and Pinocchio 29-30 move lamports out of the rent_vault PDA to any account with no signer check; true. But the Anchor sibling is also open: new_account there is any fresh keypair that signs, and the CPI signed by the vault funds it, so any caller can still drain rent_vault (the report's fix "as the Anchor variant does" would not close it). The drain is bounded to funds above the vault's rent minimum (the runtime rejects an under-funded program-owned account). Upstream: issue #671 (open, "create_new_account has no caller check - worth a note for people copying it?") and PR #732 (closed, docs caution note) show maintainers know and treat it as a documentation matter.

Upstream: Already reported upstream: #671 (issue, open), #732 (PR, closed unmerged).

35. Medium Token swap: new liquidity providers are minted LP tokens out of proportion to the pool
First rating: Medium · Reviewed rating: Medium · Review: confirmed
From the report
Evidence
let mut liquidity = (amount_a as u128)
    .checked_mul(amount_b as u128)
    .unwrap()
Why it matters

LP tokens are computed as sqrt(a·b) of each deposit, not as a share of the existing reserves. Once fees have grown the pool, new depositors are over-credited at the expense of existing LPs.

Suggested fix, not tested

After the first deposit, mint min(a·supply/pool_a, b·supply/pool_b).

Review: deposit_liquidity.rs:68-71 mints isqrt(amount_a*amount_b) for every deposit, not a share of the existing LP supply. swap_exact_tokens_for_tokens.rs:31-36 takes a fee that stays in the pool, so sqrt(k) grows above supply, and later depositors are over-credited at the expense of existing LPs. Real economic bug in a copyable AMM, no direct theft, hence MEDIUM. Upstream: no match found.

Upstream: No exact upstream report; related items: #567 (PR, closed unmerged), #690 (PR, merged).

39. Medium NFT operations: collection verification ignores the declared authority
First rating: Medium · Reviewed rating: Medium · Review: confirmed
From the report
Evidence
pub authority: Signer<'info>,
Why it matters

Any signer is accepted. The program's PDA then verifies any metadata account into its collection, so anyone can add a foreign NFT to the collection.

Suggested fix, not tested

Constrain authority to a stored admin, and constrain metadata to NFTs this program minted.

Review: Any signer can have the program verify any NFT that merely claims this collection. A verified-collection mark is the trust signal, so MEDIUM.

Upstream: Already reported upstream: #693 (PR, open).

Low after review (43)

1. Low Escrow (native): the taker can redirect the offer account's rent to an account they choose
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
**offer_info.lamports.borrow_mut() -= lamports;
**payer.lamports.borrow_mut() += lamports;
Why it matters

payer is never checked. It does not have to sign and does not have to match whoever funded the offer. A taker can pass their own wallet and collect the rent reserve the maker paid.

Suggested fix, not tested

Send the rent back to the maker, as the Anchor version does with close = maker. Alternatively, store the funder in the Offer at creation and require payer.key == offer.payer.

Review: payer unchecked (native take_offer.rs:200-204; Pinocchio :144-148, whose doc comment says [signer] but the code never checks it); loss is one offer account's rent (~0.002 SOL), and the taker already receives the vault rent. 3: chop_tree.rs:87-89 has a misleading CHECK comment and no mint validation, but the write is fixed to the key "wood" with the caller's own count and only works on mints whose update authority is this program's PDA; cosmetic NFT metadata griefing. 23: eight prepare.mjs run solana config set -um from postinstall (confirmed, incl. the one safe copy in create-token/pinocchio); a real unannounced global-config side effect but deploying to mainnet still needs a funded wallet. 33: 1_u64.pow(n) is always 1 (contribute.rs:61), functional bug, no loss. 34: vault never closed (rent stranded, re-initialize blocked); upstream #725/#731 cover this. 36: one-sided donation to an empty pool leaves pool_a == 0 so checked_div(..).unwrap() panics (deposit_liquidity.rs:46-50); a pool-seeding DoS, cheap but limited to unseeded pools. 37/38: native/Pinocchio mint have no admin check (Anchor mint.rs has mint_config.admin), so a front-runner can receive another user's NFT between create and mint; low value.

2. Low Escrow (Pinocchio): the same rent redirection
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
let new_payer_lamports = payer.lamports() + offer_account.lamports();
payer.set_lamports(new_payer_lamports);
offer_account.close()?;
Why it matters

As in finding 1, the taker picks which account receives the closed offer's lamports.

Suggested fix, not tested

Refund the maker, or check the account against a funder recorded in the offer.

Review: payer unchecked (native take_offer.rs:200-204; Pinocchio :144-148, whose doc comment says [signer] but the code never checks it); loss is one offer account's rent (~0.002 SOL), and the taker already receives the vault rent. 3: chop_tree.rs:87-89 has a misleading CHECK comment and no mint validation, but the write is fixed to the key "wood" with the caller's own count and only works on mints whose update authority is this program's PDA; cosmetic NFT metadata griefing. 23: eight prepare.mjs run solana config set -um from postinstall (confirmed, incl. the one safe copy in create-token/pinocchio); a real unannounced global-config side effect but deploying to mainnet still needs a funded wallet. 33: 1_u64.pow(n) is always 1 (contribute.rs:61), functional bug, no loss. 34: vault never closed (rent stranded, re-initialize blocked); upstream #725/#731 cover this. 36: one-sided donation to an empty pool leaves pool_a == 0 so checked_div(..).unwrap() panics (deposit_liquidity.rs:46-50); a pool-seeding DoS, cheap but limited to unseeded pools. 37/38: native/Pinocchio mint have no admin check (Anchor mint.rs has mint_config.admin), so a front-runner can receive another user's NFT between create and mint; low value.

3. Low chop_tree accepts any mint and updates its metadata with the program's authority
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
/// CHECK: Make sure the ata to the mint is actually owned by the signer
#[account(mut)]
pub mint: AccountInfo<'info>,
Why it matters

The comment promises an ownership check, but no such check exists. The program's PDA signs a Token-2022 update_field on whatever mint the caller passes. Any player can therefore overwrite the "wood" field of any NFT this program controls, not just their own.

Suggested fix, not tested

Type the account as InterfaceAccount<Mint> constrained to the Token-2022 program. Bind it to the player, either with a stored mint plus has_one, or by requiring the signer's token account for this mint to hold 1. Check the metadata-pointer and update-authority values too.

Upstream: No matching upstream report found (11 October 2026).

4. Low Allow/block-list transfer hook trusts every account it is given
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
/// CHECK:
pub mint: UncheckedAccount<'info>,
Why it matters

The hook does not check that the mint belongs to Token-2022, that a transfer is in progress, or that the list accounts are the expected PDAs owned by this program. The allow/deny decision rests on accounts the caller supplies.

Suggested fix, not tested

Add the transferring-flag check that the account-data-as-seed example already uses. Check the mint's owner. Constrain the list accounts with seeds, a bump and an owner check.

5. Low Block-list hook (Pinocchio) skips owner and length checks before reading raw offsets
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
/// 5- given all the above we can skip a lot of type and owner checks
let owner = unsafe { &*(source_data[32..64].as_ptr() as *const Address) };
Why it matters

The code assumes Token-2022 has already validated the inputs. A direct call with short or foreign accounts can make it misread data or panic.

Suggested fix, not tested

Before slicing, check that the source and destination accounts are owned by Token-2022 and are long enough. Check that the block accounts are owned by this program.

6. Low Wallet bridge returns a partly signed transaction as if signing succeeded
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
for (const [address, signature] of Object.entries(signed.signatures)) {
    if (signature) {
        signedTransaction.addSignature(new PublicKey(address), Buffer.from(signature));
Why it matters

Missing signatures are skipped silently. The failure then surfaces later as an opaque send error instead of a clear message, for example when the user rejected the request.

Suggested fix, not tested

After copying, check that every required signer, at least the fee payer, has a signature. Throw a clear error if one is missing.

7. Low Solana client library pinned to latest in one package and ^6.9.0 in its siblings
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
"@solana/kit": "latest"
Why it matters

The next install can pull a new major version into the scripts while the web app and API stay on 6.x.

Suggested fix, not tested

Use the same pinned range in every sub-package.

8. Low Wallet connect has no check for an insecure origin
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
await connect(connectorId);
Why it matters

We searched both apps for isSecureContext and location.protocol and found neither. Served over plain HTTP from a host other than localhost, common browser wallets refuse to connect, and the user sees nothing.

Suggested fix, not tested

Check the origin on load and show an explicit message. Deploy over HTTPS.

11. Low The sleep() test helper never waits
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
export async function sleep(seconds: number) {
    new Promise(resolve => setTimeout(resolve, seconds * 1000));
Why it matters

The promise is neither returned nor awaited, so the function returns at once. Anyone who relies on it for timing gets no delay.

Suggested fix, not tested

Write return new Promise(...), or delete the helper.

12. Low The settlement operator polls getProgramAccounts in a tight loop
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
for (;;) {
    try { await crankOnce(client, operatorSigner, operatorSeed, pool);
Why it matters

Every cycle runs a full account scan, which is costly on RPC quotas and adds latency.

Suggested fix, not tested

Drive the crank from an account or log subscription. Keep a slow poll only to catch missed events.

13. Low A failed settlement is retried every cycle with no backoff
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
} catch (err) {
    console.error(`  ✗ failed to settle ${pubkey}:`, err);
}
Why it matters

A pull that cannot be settled is rebuilt and resent forever, which wastes RPC calls and fees and floods the logs.

Suggested fix, not tested

Track failures per pull and apply exponential backoff or a retry cap.

14. Low The on-chain pull account layout is copied by hand into several scripts
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
const PULL_ACCOUNT_SIZE = 220n;
const PULL_POOL_OFFSET = 4n;
Why it matters

These values match today, but nothing ties them to the program, so a layout change will silently break the account filters.

Suggested fix, not tested

Export the size and offsets from the generated client, or add a test that compares them with the program's layout.

15. Low The Token-2022 extension parser walks caller data with no per-step bounds check
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
let ext_bytes = &acc_data_bytes[EXTENSION_DATA_OFFSET..];
let ext_len = unsafe { &*(ext_bytes[ext_len_idx..].as_ptr() as *const u16) };
Why it matters

A truncated entry panics or reads outside the intended range. The u16 read is unaligned, and the result is cast to an address without a length check.

Suggested fix, not tested

Read lengths with from_le_bytes on checked slices and verify idx + len <= data.len(). Check that the returned slice holds at least 32 bytes. Cap the number of entries the loop walks.

16. Low The world-cup dev API packs several independent concerns into one 777-line file
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
let surfpoolProcess: ChildProcess | null = null;
let startingValidator = false;
let deployingProgram = false;
Why it matters

Validator process state, the config store, airdrops and deploy planning share one router, so every change touches everything.

Suggested fix, not tested

Split it into validator, config-store, airdrop and deploy modules behind a route table.

17. Low "Create mock USDC" has no protection against concurrent calls
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
async function handleCreateMockUsdc(): Promise<Response> { ... const child = spawn('spl-token', ['create-token', ...]);
Why it matters

A double click or retry creates several mints, and the concurrent read-modify-write of config.json keeps only the last write.

Suggested fix, not tested

Keep one in-flight promise for this operation and serialize config writes.

18. Low "Start validator" reports success before the process has started
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
resolve(jsonResponse({ success: true, pid: child.pid }));
Why it matters

If surfpool is missing or fails to start, the client has already been told it succeeded.

Suggested fix, not tested

Resolve on the spawn event or on the first passing health check, and return the error from the error handler.

19. Low RPC_URL is read from the environment but most calls use a hard-coded endpoint
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
const healthRes = await fetch('http://127.0.0.1:8899', {
Why it matters

Setting RPC_URL points only some handlers at the new endpoint, so the server ends up talking to two nodes.

Suggested fix, not tested

Use the RPC_URL constant everywhere, and read the port from the environment too.

20. Low Pull status values are re-declared instead of imported
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
const PULL_STATUS_PENDING = 0;
const PULL_STATUS_SETTLED = 1;
Why it matters

The generated client already exports PullStatus, and these copies will drift if the program's enum changes.

Suggested fix, not tested

Import PullStatus here, in operator-settle.ts and in burst-randomness.ts.

21. Low The world-cup program address is declared in three TypeScript modules
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
const PROGRAM_ADDRESS = 'wCupoZtR1g1NXRRVELe5KqFgayyEteVKKxEerxugvxA';
Why it matters

A redeploy to a new address must be edited in three places, and missing one silently breaks that path.

Suggested fix, not tested

Export the address once, from the generated client or one config module, and import it everywhere.

22. Low The buy instruction's wire layout is hand-copied into server-side validation
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
if (!buyInstruction || !data || data.length !== 33 || data[0] !== 1) {
Why it matters

The values match today, but if the program's instruction layout changes, validation will reject every valid purchase or accept the wrong ones.

Suggested fix, not tested

Take the discriminator and length from the generated client.

23. Low Postinstall scripts switch the developer's global Solana CLI to mainnet, and the safer fix was applied to only one copy
First rating: Medium · Reviewed rating: Low · Review: rated too high
Location: tokens/nft-operations/pinocchio/prepare.mjs:25 (same in create-token/{native,anchor}, pda-mint-authority/{native,anchor}, transfer-tokens/{native,anchor}, nft-operations/anchor)
From the report
Evidence
execSync('solana config set -um', { stdio: 'inherit' });
Why it matters

After pnpm install, the developer's CLI stays pointed at mainnet-beta. A later solana program deploy (for example from cicd.sh) then spends real SOL on mainnet. tokens/create-token/pinocchio/prepare.mjs already uses the safe form.

Suggested fix, not tested

In every copy, use solana program dump -um <id> <file> and remove config set.

24. Low saveKeypairToFile deletes an existing key file without asking
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
// remove the current file, if it already exists
if (fs.existsSync(fileName)) fs.unlinkSync(fileName);
Why it matters

A caller that reuses a name loses that secret key for good.

Suggested fix, not tested

Fail when the file exists, or write with { flag: 'wx' }.

25. Low savePublicKeyToFile wipes stored addresses after a read error it swallowed
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
let data: any = loadPublicKeysFromFile(absPath);
Why it matters

The loader returns {} on any error, such as a corrupt file or one bad key. The save then writes only the new entry over the whole file.

Suggested fix, not tested

Tell "file missing" apart from "file unreadable", and abort the save in the second case.

26. Low getInstructionData is copied three times within the cNFT vault example
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
function getInstructionData(asset: any, proof: any): [number[], number[], number[], anchor.BN, number, number] {
Why it matters

Three copies of the proof-decoding logic have to be fixed in step.

Suggested fix, not tested

Move it into the example's existing tests/utils.ts.

27. Low CI orchestration is copied across four workflows, and the copies have drifted
First rating: Low · Reviewed rating: Low · Review: location checked, kept as rated
From the report
Evidence
failed_projects=$(cat $GITHUB_WORKSPACE/failed_projects.txt | jq -R -s -c 'split("\n")[:-1]')
Why it matters

The other three workflows guard this read with [ -f ... ]. solana-asm.yml lacks the Node 24 environment flag. The anchor.yml path filter omits Cargo.toml and Cargo.lock.

Suggested fix, not tested

Move the shared jobs into a reusable workflow or composite action parameterised by framework.

33. Low Fundraiser: the minimum contribution is always 1 base unit
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
amount >= 1_u64.pow(self.mint_to_raise.decimals as u32),
Why it matters

1^n is always 1, so the intended minimum of one whole token is never enforced.

Suggested fix, not tested

Use 10_u64.pow(decimals), or a named minimum scaled by the decimals.

34. Low Fundraiser: the vault token account is never closed
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
close = maker,
Why it matters

The fundraiser account is closed but its vault is not. The vault's rent stays locked, and a second initialize for the same maker and mint fails because the vault already exists.

Suggested fix, not tested

After the transfer, close the vault with a close_account CPI signed by the fundraiser PDA.

Upstream: Already reported upstream: #725, #731.

36. Low Token swap: a one-sided donation makes every deposit panic
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
let amount_b_required = amount_a_u128.checked_mul(pool_b_u128).unwrap()
    .checked_div(pool_a_u128)
    .unwrap();
Why it matters

If someone sends token B to an empty pool, pool_a stays 0 and the division panics. The pool can then never be seeded.

Suggested fix, not tested

Treat "either reserve is zero" as pool creation, and replace the unwrap() calls with returned errors.

37. Low PDA mint authority (native): anyone can mint using the program's authority
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
invoke_signed(
    &token_instruction::mint_to(
        token_program.key, mint_account.key, associated_token_account.key,
Why it matters

The PDA signs mint_to for any caller. Whoever calls first receives the token from a mint someone else created.

Suggested fix, not tested

Store a creator or admin key and require it to sign, as the Anchor variant does.

38. Low PDA mint authority (Pinocchio): the same unrestricted mint
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
MintTo { mint: mint_account, account: associated_token_account, mint_authority, amount: 1,
.invoke_signed(&signers)?;
Why it matters

Same as finding 37.

Suggested fix, not tested

Same as finding 37.

40. Low Transfer hook: the transfer limit it advertises is not enforced
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
if amount > 50 {
    msg!("The amount is too big: {}", amount);
    //return err!(TransferError::AmountTooBig);
Why it matters

Oversized transfers go through. Only a log line records them.

Suggested fix, not tested

Re-enable the error, or remove the limit and the unused error variant.

41. Low Transfer hook: the transfer counter is never saved
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
let count = ctx
    .accounts
    .counter_account
Why it matters

The incremented value is only logged. The account is not mutable and is never written, so the count is always reported as 1.

Suggested fix, not tested

Mark counter_account as mut and assign the new count.

42. Low The DAS RPC endpoint, documented as carrying an API key, is bundled into the browser
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
const DAS_URL = process.env.NEXT_PUBLIC_DAS_RPC;
Why it matters

Next.js inlines NEXT_PUBLIC_* variables into client code, so every visitor can read the provider credential.

Suggested fix, not tested

Proxy DAS calls through a server route that reads a variable without the public prefix. Rotate any key already exposed.

43. Low World cup: entry fees are locked forever if the admin never finalizes
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
if TournamentState::try_from(config.state)? != TournamentState::Finalized {
    return Err(WorldCupError::InvalidState.into());
}
Why it matters

Only the admin can advance the state. We searched the program for refund, cancel or timeout logic and found none. If the admin key is lost or the admin withholds results, every entry fee stays in the vault.

Suggested fix, not tested

Add a refund anyone can trigger after a deadline, or a cancel state.

44. Low World cup: whoever calls config initialization first becomes admin and oracle
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
Config::init(&mut config_data, config_bump, accounts.admin.address(), lock_ts, ENTRY_FEE)?;
Why it matters

The config PDA is global and any signer can initialize it. Someone who front-runs the deployer controls results and payouts.

Suggested fix, not tested

Restrict initialization to the upgrade authority or a fixed deployer key, or seed the PDAs by admin.

Upstream: No exact upstream report; related items: #743.

45. Low World cup dev API: any web page can trigger actions that change state
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
const allowedOrigin = origin && ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0];
Why it matters

Requests from unknown origins are still processed, and the body is parsed whatever its Content-Type. Any site the developer visits can call airdrop, start-validator, create-mock-usdc and save-config, and save-config can rewrite the program address.

Suggested fix, not tested

Reject origins that are not on the allow list, validate Host, require Content-Type: application/json, and add a per-run token.

46. Low World cup dev API: one malformed request crashes the server
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
if (typeof val !== baseType) throw new Error(`Field ${key} must be ${baseType}, got ${typeof val}`);
Why it matters

extractFields throws, and neither the request handler nor the server callback catches the error. A body such as {"recipient":5} ends the process, and because of finding 45 a cross-site request can send it.

Suggested fix, not tested

Wrap request handling in try/catch and return 400.

47. Low World cup dev API: save-config stores unvalidated data
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
if (body.tokens) { prev.tokens = body.tokens; }
if (body.programAddress) { prev.programAddress = body.programAddress; }
Why it matters

Anything that is an object passes as tokens, and programAddress and network are not checked at all. Later handlers that call tokens.find(...) throw, and the injected address feeds the deploy plan.

Suggested fix, not tested

Validate the network against an allow list, and check token entries and the address against base58 rules.

48. Low Anchor.toml program IDs do not match declare_id!
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
swap_example = "AsGVFxWqEn8icRBFQApxJe68x3r9zvfSbmiEzYFATGYn"
abl-token = "EYBRvArz4kb5YLtzjD4TW6DbWhS8qjcMYqBU4wHLW3qj"
Why it matters

lib.rs declares UPxp2moQ… and 3ku1ZEGv…. A deploy using the Anchor.toml key produces a program that rejects its own instructions. CI hides this because it regenerates the keys.

Suggested fix, not tested

Run anchor keys sync and regenerate the IDL and client from one source.

51. Low The expectRevert test helper can never fail
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
await promise;
throw new Error('Expected a revert');
} catch {
Why it matters

The "expected a revert" error is caught by its own catch, so every negative-path test passes whether or not the program rejects the input.

Suggested fix, not tested

Set a flag in the catch and throw after the try block, as tokens/escrow/pinocchio/tests/utils.ts does. Then re-run the affected tests.

52. Low Build-and-deploy scripts deploy a stale binary after a failed build
First rating: Medium · Reviewed rating: Low · Review: rated too high
Location: basics/account-data/native/cicd.sh:7 (and the other cicd.sh scripts)
From the report
Evidence
cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so
solana program deploy ./program/target/so/program.so
Why it matters

There is no set -e. A failed build still deploys whatever .so is left over from an earlier build.

Suggested fix, not tested

Add set -euo pipefail after the shebang. pipefail matters for the script that pipes deploy output into grep.

53. Low Maintainer script builds a shell command from package names
First rating: Medium · Reviewed rating: Low · Review: rated too high
From the report
Evidence
const npmVersion = execSync(`npm view ${pkgName} version`).toString().trim();
Why it matters

Package names come from the dependency keys of every package.json in the tree. A contributed key containing shell syntax runs on the maintainer's machine during an update.

Suggested fix, not tested

Use execFileSync('npm', ['view', pkgName, 'version']) and validate the name first.

54. Low Pull requests that change the shared CI setup action skip the builds
First rating: Medium · Reviewed rating: Low · Review: rated too high
Location: .github/workflows/anchor.yml:52 (and the native, Pinocchio and asm workflows)
From the report
Evidence
workflow:
  - added|modified: '.github/workflows/anchor.yml'
Why it matters

Edits to .github/actions/setup/** or .github/.ghaignore match no filter, so no projects are built and the breakage first shows up on main.

Suggested fix, not tested

Add those paths to the workflow filter in all four workflows.

Info after review (3)

28. Info Per-example script copies drift
First rating: Info · Reviewed rating: Info · Review: location checked, kept as rated
From the report
Evidence
cargo build-sbf --manifest-path=./program/Cargo.toml --bpf-out-dir=./program/target/so

Copying scripts into each example is a deliberate design choice. Still, seven Pinocchio scripts use --bpf-out-dir while the rest use --sbf-out-dir, and some prepare.mjs headers still mention bankrun, which has been removed. A periodic sync check would keep the copies aligned.

49. Info is_token_2022_mint reads the account data before checking its length
First rating: Medium · Reviewed rating: Info · Review: rated too high
From the report
Evidence
let mint_type_byte = data[TYPE_BYTE_OFFSET];
data.len() > TYPE_BYTE_OFFSET && mint_type_byte == MINT_TYPE_BYTE && mint.owned_by(&TOKEN_2022_PROGRAM_ID)
Why it matters

A classic 82-byte mint or an empty account panics instead of returning false and producing the intended InvalidMint error, so the length check never does its job.

Suggested fix, not tested

Check the owner and length first, or use data.get(TYPE_BYTE_OFFSET).

50. Info Pinocchio basics: short instruction data panics before the intended error
First rating: Medium · Reviewed rating: Info · Review: rated too high
From the report
Evidence
let amount_bytes: [u8; 8] = instruction_data[0..8].try_into().map_err(|_| ProgramError::InvalidInstructionData)?;
Why it matters

Slicing with [a..b], indexing, split_first().unwrap() and copy_from_slice all panic on bad input, so the map_err handling never runs and callers get an opaque failure. These examples are what people copy.

Suggested fix, not tested

Use instruction_data.get(..n).ok_or(ProgramError::InvalidInstructionData)?, and validate exact lengths before copy_from_slice.

Want this for your code?

Upload a ZIP or link a public GitHub repo, pick the areas and the depth, and get findings by severity with fixes.