RustFund

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

Improper Error Handling with unwrap()

Summary

In Solana smart contracts, error handling is crucial to ensure the contract behaves reliably and securely. The unwrap() method is often used in Rust to handle Result and Option types. However, in the context of smart contracts, using unwrap() can result in a panic, halting contract execution and potentially causing a loss of user funds or other unintended consequences. This report analyzes the improper use of .unwrap() in lib.rs contract and provides recommendations for safer error handling.

Vulnerability Details

The vulnerability stems from the use of .unwrap() in two key places within the smart contract:

  1. Using .unwrap() on Clock::get():

    • Clock::get() returns a Result<Clock, ProgramError>. The method .unwrap() is called directly on the result of Clock::get().

    • If Clock::get() fails (e.g., due to a system clock error or program-specific issue), it returns an Err. Using .unwrap() on this result causes a panic and halts contract execution.

  2. Using .unwrap() after .try_into() on unix_timestamp:

    • .try_into() attempts to convert a value (likely an i64 timestamp) into a different type (e.g., u64). If the conversion fails (e.g., if the unix_timestamp is negative or cannot be cast to the target type), .unwrap() is called.

    • This will result in a panic if the conversion fails, which can stop the contract execution and lead to an inconsistent state.

Both of these cases involve calling .unwrap() on Result or Option types without handling the potential error, resulting in the program terminating unexpectedly.

Locations in Code

  • In refund:

    if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() {return Err(ErrorCode::DeadlineNotReached.into());}
  • In contribute:

if fund.deadline != 0 && fund.deadline < Clock::get().unwrap().unix_timestamp.try_into().unwrap() {return Err(ErrorCode::DeadlineReached.into());}

Root Cause

The use of unwrap() bypasses Rust’s error-handling mechanisms (Result and Option), assuming success in scenarios where failure is possible. In Solana programs, panics are particularly problematic because they terminate execution mid-transaction, leaving accounts unchanged but still charging users for fees.

Impact

  • Panic Halts Execution:

    • A panic in a smart contract will stop the transaction from being processed and revert any state changes made prior to the panic.

    • While the transaction itself is reverted, the associated transaction fees (gas) are still charged. Users are left with wasted fees if the transaction fails due to a panic, without any progress on their intended actions.

    2. Loss of Funds:

    • In cases where SOL is being transferred or a contract's state is modified, a panic can prevent the user’s transaction from completing. However, users still lose gas fees spent during the failed transaction attempt.

Tools Used

manual review

Recommendations

Replace unwrap() with Rust’s ? operator and explicit error mapping to propagate errors gracefully. This ensures the program returns a Result with a meaningful error instead of panicking.

Fixed refund Function

pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
// Get current time with error handling
let current_time = Clock::get()?.unix_timestamp.try_into()
.map_err(|_| ProgramError::InvalidArgument)?;
// Check deadline
if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > current_time {
return Err(ErrorCode::DeadlineNotReached.into());
}
// Transfer lamports from fund to contributor
**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)?;
Ok(())
}

Fixed contribute Function

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let contribution = &mut ctx.accounts.contribution;
// Get current time with error handling
let current_time = Clock::get()?.unix_timestamp.try_into()
.map_err(|_| ProgramError::InvalidArgument)?;
// Check deadline
if fund.deadline != 0 && fund.deadline < current_time {
return Err(ErrorCode::DeadlineReached.into());
}
// Initialize contribution record if new
if contribution.contributor == Pubkey::default() {
contribution.contributor = ctx.accounts.contributor.key();
contribution.fund = fund.key();
contribution.amount = 0;
}
// Transfer SOL from contributor to fund
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.contributor.to_account_info(),
to: fund.to_account_info(),
},
);
system_program::transfer(cpi_context, amount)?;
fund.amount_raised += amount;
Ok(())
}

Explanation of Fixes

  1. Clock::get()?:

    • Uses the ? operator to propagate any ProgramError returned by Clock::get() up the call stack, returning it as the function’s Result.

  2. .try_into().map_err(...):

    • Converts a TryFromIntError (from failed type conversion) into a ProgramError::InvalidArgument, providing a clear error reason without panicking.

  3. Return Result<()>:

    • Both functions now return Ok(()) on success or an Err with a specific error code, adhering to Solana’s error-handling conventions.

Updates

Appeal created

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

[Invalid] Incorrect error handling for timestamp

It is very unlikely `Clock::get` to fail, therefore I think it is safe to use `unwrap` here. Consider this issue as informational.

Support

FAQs

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