refund never decrements fund.amount_raised, desynchronizing recorded total from real lamportsWhen a contributor calls refund (lib.rs:66-88), the program moves contribution.amount lamports out of the fund and zeroes the per-contributor record at line 85, but it never reduces fund.amount_raised. The fund's recorded total therefore permanently overstates the lamports it actually holds.
Likelihood:
Every refund triggers it; there is no path through refund that updates amount_raised. Any campaign that processes even one refund ends in a corrupted state.
Impact:
amount_raised becomes a lie about the fund's balance. It is the value withdraw (lib.rs:91) pays out and the figure any UI/indexer reports as raised, so the corruption propagates into goal evaluation, accounting, and the withdrawal amount, breaking the core invariant amount_raised == lamports contributed and not yet refunded.
Contribute, refund, then read the account state.
Decrement amount_raised by the refunded amount inside refund.
# \[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(()) } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.