use anchor_lang::{prelude::Pubkey, AccountDeserialize, InstructionData, ToAccountMetas};
use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult};
use solana_program_test::{processor, BanksClientError, ProgramTest, ProgramTestContext};
use solana_sdk::{
signature::{Keypair, Signer},
system_instruction, system_program,
transaction::Transaction,
};
fn process_rustfund_instruction<'a, 'b, 'c, 'd>(
program_id: &'a Pubkey,
accounts: &'b [AccountInfo<'c>],
instruction_data: &'d [u8],
) -> ProgramResult {
let accounts: &'c [AccountInfo<'c>] = unsafe { std::mem::transmute(accounts) };
rustfund::entry(program_id, accounts, instruction_data)
}
fn derive_fund_pda(program_id: &Pubkey, name: &str, creator: &Pubkey) -> Pubkey {
Pubkey::find_program_address(&[name.as_bytes(), creator.as_ref()], program_id).0
}
fn derive_contribution_pda(program_id: &Pubkey, fund: &Pubkey, contributor: &Pubkey) -> Pubkey {
Pubkey::find_program_address(&[fund.as_ref(), contributor.as_ref()], program_id).0
}
async fn send_tx(
ctx: &mut ProgramTestContext,
instructions: Vec<solana_sdk::instruction::Instruction>,
extra_signers: Vec<&Keypair>,
) -> Result<(), BanksClientError> {
let recent_blockhash = ctx.banks_client.get_latest_blockhash().await.unwrap();
let mut signers = vec![&ctx.payer];
signers.extend(extra_signers);
let tx = Transaction::new_signed_with_payer(
&instructions,
Some(&ctx.payer.pubkey()),
&signers,
recent_blockhash,
);
ctx.banks_client.process_transaction(tx).await
}
async fn get_lamports(ctx: &mut ProgramTestContext, address: Pubkey) -> u64 {
ctx.banks_client
.get_account(address)
.await
.unwrap()
.map(|a| a.lamports)
.unwrap_or(0)
}
async fn fetch_fund(ctx: &mut ProgramTestContext, fund: Pubkey) -> rustfund::Fund {
let account = ctx
.banks_client
.get_account(fund)
.await
.unwrap()
.expect("fund account should exist");
rustfund::Fund::try_deserialize(&mut account.data.as_ref()).unwrap()
}
#[tokio::test]
async fn poc_finding3_injected_lamports_stuck() {
let program_id = rustfund::id();
let mut pt = ProgramTest::new("rustfund", program_id, processor!(process_rustfund_instruction));
let mut ctx = pt.start_with_context().await;
let creator = ctx.payer.pubkey();
let contributor = Keypair::new();
send_tx(
&mut ctx,
vec![system_instruction::transfer(&creator, &contributor.pubkey(), 2_000_000_000)],
vec![],
)
.await
.unwrap();
let name = "inject-stuck";
let description = "lamport injection PoC";
let goal = 1u64;
let contribute_amount = 1_000_000u64;
let injected = 42_424u64;
let fund = derive_fund_pda(&program_id, name, &creator);
let contribution = derive_contribution_pda(&program_id, &fund, &contributor.pubkey());
let ix_create = solana_sdk::instruction::Instruction {
program_id,
accounts: rustfund::accounts::FundCreate {
fund,
creator,
system_program: system_program::ID,
}
.to_account_metas(None),
data: rustfund::instruction::FundCreate {
name: name.to_string(),
description: description.to_string(),
goal,
}
.data(),
};
send_tx(&mut ctx, vec![ix_create], vec![]).await.unwrap();
let rent_baseline = get_lamports(&mut ctx, fund).await;
let ix_contribute = solana_sdk::instruction::Instruction {
program_id,
accounts: rustfund::accounts::FundContribute {
fund,
contributor: contributor.pubkey(),
contribution,
system_program: system_program::ID,
}
.to_account_metas(None),
data: rustfund::instruction::Contribute {
amount: contribute_amount,
}
.data(),
};
send_tx(&mut ctx, vec![ix_contribute], vec![&contributor])
.await
.unwrap();
send_tx(
&mut ctx,
vec![system_instruction::transfer(&contributor.pubkey(), &fund, injected)],
vec![&contributor],
)
.await
.unwrap();
let fund_state_after_inject = fetch_fund(&mut ctx, fund).await;
assert_eq!(fund_state_after_inject.amount_raised, contribute_amount);
let ix_withdraw = solana_sdk::instruction::Instruction {
program_id,
accounts: rustfund::accounts::FundWithdraw {
fund,
creator,
system_program: system_program::ID,
}
.to_account_metas(None),
data: rustfund::instruction::Withdraw {}.data(),
};
send_tx(&mut ctx, vec![ix_withdraw.clone()], vec![]).await.unwrap();
let fund_after_withdraw = get_lamports(&mut ctx, fund).await;
println!(
"fund balance after withdraw: {} (expected rent {} + injected {})",
fund_after_withdraw, rent_baseline, injected
);
assert_eq!(fund_after_withdraw, rent_baseline + injected);
let err = send_tx(&mut ctx, vec![ix_withdraw], vec![]).await.unwrap_err();
println!("second withdraw error: {err:?}");
}