Contributors rely on campaign parameters such as the deadline and goal when deciding whether to contribute. After contributors deposit SOL, the creator should not be able to arbitrarily change the campaign deadline and then withdraw funds from an underfunded campaign.
pub fn set_deadline(ctx: Context<FundSetDeadline>, deadline: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
if fund.dealine_set {
return Err(ErrorCode::DeadlineAlreadySet.into());
}
@> fund.deadline = deadline;
@>
Ok(())
}
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
@> let amount = ctx.accounts.fund.amount_raised;
@>
**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.creator.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.creator.to_account_info().lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
Ok(())
}
const airdropSolAmount = 5;
const airdrop = async (
pubkey: anchor.web3.PublicKey,
sol = airdropSolAmount
) => {
const sig = await provider.connection.requestAirdrop(
pubkey,
sol * anchor.web3.LAMPORTS_PER_SOL
);
const latest = await provider.connection.getLatestBlockhash();
await provider.connection.confirmTransaction(
{
signature: sig,
blockhash: latest.blockhash,
lastValidBlockHeight: latest.lastValidBlockHeight,
},
"confirmed"
);
};
it("creator can alter deadline after contribution and withdraw before goal is reached", async () => {
const testCreator = provider.wallet;
const testContributor = anchor.web3.Keypair.generate();
const testFundName = `refund-accounting-${Date.now()}`;
const testContribution = new anchor.BN(anchor.web3.LAMPORTS_PER_SOL / 2);
await airdrop(testContributor.publicKey);
const [testFundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(testFundName), testCreator.publicKey.toBuffer()],
program.programId
);
const [testContributionPDA] = await PublicKey.findProgramAddress(
[testFundPDA.toBuffer(), testContributor.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(testFundName, description, goal)
.accountsPartial({
fund: testFundPDA,
creator: testCreator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const futureDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 100);
await program.methods
.setDeadline(futureDeadline)
.accountsPartial({
fund: testFundPDA,
creator: testCreator.publicKey
})
.rpc();
const fundAccountBeforeSetPast =
await program.account.fund.fetch(testFundPDA);
await program.methods
.contribute(testContribution)
.accountsPartial({
fund: testFundPDA,
contributor: testContributor.publicKey,
contribution: testContributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([testContributor])
.rpc();
const pastDeadline = new anchor.BN(Math.floor(Date.now() / 1000) - 150);
await program.methods
.setDeadline(pastDeadline)
.accountsPartial({
fund: testFundPDA,
creator: testCreator.publicKey
})
.rpc();
const fundAccountAfterSetPast =
await program.account.fund.fetch(testFundPDA);
expect(fundAccountBeforeSetPast.deadline.toNumber()).to.eq(
futureDeadline.toNumber()
);
expect(fundAccountAfterSetPast.deadline.toNumber()).to.eq(
pastDeadline.toNumber()
);
expect(fundAccountAfterSetPast.deadline.toNumber()).to.be.lessThan(
Math.floor(Date.now() / 1000)
);
expect(fundAccountBeforeSetPast.deadline.toNumber()).to.be.greaterThan(
fundAccountAfterSetPast.deadline.toNumber()
);
const fundBeforeWithdraw = await program.account.fund.fetch(testFundPDA);
expect(fundBeforeWithdraw.amountRaised.eq(testContribution)).to.eq(true);
expect(fundBeforeWithdraw.goal.toNumber()).to.be.greaterThan(
fundBeforeWithdraw.amountRaised.toNumber()
);
const fundBalanceBeforeWithdraw =
await provider.connection.getBalance(testFundPDA);
const creatorBalanceBeforeWithdraw =
await provider.connection.getBalance(testCreator.publicKey);
await program.methods
.withdraw()
.accountsPartial({
fund: testFundPDA,
creator: testCreator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const fundBalanceAfterWithdraw =
await provider.connection.getBalance(testFundPDA);
const creatorBalanceAfterWithdraw =
await provider.connection.getBalance(testCreator.publicKey);
const fundDelta = fundBalanceBeforeWithdraw - fundBalanceAfterWithdraw;
const creatorDelta = creatorBalanceAfterWithdraw - creatorBalanceBeforeWithdraw;
expect(fundDelta).to.eq(fundBeforeWithdraw.amountRaised.toNumber());
expect(creatorDelta).to.be.greaterThan(0);
expect(creatorDelta).to.be.lessThanOrEqual(fundDelta);
});
The test first sets a future deadline, accepts a contribution, then changes the deadline to a past timestamp. The fund remains below its goal, but the creator still withdraws the contributed lamports.
Lock the deadline after the first successful update and enforce the intended withdrawal conditions before transferring funds.
pub fn set_deadline(ctx: Context<FundSetDeadline>, deadline: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
if fund.dealine_set {
return Err(ErrorCode::DeadlineAlreadySet.into());
}
fund.deadline = deadline;
+ fund.dealine_set = true;
Ok(())
}
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
+ let now: u64 = Clock::get()?
+ .unix_timestamp
+ .try_into()
+ .map_err(|_| ErrorCode::CalculationOverflow)?;
+
+ require!(
+ ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline <= now,
+ ErrorCode::DeadlineNotReached
+ );
+ require!(
+ ctx.accounts.fund.amount_raised >= ctx.accounts.fund.goal,
+ ErrorCode::GoalNotReached
+ );
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.fund.to_account_info().lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;