RustFund

First Flight #36
Beginner FriendlyRust
100 EXP
View results
Submission Details
Severity: high
Invalid

Race Condition in Refunds (Double Refund Attack)

Summary

The refund()function allows contributors to execute multiple refund transactions in parallel before the state updates, resulting in double or multiple withdrawals of the same funds.

Vulnerability Details

1. The function transfers SOL before updating the contribution state.
2. Parallel execution in Solana allows multiple transactions to pass the refund check before contribution.amount = 0; is set.

Impact

I believe there are 2 impacts of this bug:

1. A malicious contributor can withdraw more than their original contribution.
2. The contract fund can be drained because refunds do not correctly track and update contributions before transfer.

Tools Used

  1. Manual Review

  2. Anchor Testing Framework

POC
Steps to Exploit

  1. User A contributes 5 SOL to a crowdfunding campaign.

  2. User A rapidly submits multiple refund() transactions in parallel before the contract state updates.

  3. Since state updates happen after the fund transfer, multiple refund transactions pass, allowing the contributor to withdraw more than their original contribution.

  4. Result:

    • Expected refund: 5 SOL

    • Actual refund due to race condition: 15 SOL (3x 5 SOL in parallel transactions)

    • The contract fund is drained unexpectedly.

Code issue in refund():

pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
**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.contributor.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.contributor.to_account_info().lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
ctx.accounts.contribution.amount = 0; // State is updated AFTER transfer
Ok(())
}


Recommendation:

Perform state Update before transferring SOL to prevent multiple refund transaction scenarios.

Like here:

ctx.accounts.contribution.amount = 0; // Now any second transaction will fail
**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.contributor.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.contributor.to_account_info().lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
Updates

Appeal created

bube Lead Judge 6 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

[Invalid] Reentrancy in refund

The reentrancy risk on Solana is highly eliminated. The `try_borrow_mut_lamports` ensures that only one reference to an account exists at a time. Also, once the fund’s lamports are borrowed mutably, no other transaction can modify them until the borrow is released. This means the function will reset the `amount` before the next call.

Support

FAQs

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