Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Severity: high
Valid

Logic Error in Anchor Program Causes Total Protocol Failure and Contributor Fund Loss

Root + Impact

Description

  • In a properly functioning crowdfunding smart contract, when a contributor calls the contribute() function and transfers SOL to the campaign, the contract must maintain two critical accounting records: (1) the global fund.amount_raised tracking total contributions across all users, and (2) the individual contribution.amount tracking each user's specific contribution for refund purposes. When a campaign fails to reach its funding goal by the deadline, contributors should be able to call the refund() function, which reads their individual contribution.amount and transfers that exact amount of SOL back from the fund PDA to their wallet, ensuring contributors can recover 100% of their funds from unsuccessful campaigns.

  • The contribute() function contains a critical state update omission on line 50 of lib.rs where contribution.amount is never incremented after the SOL transfer completes. While the function correctly transfers SOL from the contributor to the fund PDA and updates the global fund.amount_raised, it completely fails to record the contribution amount in the individual contribution.amount field, leaving it permanently at 0. Consequently, when contributors attempt to reclaim their funds via the refund() function after a campaign fails, the function reads contribution.amount = 0 and transfers zero lamports back to the user, resulting in 100% permanent fund loss for all contributors while their SOL remains trapped in the fund PDA with no recovery mechanism .

// File: lib.rs,
pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let contribution = &mut ctx.accounts.contribution;
if fund.deadline != 0 && fund.deadline < Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineReached.into());
}
// Initialize or update contribution record
if contribution.contributor == Pubkey::default() {
contribution.contributor = ctx.accounts.contributor.key();
contribution.fund = fund.key();
contribution.amount = 0; // Initialized to 0
}
// Transfer SOL from contributor to fund account
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)?; // ✅ Transfer succeeds
fund.amount_raised += amount; // ✅ Global total updated
// @> MISSING: contribution.amount += amount;
// @> Root Cause: Individual contribution tracking is never updated
// @> This causes contribution.amount to remain 0 permanently
Ok(())
}

Risk

Likelihood:

  • This vulnerability triggers automatically on every single contribution made to any crowdfunding campaign in the protocol, affecting all users under all conditions without exception. The missing state update contribution.amount += amount; means that every time the contribute() function executes successfully, the individual contribution tracking remains at zero regardless of the actual SOL amount transferred. There are no conditional branches, access controls, or timing dependencies that could prevent this bug from manifesting—it occurs deterministically in the core execution path of the contribution logic.

  • The vulnerability's impact becomes realized whenever any contributor attempts to reclaim their funds through the refund() function after a campaign fails to meet its goal by the deadline . Since crowdfunding campaigns inherently carry the possibility of failure (contributors may not reach the funding goal), and the entire purpose of the refund mechanism is to protect contributors in these scenarios, the exploit path will be exercised in normal protocol operation. Every failed campaign results in contributors discovering they cannot recover their funds, making this a guaranteed protocol failure rather than an edge case requiring specific attacker actions or unusual circumstances.

Impact:

  • Contributors suffer 100% permanent loss of all funds sent to any crowdfunding campaign that fails to reach its goal . When the refund() function reads contribution.amount = 0 instead of the actual contributed amount, it transfers zero lamports back to users, leaving their SOL permanently trapped in the fund PDA with no recovery mechanism. A contributor who sends 5 SOL receives 0 SOL back during refund attempts, as demonstrated in the proof of concept where 5.03759096 SOL remained locked in the fund account after the refund transaction completed .

  • The refund mechanism—a core safety feature and fundamental promise of crowdfunding smart contracts—is completely non-functional, rendering the entire protocol unfit for its intended purpose. All contributors to all campaigns are affected universally, with every failed campaign resulting in total fund loss for all its backers. The bug creates an asymmetric exploitation scenario where creators can successfully call withdraw() using the correctly-tracked fund.amount_raised, enabling them to drain funds even from failed campaigns, while contributors cannot recover anything through the broken refund path.

Proof of Concept

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Rustfund } from "../target/types/rustfund";
import { PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { expect } from 'chai';
describe("[CRITICAL-01] Proof of Concept: Contribution Amount Never Recorded", () => {
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 = "PoC Critical Bug Fund";
const description = "Testing critical vulnerability";
const goal = new anchor.BN(10 * LAMPORTS_PER_SOL); // 10 SOL goal
const contributionAmount = new anchor.BN(5 * LAMPORTS_PER_SOL); // 5 SOL contribution
const deadline = new anchor.BN(Math.floor(Date.now() / 1000) + 5); // 5 seconds
let fundPDA: PublicKey;
let contributionPDA: PublicKey;
before(async () => {
// Airdrop SOL to contributor for testing
const airdropSig = await provider.connection.requestAirdrop(
contributor.publicKey,
10 * LAMPORTS_PER_SOL
);
await provider.connection.confirmTransaction(airdropSig);
// Generate PDA for fund
[fundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(fundName), creator.publicKey.toBuffer()],
program.programId
);
// Generate PDA for contribution
[contributionPDA] = await PublicKey.findProgramAddress(
[fundPDA.toBuffer(), contributor.publicKey.toBuffer()],
program.programId
);
});
it("Step 1: Create crowdfunding campaign", async () => {
await program.methods
.fundCreate(fundName, description, goal)
.accounts({})
.rpc();
const fund = await program.account.fund.fetch(fundPDA);
console.log("\n=== FUND CREATED ===");
console.log(`Fund Name: ${fund.name}`);
console.log(`Goal: ${fund.goal.toNumber() / LAMPORTS_PER_SOL} SOL`);
console.log(`Amount Raised: ${fund.amountRaised.toNumber() / LAMPORTS_PER_SOL} SOL`);
expect(fund.amountRaised.toNumber()).to.equal(0);
});
it("Step 2: Set deadline for the campaign", async () => {
await program.methods
.setDeadline(deadline)
.accounts({
fund: fundPDA,
})
.rpc();
const fund = await program.account.fund.fetch(fundPDA);
console.log(`\n=== DEADLINE SET ===`);
console.log(`Deadline: ${new Date(fund.deadline.toNumber() * 1000).toISOString()}`);
});
it("Step 3: Contributor sends 5 SOL to campaign", async () => {
const contributorBalanceBefore = await provider.connection.getBalance(contributor.publicKey);
const fundBalanceBefore = await provider.connection.getBalance(fundPDA);
console.log("\n=== BEFORE CONTRIBUTION ===");
console.log(`Contributor Balance: ${contributorBalanceBefore / LAMPORTS_PER_SOL} SOL`);
console.log(`Fund PDA Balance: ${fundBalanceBefore / LAMPORTS_PER_SOL} SOL`);
await program.methods
.contribute(contributionAmount)
.accounts({
fund: fundPDA,
contributor: contributor.publicKey,
})
.signers([contributor])
.rpc();
const contributorBalanceAfter = await provider.connection.getBalance(contributor.publicKey);
const fundBalanceAfter = await provider.connection.getBalance(fundPDA);
const fund = await program.account.fund.fetch(fundPDA);
const contributionAccount = await program.account.contribution.fetch(contributionPDA);
console.log("\n=== AFTER CONTRIBUTION ===");
console.log(`Contributor Balance: ${contributorBalanceAfter / LAMPORTS_PER_SOL} SOL`);
console.log(`Fund PDA Balance: ${fundBalanceAfter / LAMPORTS_PER_SOL} SOL`);
console.log(`Fund.amount_raised: ${fund.amountRaised.toNumber() / LAMPORTS_PER_SOL} SOL`);
console.log(`\n🔴 CRITICAL BUG DETECTED:`);
console.log(`Contribution.amount (should be 5 SOL): ${contributionAccount.amount.toNumber() / LAMPORTS_PER_SOL} SOL`);
// Verify the bug: contribution.amount is 0 despite 5 SOL being transferred
expect(contributionAccount.amount.toNumber()).to.equal(0); // ❌ This is the bug!
expect(fund.amountRaised.toNumber()).to.equal(contributionAmount.toNumber()); // ✅ Fund total is correct
// Verify SOL was actually transferred
const actualTransferred = fundBalanceAfter - fundBalanceBefore;
console.log(`\nActual SOL transferred to fund: ${actualTransferred / LAMPORTS_PER_SOL} SOL`);
expect(actualTransferred).to.be.greaterThan(4.9 * LAMPORTS_PER_SOL); // Account for rent
});
it("Step 4: Campaign fails (deadline passes without reaching goal)", async () => {
console.log("\n=== WAITING FOR DEADLINE TO PASS ===");
console.log("Waiting 10 seconds for deadline expiry...");
await new Promise(resolve => setTimeout(resolve, 10000));
const currentTime = Math.floor(Date.now() / 1000);
const fund = await program.account.fund.fetch(fundPDA);
console.log(`Current Time: ${new Date(currentTime * 1000).toISOString()}`);
console.log(`Deadline: ${new Date(fund.deadline.toNumber() * 1000).toISOString()}`);
console.log(`Campaign Failed: Goal not reached (${fund.amountRaised.toNumber() / LAMPORTS_PER_SOL} SOL < ${fund.goal.toNumber() / LAMPORTS_PER_SOL} SOL)`);
});
it("Step 5: EXPLOIT - Refund returns 0 SOL instead of 5 SOL", async () => {
const contributorBalanceBefore = await provider.connection.getBalance(contributor.publicKey);
const fundBalanceBefore = await provider.connection.getBalance(fundPDA);
const contributionAccount = await program.account.contribution.fetch(contributionPDA);
console.log("\n=== ATTEMPTING REFUND ===");
console.log(`Contributor Balance Before Refund: ${contributorBalanceBefore / LAMPORTS_PER_SOL} SOL`);
console.log(`Fund PDA Balance Before Refund: ${fundBalanceBefore / LAMPORTS_PER_SOL} SOL`);
console.log(`Contribution.amount (to be refunded): ${contributionAccount.amount.toNumber() / LAMPORTS_PER_SOL} SOL`);
await program.methods
.refund()
.accounts({
fund: fundPDA,
contributor: contributor.publicKey,
})
.signers([contributor])
.rpc();
const contributorBalanceAfter = await provider.connection.getBalance(contributor.publicKey);
const fundBalanceAfter = await provider.connection.getBalance(fundPDA);
const actualRefunded = contributorBalanceAfter - contributorBalanceBefore;
console.log("\n=== AFTER REFUND ===");
console.log(`Contributor Balance After Refund: ${contributorBalanceAfter / LAMPORTS_PER_SOL} SOL`);
console.log(`Fund PDA Balance After Refund: ${fundBalanceAfter / LAMPORTS_PER_SOL} SOL`);
console.log(`\n💥 CRITICAL VULNERABILITY CONFIRMED:`);
console.log(`Expected Refund: 5.0 SOL`);
console.log(`Actual Refund: ${actualRefunded / LAMPORTS_PER_SOL} SOL`);
console.log(`\n⚠️ FUNDS PERMANENTLY TRAPPED: ${(fundBalanceAfter - 0.00203928 * LAMPORTS_PER_SOL) / LAMPORTS_PER_SOL} SOL`);
// Verify the exploit: contributor receives ~0 SOL back (only gas refund)
expect(actualRefunded).to.be.lessThan(0.001 * LAMPORTS_PER_SOL); // Only gas, no refund
// Verify funds are still trapped in the fund account
expect(fundBalanceAfter).to.be.greaterThan(4.9 * LAMPORTS_PER_SOL);
});
it("Step 6: Impact Summary", () => {
console.log("\n" + "=".repeat(80));
console.log("CRITICAL VULNERABILITY IMPACT SUMMARY");
console.log("=".repeat(80));
console.log("\n📋 Vulnerability Details:");
console.log(" • Bug Location: contribute() function (line 25-52 in lib.rs)");
console.log(" • Missing Code: contribution.amount += amount;");
console.log(" • Root Cause: State update omission in contribution recording");
console.log("\n💰 Financial Impact:");
console.log(" • Contributor sent: 5.0 SOL");
console.log(" • Contributor received: ~0.0 SOL (only gas refund)");
console.log(" • Funds trapped: 5.0 SOL (100% loss)");
console.log("\n🎯 Attack Vector:");
console.log(" 1. Any user contributes to a campaign");
console.log(" 2. Campaign fails to reach goal");
console.log(" 3. User attempts refund");
console.log(" 4. refund() reads contribution.amount → always 0");
console.log(" 5. Zero lamports transferred back");
console.log(" 6. Funds permanently trapped in fund PDA");
console.log("\n⚠️ Severity: CRITICAL");
console.log(" • Affects: 100% of contributors");
console.log(" • Exploitability: Automatic (no attacker needed)");
console.log(" • Fund Recovery: Impossible without contract upgrade");
console.log(" • Protocol Status: Completely broken");
console.log("\n🔧 Fix Required:");
console.log(" In contribute() function, add after line 50:");
console.log(" contribution.amount += amount;");
console.log("\n" + "=".repeat(80));
});
});

POC RESULT:

[CRITICAL-01] Proof of Concept: Contribution Amount Never Recorded
=== FUND CREATED ===
Fund Name: PoC Critical Bug Fund
Goal: 10 SOL
Amount Raised: 0 SOL
✔ Step 1: Create crowdfunding campaign (405ms)
=== DEADLINE SET ===
Deadline: 2026-02-04T21:11:02.000Z
✔ Step 2: Set deadline for the campaign (403ms)
=== BEFORE CONTRIBUTION ===
Contributor Balance: 10 SOL
Fund PDA Balance: 0.03759096 SOL
=== AFTER CONTRIBUTION ===
Contributor Balance: 4.99855232 SOL
Fund PDA Balance: 5.03759096 SOL
Fund.amount_raised: 5 SOL
🔴 CRITICAL BUG DETECTED:
Contribution.amount (should be 5 SOL): 0 SOL
Actual SOL transferred to fund: 5 SOL
✔ Step 3: Contributor sends 5 SOL to campaign (406ms)
=== WAITING FOR DEADLINE TO PASS ===
Waiting 10 seconds for deadline expiry...
Current Time: 2026-02-04T21:11:08.000Z
Deadline: 2026-02-04T21:11:02.000Z
Campaign Failed: Goal not reached (5 SOL < 10 SOL)
✔ Step 4: Campaign fails (deadline passes without reaching goal) (10008ms)
=== ATTEMPTING REFUND ===
Contributor Balance Before Refund: 4.99855232 SOL
Fund PDA Balance Before Refund: 5.03759096 SOL
Contribution.amount (to be refunded): 0 SOL
=== AFTER REFUND ===
Contributor Balance After Refund: 4.99855232 SOL
Fund PDA Balance After Refund: 5.03759096 SOL
💥 CRITICAL VULNERABILITY CONFIRMED:
Expected Refund: 5.0 SOL
Actual Refund: 0 SOL
⚠️ FUNDS PERMANENTLY TRAPPED: 5.03555168 SOL
✔ Step 5: EXPLOIT - Refund returns 0 SOL instead of 5 SOL (242ms)
================================================================================
CRITICAL VULNERABILITY IMPACT SUMMARY
================================================================================
📋 Vulnerability Details:
• Bug Location: contribute() function (line 25-52 in lib.rs)
• Missing Code: contribution.amount += amount;
• Root Cause: State update omission in contribution recording
💰 Financial Impact:
• Contributor sent: 5.0 SOL
• Contributor received: ~0.0 SOL (only gas refund)
• Funds trapped: 5.0 SOL (100% loss)
🎯 Attack Vector:
1. Any user contributes to a campaign
2. Campaign fails to reach goal
3. User attempts refund
4. refund() reads contribution.amount → always 0
5. Zero lamports transferred back
6. Funds permanently trapped in fund PDA
⚠️ Severity: CRITICAL
• Affects: 100% of contributors
• Exploitability: Automatic (no attacker needed)
• Fund Recovery: Impossible without contract upgrade
• Protocol Status: Completely broken
🔧 Fix Required:
In contribute() function, add after line 50:
contribution.amount += amount;
================================================================================
✔ Step 6: Impact Summary
rustfund
fundName firstflight Fund
fundDescription this program is for firstflight
fundGoal <BN: 3b9aca00>
fundCreator PublicKey [PublicKey(7NHizJffomjBQGQhoSES196FRXk2v5rPtqvNF6vNZg2o)] {
_bn: <BN: 5e9a3a75ab72a017a4a1cf8bbb09c4a5d44c54f9016a597f9e1d454311795394>
}
fundAmountRaised <BN: 0>
✔ Creates a fund (403ms)
fundDeadline <BN: 6983b5eb>
✔ Sets a deadline (436ms)
1) Contributes to fund
fundBalanceBefore 37590960
2) Refunds contribution
creatorBalanceAfter 499999999924773060
fundBalanceAfter 37590960
✔ Withdraws funds (331ms)

Recommended Mitigation

Update Contribution Amount in contribute() Function

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
let contribution = &mut ctx.accounts.contribution;
if fund.deadline != 0 && fund.deadline < Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineReached.into());
}
// Initialize or update contribution record
if contribution.contributor == Pubkey::default() {
contribution.contributor = ctx.accounts.contributor.key();
contribution.fund = fund.key();
contribution.amount = 0;
}
// Transfer SOL from contributor to fund account
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;
+ contribution.amount += amount;
Ok(())
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 2 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-03] Permanent Loss of Contributor Funds: Missing Update to contribution.amount in the contribute() rustfund Contract

## Description The `rustfund` contract contains a logical error in the `contribute()` function that prevents `contribution.amount` from updating after a user makes a donation. Even though the code increments `fund.amount_raised`, the individual contributor’s record is never updated. As a result, the refund mechanism relies on a zeroed `contribution.amount`, preventing contributors from recovering the correct amount of funds. This issue disrupts the expected crowdfunding flow, undermines the integrity of individual contributions, and ultimately breaks the refund logic for users who should be entitled to their donated lamports if a project does not reach its goal. ## Vulnerability Details The `rustfund` contract fails to update the `contribution.amount` field in the `contribute()` function. While `fund.amount_raised` reflects the total lamports contributed, individual contributors’ amounts remain at zero, effectively breaking the logic for refunds. This oversight compromises the contract’s guarantee that users can retrieve their funds if the project does not succeed or if they become eligible for a refund. In its current state, once a user initiates a valid contribution, there is no proper record of their deposit aside from the aggregated fund total. Any subsequent `refund()` call will use the uninitialized `contribution.amount` (which remains zero), meaning contributors are unable to recover their deposits. Although this issue does not inherently enable an external attacker to steal funds directly, it causes loss of user funds through an incomplete or misleading refund process. ## Impact This logic flaw undermines the contract’s refund mechanism, potentially causing permanent loss of contributed funds. Contributors are led to believe they can retrieve their deposits if the crowdfunding goal is not met or the deadline passes; however, because `contribution.amount` never reflects the actual amount contributed, no valid refund can occur. This defect results in a direct financial impact for users who cannot recover their funds, and it diminishes trust in the contract’s overall integrity. ## Likelihood Explanation This vulnerability manifests whenever contributors interact with the `contribute()` and `refund()` functions in a real-world scenario. Because the missing code update is consistent across all calls, **every** contribution will fail to correctly record the contributor’s amount. Consequently, any refund operation will lead to the same zero-amount issue. This makes the flaw highly likely to occur and reliably reproducible for every user who attempts to donate and then request a refund. ## Proof of Concept The logical error lies in the `contribute()` function, where the `amount` is transferred to the `fund` and `fund.amount_raised` is incremented, yet `contribution.amount` remains unchanged. As a result, if `refund()` is called later, the contributed funds are not reimbursed because `contribution.amount` remains at zero. ### Code Analysis - [lib.rs -](https://github.com/CodeHawks-Contests/2025-03-rustfund/blob/main/programs/rustfund/src/lib.rs#L34-L52) [`contribute`](https://github.com/CodeHawks-Contests/2025-03-rustfund/blob/main/programs/rustfund/src/lib.rs#L34-L51) Below is an abridged version of the `contribute()` function focusing on the relevant sections: ```Rust pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> { // ... Preliminary code ... // Initialize or update contribution record if contribution.contributor == Pubkey::default() { contribution.contributor = ctx.accounts.contributor.key(); contribution.fund = fund.key(); contribution.amount = 0; } // (!) The amount is transferred but 'contribution.amount' is never updated 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(()) } ``` After `system_program::transfer(...)`, the update to `contribution.amount` is missing. The required line should be: ```rust contribution.amount = contribution.amount.checked_add(amount) .ok_or(ErrorCode::CalculationOverflow)?; ``` ### Explanation Since `contribution.amount` never increments during a contribution, the contract correctly records the transferred amount in `fund.amount_raised` but fails to mirror that amount in the contribution account. Consequently, `refund()` relies on a `contribution.amount` that remains zero, preventing users from retrieving their funds. ### Vulnerable Scenario 1. Alice creates a new fund using `fund_create()`. 2. Alice contributes 0.5 SOL via `contribute()`. Internally, `fund.amount_raised` increments, but `contribution.amount` remains at 0. 3. The fund’s deadline passes, and `refund()` is called. 4. The `refund()` function attempts to return the amount stored in `contribution.amount`, which is 0, so Alice does not get her 0.5 SOL back. ### Test and Result This test aims to verify that when a user contributes a specific amount to the fund, both `contribution.amount` and `fund.amountRaised` are updated accordingly. After invoking the `contribute()` method and fetching the relevant on-chain accounts, the test checks if the recorded amounts match the expected value. In the provided output, `contribution.amount` remains at zero instead of reflecting the correct 500000000 lamports, confirming that the code to increment this field is missing or not executed, resulting in the failing assertion. - Add the following test to `tests/rustfund.ts` after of the function test Contributes to fund ```TypeScript it("Contributes to fund", async () => {}); it("should update the contribution amount when a user contributes", async () => { // Derive the PDA for the contribution account [contributionPDA, contributionBump] = await PublicKey.findProgramAddress( [fundPDA.toBuffer(), provider.wallet.publicKey.toBuffer()], program.programId ); // Invoke the 'contribute' function to transfer the specified amount await program.methods .contribute(contribution) .accounts({ fund: fundPDA, contributor: provider.wallet.publicKey, contribution: contributionPDA, systemProgram: anchor.web3.SystemProgram.programId, }) .rpc(); // Fetch the updated 'fund' and 'contribution' accounts to validate changes const fundAccount = await program.account.fund.fetch(fundPDA); const contributionAccount = await program.account.contribution.fetch( contributionPDA ); // Confirm that 'contribution.amount' correctly reflects the contributed amount expect(contributionAccount.amount.toNumber()).to.equal( contribution.toNumber(), "The contribution.amount was not correctly updated" ); // Verify that 'fund.amountRaised' also matches the newly contributed amount expect(fundAccount.amountRaised.toNumber()).to.equal( contribution.toNumber(), "The fund.amountRaised was not correctly updated" ); }); it("Refunds contribution", async () => {}); ``` ```bash 1) rustfund should update the contribution amount when a user contributes: The contribution.amount was not correctly updated + expected - actual -0 +500000000 ``` ### Confirmation This flaw is confirmed by observing that `contribution.amount` never increases after a contribution. Its persistent zero value leads to `refund()` failing to return the appropriate funds. A safe and effective fix is to update `contribution.amount` within `contribute()`, for example by using `checked_add` to avoid overflow. ## Recommendations Include a line to increment the `contribution.amount` within the `contribute()` function, ensuring it tracks each user's donation amount. Use a safe addition operation to prevent overflow: ```rust contribution.amount = contribution.amount.checked_add(amount) .ok_or(ErrorCode::CalculationOverflow)?; ``` This change ensures the refund mechanism properly returns the correct amount to contributors.

Support

FAQs

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

Give us feedback!