In a properly functioning crowdfunding smart contract implementing the "all-or-nothing" funding model, the refund() function should only permit contributors to reclaim their funds when a campaign fails to meet its funding goal by the deadline [file:1]. The function must validate two conditions before processing refunds: (1) the campaign deadline has passed, ensuring the funding period has concluded, and (2) the fund.amount_raised is less than fund.goal, confirming the campaign failed to achieve its objective. This ensures that successful campaigns—where contributors' funds should be locked for the creator to withdraw—cannot have their funds drained through refunds. Contributors accept that if a campaign succeeds, their funds are committed to the project and cannot be reclaimed, which is the core promise of crowdfunding platforms like Kickstarter.
The refund() function contains insufficient validation, checking only that the deadline has passed but never validating whether the campaign failed to meet its goal [file:1]. Lines 66-88 of lib.rs show the function verifies fund.deadline > current_time to ensure the deadline was reached, but completely omits the critical check fund.amount_raised < fund.goal that would confirm campaign failure. This allows contributors to request refunds even when campaigns successfully exceed their funding goals—for example, a campaign with a 10 SOL goal that raises 12 SOL can have all contributors invoke refund() after the deadline passes, draining the fund PDA and leaving the creator with nothing despite exceeding the target. While currently masked by CRITICAL-01 (which causes refunds to return 0 SOL), the logic bug exists at the transaction level—refund() calls succeed when they should be rejected. Once CRITICAL-01 is fixed, this vulnerability becomes immediately exploitable, enabling contributors to collectively drain successful campaigns and creating race conditions where creators and contributors compete to extract funds after deadlines
Likelihood:
The missing goal validation will be triggered in every scenario where a campaign successfully meets its goal and contributors attempt refunds after the deadline passes [file:1]. While initially masked by CRITICAL-01, once that vulnerability is patched, any contributor who misunderstands the platform logic or acts maliciously can invoke refund() on successful campaigns and receive their funds back. The bug manifests automatically during normal post-campaign operations—after a 30-day campaign succeeds with 150% funding, well-meaning contributors checking their options will discover they can call refund() and receive SOL back, not realizing this breaks the platform's intended behavior. User confusion alone will trigger this bug frequently.
Coordinated attacks or individual opportunistic behavior will exploit this vulnerability once discovered [code_file:14]. In scenarios where some contributors become dissatisfied with a project even after funding succeeds (scope changes, timeline delays, team conflicts), they can collectively organize to refund their contributions post-deadline, effectively "cancelling" a successful campaign. This creates a toxic dynamic where creators who successfully raise funds face perpetual risk of fund drainage even after meeting goals. Additionally, contributors monitoring on-chain activity can front-run creator withdrawals—seeing a withdraw() transaction in the mempool, a contributor can quickly submit refund transactions to extract their share before the creator receives funds, creating a profitable MEV opportunity.
Impact:
Financial Impact - Campaign Drainage
Successful campaigns that exceed their funding goals can have all funds drained through contributor refunds, leaving creators with zero proceeds despite achieving objectives [code_file:14]. Example scenario: A campaign with a 10 SOL goal successfully raises 12 SOL from 5 contributors over 30 days. After the deadline passes, all contributors invoke refund(), each receiving their original contributions back. The fund PDA balance returns to ~0 SOL, and when the creator attempts withdraw(), they receive nothing. The creator delivered on their promise to reach the goal but receives no compensation, while contributors reclaim funds they had committed to a successful project. This completely undermines the incentive structure of crowdfunding platforms.
The vulnerability breaks the fundamental "all-or-nothing" crowdfunding model where success means contributors' funds are locked and failure means funds are returned [file:1]. By allowing refunds on successful campaigns, the platform transforms into a "reversible commitment" system where contributors can change their minds after seeing campaign outcomes. This removes the financial certainty creators need to plan projects—even after raising 150% of their goal, creators cannot rely on fund availability because contributors retain indefinite refund rights. This makes the platform unsuitable for its stated purpose of conditional crowdfunding and creates false expectations for both creators and contributors about fund finality.
Race Condition Creation
The dual availability of withdraw() and refund() after successful campaign deadlines creates race conditions where creators and contributors compete to extract funds first [code_file:14]. Scenario: Campaign raises 20 SOL (goal: 15 SOL). Deadline passes. Creator submits withdraw() transaction to extract 20 SOL. Simultaneously, 3 contributors submit refund() transactions totaling 10 SOL. Transaction ordering determines outcomes—if refunds execute first, creator receives only 10 SOL instead of 20 SOL. This introduces MEV opportunities where sophisticated actors monitor successful campaigns and front-run creator withdrawals to extract maximum refunds. The unpredictability damages platform reliability even when campaigns succeed.
Discovery that successful campaigns can be drained via refunds would severely damage platform adoption and creator trust [file:1]. Creators evaluating the platform would realize that meeting funding goals provides no guarantee of receiving funds, as contributors retain the ability to reverse their commitments post-deadline. This makes the platform non-competitive with established crowdfunding services like Kickstarter or GoFundMe, where success conditions provide clear fund finality. The vulnerability also creates documentation and user education challenges—explaining that "you should only refund failed campaigns" when the code permits refunding successful ones creates confusion and suggests incomplete implementation.
POC RESULT:
Add Goal Vailidation to refund()
## Description the refund mechanism only verifies that the current time has passed the campaign deadline, without checking whether the campaign has failed to meet its funding goal.This oversight may result in refunds being issued even if the campaign was, in principle, successful, potentially undermining the trust and financial integrity of the platform.   ## Vulnerability Details The refund function in the contract is designed to return funds to contributors if a campaign fails. However, it only checks whether the campaign deadline has been reached (or passed) before allowing a refund, without verifying if the campaign's funding goal was met. In other words, the function solely relies on a time-based condition and does not incorporate the additional logic required to determine if a campaign has been unsuccessful. **Code Analysis:**\ The refund function contains the following check: ```Rust if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } ``` This condition ensures that refunds are only triggered after the deadline has passed. However, there is no subsequent verification that compares `fund.amount_raised` to the `fund.goal` to determine whether the campaign failed to meet its funding target. As a result, even if the campaign has met or exceeded its goal, contributors could potentially request refunds simply because the deadline has passed. ## proof Of Concept ```typescript it("Allows refund on a successful campaign due to missing goal check", async () => { // Define campaign parameters with a near-future deadline (5 seconds from now) const fundName = "refund flaw"; const description = "Test for refund vulnerability on a successful campaign"; const goal = new anchor.BN(1000000000); // 1 SOL goal // Set deadline to 5 seconds from now const deadline = new anchor.BN(Math.floor(Date.now() / 1000) + 5); // Generate PDA for the fund using the campaign name and creator's public key let [fundPDA, fundBump] = await PublicKey.findProgramAddress( [Buffer.from(fundName), creator.publicKey.toBuffer()], program.programId ); // Create the fund campaign await program.methods .fundCreate(fundName, description, goal) .accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .rpc(); // Set the campaign deadline await program.methods .setDeadline(deadline) .accounts({ fund: fundPDA, creator: creator.publicKey, }) .rpc(); // Airdrop lamports to otherUser so they can contribute const airdropSig = await provider.connection.requestAirdrop( otherUser.publicKey, 2 * anchor.web3.LAMPORTS_PER_SOL // e.g., 2 SOL ); await provider.connection.confirmTransaction(airdropSig); // Generate PDA for the contribution account using fund's PDA and otherUser's public key let [contributionPDA, contributionBump] = await PublicKey.findProgramAddress( [fundPDA.toBuffer(), otherUser.publicKey.toBuffer()], program.programId ); // otherUser contributes 1 SOL, meeting the campaign goal const contributionAmount = new anchor.BN(1000000000); // 1 SOL await program.methods .contribute(contributionAmount) .accounts({ fund: fundPDA, contributor: otherUser.publicKey, contribution: contributionPDA, systemProgram: anchor.web3.SystemProgram.programId, }) .signers([otherUser]) .rpc(); // Verify the campaign is successful by checking that amountRaised >= goal let fundBeforeDeadline = await program.account.fund.fetch(fundPDA); expect(fundBeforeDeadline.amountRaised.gte(goal)).to.be.true; // Wait until after the deadline has passed await new Promise((resolve) => setTimeout(resolve, 6000)); // otherUser calls refund despite the campaign being successful // (a correct implementation should disallow this refund) let refundTxSucceeded = true; try { await program.methods .refund() .accounts({ fund: fundPDA, contribution: contributionPDA, contributor: otherUser.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .signers([otherUser]) .rpc(); } catch (err) { refundTxSucceeded = false; } // The vulnerability: refund call is erroneously allowed even though the campaign met its goal. expect(refundTxSucceeded).to.be.true; // check the contributor's balance change to further demonstrate the refund was processed. const balanceAfterRefund = await provider.connection.getBalance( otherUser.publicKey ); console.log("Contributor balance after refund:", balanceAfterRefund); }); ``` ## Impact - **Financial Discrepancies:**\ The improper refund mechanism result in successful campaigns losing funds that were meant to be retained by the campaign creator, leading to financial imbalances within the contract. - **Erosion of Trust:**\ Contributors and creators rely on the refund logic to be fair and accurate. The absence of a funding goal check in the refund function erode trust in the platform, as users could experience unexpected fund reversals or disputes over campaign success. - **Operational Risks:**\ Campaigns that meet their funding goals still be subject to refund requests, creating operational inefficiencies and potential disputes between creators and contributors. This undermines the intended crowdfunding model and could deter future participation.   ## Recommendations Update the refund function to include a check that verifies whether the campaign's funding goal has been met. Refunds should only be processed if both the deadline has passed and the `amount_raised` is below the `goal`.    ```Solidity if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } if ctx.accounts.fund.amount_raised >= ctx.accounts.fund.goal { return Err(ErrorCode::CampaignSuccessful.into()); } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.