RustFund

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

[H-4] Users get no refunds

Summary

The current implementation of the contribute function doesn't track the amounts that each contributor sends to the crowdfunding fund.

Vulnerability Details

If the campaign doesn't reach the goal and users attempt to get refunds, they will get no tokens back because their ctx.accounts.contribution.amount will be zero.

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
//..
//..
//@audit this function doesn't track contributed amounts
system_program::transfer(cpi_context, amount)?;
fund.amount_raised += amount;
Ok(())
}

Impact

Loss of funds for users.

PoC

Add the following test to the test file

// Helper to airdrop lamports to a given public key.
async function airdropSol(publicKey: PublicKey, amount: number) {
const airdropTx = await anchor
.getProvider()
.connection.requestAirdrop(publicKey, amount);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx: string) {
const latestBlockHash = await anchor
.getProvider()
.connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
it.only("eronous refunds due to wrong contributions", async () => {
// Set up parameters.
const testFundName = "RefundBugFund-" + Date.now();
const testDescription = "Test fund for refund bug demonstration";
const testGoal = new anchor.BN(10_000_000_000); // 10 SOL in lamports.
const depositAmount = new anchor.BN(4_000_000_000); // 6 SOL deposit.
// Set deadline to 10 seconds from now.
const deadline = new anchor.BN(Math.floor(Date.now() / 1000) + 10);
// Derive the PDA for the fund using seeds [name, creator].
const [testFundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(testFundName), creator.publicKey.toBuffer()],
program.programId
);
// Create the fund.
await program.methods
.fundCreate(testFundName, testDescription, testGoal)
.accounts({
fund: testFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Set the deadline.
await program.methods
.setDeadline(deadline)
.accounts({
fund: testFundPDA,
creator: creator.publicKey,
})
.rpc();
// Contribution from the creator.
const [creatorContributionPDA] = await PublicKey.findProgramAddress(
[testFundPDA.toBuffer(), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.contribute(depositAmount)
.accounts({
fund: testFundPDA,
contributor: creator.publicKey,
contribution: creatorContributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// Contribution from another user.
await airdropSol(otherUser.publicKey, 10_000_000_000); // ensure sufficient funds (10 SOL).
const [otherContributionPDA] = await PublicKey.findProgramAddress(
[testFundPDA.toBuffer(), otherUser.publicKey.toBuffer()],
program.programId
);
await program.methods
.contribute(depositAmount)
.accounts({
fund: testFundPDA,
contributor: otherUser.publicKey,
contribution: otherContributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([otherUser])
.rpc();
// Verify that total amount raised is 8 SOL, which is below the 10 SOL goal
const fundAccount = await program.account.fund.fetch(testFundPDA);
console.log("Fund goal:", fundAccount.goal.toString());
console.log("Fund amount raised:", fundAccount.amountRaised.toString());
expect(fundAccount.amountRaised.eq(new anchor.BN(8_000_000_000))).to.be
.true;
expect(fundAccount.amountRaised.lt(testGoal)).to.be.true;
// Wait for the deadline to pass.
console.log("Waiting for deadline to pass...");
await new Promise((resolve) => setTimeout(resolve, 15000));
// Call refund as the user.
const contributorBalanceBefore = await provider.connection.getBalance(
otherUser.publicKey
);
await program.methods
.refund()
.accounts({
fund: testFundPDA,
contribution: otherContributionPDA,
contributor: otherUser.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([otherUser])
.rpc();
const contributorBalanceAfter = await provider.connection.getBalance(
otherUser.publicKey
);
console.log("Contributor balance before refund:", contributorBalanceBefore);
console.log("Contributor balance after refund:", contributorBalanceAfter);
});

Test output

rustfund
(node:18196) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Fund goal: 10000000000
Fund amount raised: 8000000000
Waiting for deadline to pass...
Contributor balance before refund: 5998552320
Contributor balance after refund: 5998552320
✔ eronous refunds due to wrong contributions (16828ms)
1 passing (17s)
Done in 17.96s.

The test clearly shows that the balance of the user doesn't change before and after a refund.

Tools Used

Manual review

Recommendations

pub fn contribute(ctx: Context<FundContribute>, amount: u64) -> Result<()> {
//..
//..
fund.amount_raised += amount;
+ contribution.amount += amount;
Ok(())
}
Updates

Appeal created

bube Lead Judge 5 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.