Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Severity: medium
Valid

Refund Doesn't Update Fund Amount Raised

Root + Impact

Description

  • Describe the normal behavior in one or more sentences
    When a refund is processed, the function resets `contribution.amount` to 0 but never updates `fund.amount_raised`. This creates a state inconsistency where the fund's tracked amount doesn't match the actual balance, potentially allowing incorrect withdrawals or preventing proper goal tracking.


  • Explain the specific issue or problem in one or more sentences
    The normal behavior should maintain consistency between `fund.amount_raised` and the actual funds in the account. When a refund occurs, both the contribution amount and the fund's total raised amount should be reduced.

```rust:66-88:programs/rustfund/src/lib.rs
pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineNotReached.into());
}
**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)?;
// Reset contribution amount after refund
ctx.accounts.contribution.amount = 0;
Ok(())
}
```
The function transfers lamports and resets `contribution.amount`, but `fund.amount_raised` is never decremented, creating a discrepancy between tracked and actual funds.

Risk

Likelihood:

  • * This occurs on every refund transaction - amount_raised is never updated

    * The bug is deterministic and happens 100% of the time when refunds are processed

Impact:

  • * State inconsistency between `amount_raised` and actual fund balance

    * Creator could withdraw more than actually available if `amount_raised` is higher than balance

    * Goal tracking becomes inaccurate after refunds

    * Could lead to incorrect business logic decisions based on wrong `amount_raised` value

Proof of Concept

1. Campaign raises 10 SOL, `fund.amount_raised = 10 SOL`
2. Contributor gets refund of 2 SOL
3. Actual fund balance = 8 SOL
4. `fund.amount_raised` still = 10 SOL (incorrect)
5. Creator tries to withdraw - system thinks 10 SOL available but only 8 SOL exists
6. Or if goal was 9 SOL, system thinks goal met (10 >= 9) but only 8 SOL available

Recommended Mitigation

```diff
pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
+
+ if amount == 0 {
+ return Err(ErrorCode::NoContribution.into());
+ }
+
if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineNotReached.into());
}
+
+ if ctx.accounts.fund.amount_raised >= ctx.accounts.fund.goal {
+ return Err(ErrorCode::GoalMet.into());
+ }
**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)?;
// Reset contribution amount after refund
ctx.accounts.contribution.amount = 0;
+ // Update fund amount_raised to maintain state consistency
+ ctx.accounts.fund.amount_raised = ctx.accounts.fund.amount_raised
+ .checked_sub(amount)
+ .ok_or(ErrorCode::CalculationOverflow)?;
Ok(())
}
```
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 16 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-03] Fund Creator Can't Withdraw If Someone Has Refunded Their Contribution

# \[H-02] Fund Creator Can't Withdraw If Someone Has Refunded Their Contribution ## Description The `refund` function does not update `fund.amount_raised`, causing an inconsistency between the fund's actual balance and the recorded raised amount. As a result, when the fund creator tries to withdraw funds, the transaction may fail due to insufficient balance, effectively locking funds in the contract. ## Vulnerability Details The issue arises in the `refund` function, which transfers funds back to the contributor but does not update the `amount_raised` field: ```rust pub fn refund(ctx: Context<FundRefund>) -> Result<()> { let amount = ctx.accounts.contribution.amount; if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } 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)?; // Reset contribution amount after refund ctx.accounts.contribution.amount = 0; Ok(()) } ``` The issue becomes evident when the fund creator attempts to withdraw using the following function: ```rust 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(()) } ``` Since `amount_raised` is never updated when a refund occurs, the creator will attempt to withdraw more than what actually exists in the fund, causing an insufficient funds error and failing the transaction. ## Impact - If any contributor requests a refund, the total balance in the fund decreases. However, `fund.amount_raised` remains unchanged, leading to an overestimated available balance. - When the fund creator calls `withdraw`, they attempt to transfer `fund.amount_raised`, which no longer matches the actual available balance. - This results in a failed transaction, effectively locking funds in the contract since the withdraw function will always fail if refunds have been processed. ## Proof of Concept This issue is not currently caught by tests because the `contribute` function itself has a bug (not updating `contribution.amount`), preventing the refund function from executing properly. Once the contribute function is fixed, the issue will be clearly visible in test cases. ## Recommendations The `refund` function must update `fund.amount_raised` to ensure the contract state reflects the actual balance after refunds. ### Fixed Code: ```diff pub fn refund(ctx: Context<FundRefund>) -> Result<()> { let amount = ctx.accounts.contribution.amount; if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } 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)?; // Reset contribution amount after refund ctx.accounts.contribution.amount = 0; + // Fix: Decrease the fund's recorded amount_raised + let fund = &mut ctx.accounts.fund; + fund.amount_raised = fund.amount_raised.checked_sub(amount).ok_or(ErrorCode::CalculationOverflow)?; Ok(()) } ```

Support

FAQs

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

Give us feedback!