Rust Fund

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

Lack of target ownership validation allows contribution hijacking

Root + Impact

Description

  • The program allows contributors to fund specific campaign accounts during the active campaign phase.

  • The `FundContribute` accounts structure accepts the target campaign account `fund` without validating that the target fund belongs to the creator/campaign that the contributor intended to fund.

#[derive(Accounts)]
pub struct FundContribute<'info> {
// @> Root Cause: Missing check to verify creator or target campaigns
#[account(mut)]
pub fund: Account<'info, Fund>,
#[account(mut)]
pub contributor: Signer<'info>,

Risk

Likelihood:

  • Attackers passing a malicious campaign PDA under their control instead of the genuine campaign account during transaction assembly.

Impact:

  • Contributor transfers their SOL to an incorrect campaign, hijacking the funds to a malicious campaign creator's wallet.

Proof of Concept

### Proof of Concept Field (Set language to Rust)
```rust
#[test]
fn test_contribute_wrong_campaign_vulnerability() {
setup_syscall_stubs();
let campaign_a_key = Pubkey::new_unique();
let creator_a_key = Pubkey::new_unique();
let campaign_b_key = Pubkey::new_unique(); // Malicious Campaign
let creator_b_key = Pubkey::new_unique(); // Malicious Creator
let contributor_key = Pubkey::new_unique();
let contribution_key = Pubkey::new_unique();
let program_id = crate::ID;
// Setup Campaign A (The one the contributor INTENDS to fund)
let mut fund_a_lamports = 100_000_000;
let mut fund_a_data = vec![0u8; 8 + Fund::INIT_SPACE];
let fund_a_state = Fund {
name: "Campaign A".to_string(),
description: "Good Campaign".to_string(),
goal: 1000,
deadline: 0,
creator: creator_a_key,
amount_raised: 0,
dealine_set: false,
};
let mut writer_a = &mut fund_a_data[..];
fund_a_state.try_serialize(&mut writer_a).unwrap();
// Setup Campaign B (The MALICIOUS campaign)
let mut fund_b_lamports = 100_000_000;
let mut fund_b_data = vec![0u8; 8 + Fund::INIT_SPACE];
let fund_b_state = Fund {
name: "Campaign B".to_string(),
description: "Malicious Campaign".to_string(),
goal: 1000,
deadline: 0,
creator: creator_b_key,
amount_raised: 0,
dealine_set: false,
};
let mut writer_b = &mut fund_b_data[..];
fund_b_state.try_serialize(&mut writer_b).unwrap();
let mut contributor_lamports = 1_000_000_000;
let mut contributor_data = vec![];
let mut contribution_lamports = 10_000_000;
let mut contribution_data = vec![0u8; 8 + Contribution::INIT_SPACE];
contribution_data[..8].copy_from_slice(&Contribution::DISCRIMINATOR);
let system_program_key = anchor_lang::solana_program::system_program::ID;
let bpf_loader_id = anchor_lang::solana_program::bpf_loader::id();
let mut system_program_lamports = 0;
let mut system_program_data = vec![];
let system_program_info = AccountInfo::new(
&system_program_key, false, false, &mut system_program_lamports, &mut system_program_data, &bpf_loader_id, true, 0
);
let fund_b_info = AccountInfo::new(&campaign_b_key, false, true, &mut fund_b_lamports, &mut fund_b_data, &program_id, false, 0);
let contributor_info = AccountInfo::new(&contributor_key, true, true, &mut contributor_lamports, &mut contributor_data, &program_id, false, 0);
let contribution_info = AccountInfo::new(&contribution_key, false, true, &mut contribution_lamports, &mut contribution_data, &program_id, false, 0);
let fund_b_acc: Account<Fund> = Account::try_from(&fund_b_info).unwrap();
let contributor_sig: Signer = Signer::try_from(&contributor_info).unwrap();
let contribution_acc: Account<Contribution> = Account::try_from(&contribution_info).unwrap();
let system_prog: Program<System> = Program::try_from(&system_program_info).unwrap();
// Pass the malicious Campaign B as the target
let mut accounts = FundContribute {
fund: fund_b_acc,
contributor: contributor_sig,
contribution: contribution_acc,
system_program: system_prog,
};
let ctx = Context::new(&program_id, &mut accounts, &[], FundContributeBumps::default());
let res = crate::rustfund::contribute(ctx, 500);
// Assert: Succeeds because there is no target creator validation check
assert!(res.is_ok());
}

POC Explanation:-


The test sets up two separate campaign accounts: the intended Campaign A and a malicious Campaign B. When constructing the contribute instruction, the contributor's accounts context specifies Campaign B as the destination fund. The program allows the contribution transaction to complete successfully without asserting that the campaign target matches the campaign the contributor intended to fund.


Recommended Mitigation

#[derive(Accounts)]
pub struct FundContribute<'info> {
- #[account(mut)]
+ #[account(mut, seeds = [fund.name.as_bytes(), fund.creator.as_ref()], bump)]
pub fund: Account<'info, 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!