Summary
Direct modification of Lamports instead of using Solana's System Program.
Vulnerability Details
The contract modifies Lamports directly using try_borrow_mut_lamports()
This bypasses Solana’s built-in atomicity safeguards, which ensure a transaction either fully succeeds or fully fails.
If a transaction fails partway, funds may be partially transferred, leaving the contract in an inconsistent state.
Impact
Solana transactions are atomic by design—either everything executes, or nothing does.
RustFund bypasses atomicity by directly mutating lamports using try_borrow_mut_lamports()
, instead of invoking a system program transaction such as: system_instruction::transfer()
.
If one part of a transaction fails, but the lamports were already modified, the contract state could become inconsistent.
This can lead to fund loss or balances becoming stuck.
Tools Used
1. Manual Review
PoC
Steps to Reproduce (Inconsistent Fund State):\
1. User A contributes 10 SOL to a crowdfunding campaign.
2. User A calls refund()
, but the transaction fails mid-execution (e.g., due to Solana network congestion, program constraints, etc).
3. Result:
i. The transaction partially executes—it deducts lamports from the fund but fails before crediting contributor.
ii. Fund state becomes inconsistent, with lost or stuck funds.
Code Issue in the withdraw()
function
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.fund.to_account_info().lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;
**ctx.accounts.creator.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.creator.to_account_info().lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
Ok(())
}
Recommendations
Using Solana’s System Program (system_instruction::transfer())
for all fund transfers ensures transactions execute as a single atomic unit.
Secure Fix:
use anchor_lang::solana_program::{system_instruction, program::invoke};
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &ctx.accounts.fund;
let creator = &ctx.accounts.creator;
let amount = fund.amount_raised;
if fund.to_account_info().lamports() < amount {
return Err(ErrorCode::InsufficientFunds.into());
}
let transfer_ix = system_instruction::transfer(
&fund.to_account_info().key(),
&creator.to_account_info().key(),
amount,
);
invoke(
&transfer_ix,
&[
fund.to_account_info(),
creator.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
Ok(())
}
use anchor_lang::solana_program::{system_instruction, program::invoke};
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &ctx.accounts.fund;
let creator = &ctx.accounts.creator;
let amount = fund.amount_raised;
if fund.to_account_info().lamports() < amount {
return Err(ErrorCode::InsufficientFunds.into());
}
let transfer_ix = system_instruction::transfer(
&fund.to_account_info().key(),
&creator.to_account_info().key(),
amount,
);
invoke(
&transfer_ix,
&[
fund.to_account_info(),
creator.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
Ok(())
}