01. Relevant GitHub Links
02. Summary
The contract’s refund and withdraw functions do not enforce the conditions outlined in the documentation. Refunds are allowed if the deadline is unset or passed, without checking the goal, and creators can withdraw funds at any time, even if the goal isn’t met. Additionally, the amount_raised
value isn’t updated during refunds, which can lock funds and cause withdraw failures.
03. Vulnerability Details
The documentation states that refunds should only occur after the deadline if the goal isn’t met, and withdrawals should only be allowed after the campaign succeeds (goal is met).
Refund Mechanism: Contributors can get refunds if deadlines are reached and goals aren't met
Secure Withdrawals: Creators can withdraw funds once their campaign succeeds
However:
In the refund function, there’s no check for the goal. Refunds proceed if the deadline is unset or passed.
In the withdraw function, there’s no goal check either, allowing creators to withdraw funds anytime.
Another issue: refund doesn’t subtract the refunded amount from fund.amount_raised
. Since withdraw uses amount_raised
to determine the withdrawal amount, this can lead to failures if the actual balance is lower, locking the funds.
04. Impact
Contributors can be refunded even when they shouldn’t be, undermining the campaign’s integrity.
Creators can withdraw funds prematurely, potentially stealing contributions.
Funds can become locked due to the mismatch between amount_raised
and the actual balance, preventing legitimate withdrawals.
05. Tools Used
Manual Code Review and Foundry
06. Recommended Mitigation
Add goal checks to both functions and update amount_raised
during refunds:
For refund
: Ensure it only works if the deadline is set, passed, and the goal isn’t met.
For withdraw
: Allow withdrawals only if the goal is met.
Subtract refunded amounts from fund.amount_raised
.
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() {
+ if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() || !ctx.accounts.fund.dealine_set {
return Err(ErrorCode::DeadlineNotReached.into());
}
+ if ctx.accounts.fund.goal <= ctx.accounts.fund.amount_raised {
+ return Err(ErrorCode::GoalReached.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(())
}
#[error_code]
pub enum ErrorCode {
#[msg("Deadline already set")]
DeadlineAlreadySet,
#[msg("Deadline reached")]
DeadlineReached,
#[msg("Deadline not reached")]
DeadlineNotReached,
#[msg("Unauthorized access")]
UnauthorizedAccess, // @question : 사용 안되는데?
#[msg("Calculation overflow occurred")]
CalculationOverflow,
+ #[msg("Goal Reached")]
+ GoalReached,
}
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
+ if ctx.accounts.fund.goal > ctx.accounts.fund.amount_raised {
+ return Err(ErrorCode::GoalNotReached.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.creator.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.creator.to_account_info().lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
Ok(())
}
#[error_code]
pub enum ErrorCode {
#[msg("Deadline already set")]
DeadlineAlreadySet,
#[msg("Deadline reached")]
DeadlineReached,
#[msg("Deadline not reached")]
DeadlineNotReached,
#[msg("Unauthorized access")]
UnauthorizedAccess, // @question : 사용 안되는데?
#[msg("Calculation overflow occurred")]
CalculationOverflow,
+ #[msg("Goal Not Reached")]
+ GoalNotReached,
}