RustFund

First Flight #36
Beginner FriendlyRust
100 EXP
View results
Submission Details
Severity: medium
Valid

H-03. Missing Withdrawal State Tracking and Inconsistent Fund Accounting

H-03. Missing Withdrawal State Tracking and Inconsistent Fund Accounting

Severity: High
Category: Economic Integrity / Fund Management

Summary

The RustFund protocol lacks proper state management following withdrawals. Specifically, the amount_raised remains unchanged even after funds have been withdrawn, allowing ambiguity in campaign state and potentially enabling repeated withdrawals.

Vulnerability Details

The withdraw function does not update or reset the fund's state after successful withdrawals. This leaves the campaign's amount_raised unchanged and introduces the risk of improper or duplicate withdrawals, especially if new contributions are made after withdrawal.

Vulnerable Component:

  • File: lib.rs

  • Function: withdraw

  • Struct: Fund

Impact

  • Campaign state ambiguity: The protocol does not clearly indicate whether funds have already been withdrawn.

  • Risk of multiple withdrawals: Future contributions might trigger further withdrawals due to missing state enforcement.

  • Economic inconsistency: Protocol state diverges from actual on-chain balances, potentially misleading contributors.

Proof of Concept (PoC)

Here's a minimalist and professional PoC for H-03 (Missing Withdrawal State Tracking):

// Create fund with 5 SOL goal
await program.methods.fundCreate(FUND_NAME, "Test fund", new anchor.BN(5 * LAMPORTS_PER_SOL))
.accounts({
fund,
creator: creator.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([creator])
.rpc();
// Contribute 5 SOL to meet the goal
await program.methods.contribute(new anchor.BN(5 * LAMPORTS_PER_SOL))
.accounts({
fund,
contributor: contributor.publicKey,
contribution,
systemProgram: SystemProgram.programId,
})
.signers([contributor])
.rpc();
// Get state before withdrawal
const fundBefore = await program.account.fund.fetch(fund);
console.log("Fund amountRaised before withdrawal:", fundBefore.amountRaised / LAMPORTS_PER_SOL, "SOL");
// Withdraw funds
await program.methods.withdraw()
.accounts({
fund,
creator: creator.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([creator])
.rpc();
// Check state after withdrawal
const fundAfter = await program.account.fund.fetch(fund);
console.log("Fund amountRaised after withdrawal:", fundAfter.amountRaised / LAMPORTS_PER_SOL, "SOL");
/* OUTPUT:
Fund amountRaised before withdrawal: 5 SOL
Withdrawal successful!
Fund amountRaised after withdrawal: 5 SOL
Fund accounting NOT reset. Potential for multiple withdrawals!
*/

Tools Used

  • Anchor Framework (JavaScript tests)

  • Manual Code Review

Recommendations

Explicitly track withdrawal state by updating the Fund struct and modifying the withdraw function to ensure proper state transitions:

// Add to Fund struct
pub withdrawn: bool,
// Modify withdraw function
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &mut ctx.accounts.fund;
// Prevent multiple withdrawals
if fund.withdrawn {
return Err(ErrorCode::AlreadyWithdrawn.into());
}
let amount = fund.amount_raised;
// Existing withdrawal logic...
// Set withdrawal flag and reset accounting
fund.withdrawn = true;
fund.amount_raised = 0;
Ok(())
}
// Add error code
#[error_code]
pub enum ErrorCode {
// ...existing errors
#[msg("Campaign funds already withdrawn")]
AlreadyWithdrawn,
}
Updates

Appeal created

bube Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

`amount_raised` is not reset to 0 in `withdraw` function

Support

FAQs

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