Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Impact: low
Likelihood: medium
Invalid

Zero-amount contributions accepted — free PDA spam enables on-chain griefing

Root + Impact

Description

contribute() does not verify that amount > 0. A caller passing amount = 0 will pass the deadline check, execute a no-op system_program::transfer, initialise a contribution PDA via init_if_needed (costing only PDA rent from the spammer), and add 0 to fund.amount_raised. An attacker can generate thousands of keypairs and submit zero-lamport contributions to flood a campaign with fake accounts — disrupting off-chain indexers, inflating contributor counts, and degrading RPC performance for any query filtering that campaign's accounts.

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
@> // BUG: No require!(amount > 0)
// CPI is a no-op for amount = 0 but still succeeds
system_program::transfer(cpi_context, amount)?;
fund.amount_raised += amount; // += 0, no state change
Ok(())
}

Risk

Likelihood:

  • Creating spam contributor keypairs on Solana costs only PDA rent (~0.002 SOL each) plus transaction fees — large-scale spam is economically feasible for a motivated attacker

  • There is no on-chain rate limiting or minimum stake requirement for creating contribution accounts

Impact:

  • Off-chain dashboards counting getProgramAccounts results display inflated contributor counts, misleading users about campaign popularity

  • Spam contribution PDAs permanently consume on-chain storage for the lifetime of the program

  • RPC calls fetching all contributions for a campaign become increasingly expensive as spam accounts accumulate

Proof of Concept

// Attacker script — 1000 zero-lamport contributions
for (let i = 0; i < 1000; i++) {
const spamWallet = Keypair.generate();
await airdrop(spamWallet.publicKey, 0.01 * LAMPORTS_PER_SOL); // rent only
const [spamPda] = PublicKey.findProgramAddressSync(
[fundPda.toBuffer(), spamWallet.publicKey.toBuffer()],
program.programId
);
@>await program.methods.contribute(new BN(0)) // zero-amount passes all checks
.accounts({ fund: fundPda, contributor: spamWallet.publicKey,
contribution: spamPda, systemProgram })
.signers([spamWallet]).rpc();
}
// Fund now shows 1000 contributor accounts, all with amount = 0

Recommended Mitigation

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
+ require!(amount > 0, ErrorCode::InvalidAmount);
+ // Optional stronger minimum:
+ // require!(amount >= MIN_CONTRIBUTION, ErrorCode::ContributionTooSmall);
let fund = &mut ctx.accounts.fund;
...
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!