Rust Fund

AI First Flight #9
Beginner FriendlyRust
EXP
View results
Submission Details
Severity: medium
Valid

Deadline Can Be Reset Multiple Times

Root + Impact

Description

The set_deadline() function contains two compounding bugs: a typo in the state field name (dealine_set instead of
deadline_set) and a logic error where the immutability flag is never actually set to true. This allows creators to change
the deadline multiple times, violating the expectation that deadlines are immutable once set.

Location 1: Struct Definition (Line 190)
#[account]
#[derive(InitSpace)]
pub struct Fund {
#[max_len(200)]
pub name: String,
#[max_len(5000)]
pub description: String,
pub goal: u64,
pub deadline: u64,
pub creator: Pubkey,
pub amount_raised: u64,
pub dealine_set: bool, // @> TYPO: "dealine" instead of "deadline"
}
Location 2: set_deadline() Function (Lines 55-63)
pub fn set_deadline(ctx: Context<FundSetDeadline>, deadline: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
// @> Checks the typo'd field name
if fund.dealine_set {
return Err(ErrorCode::DeadlineAlreadySet.into());
}
// @> Sets deadline value
fund.deadline = deadline;
// @> MISSING: fund.dealine_set = true;
// @> Flag is never updated, remains false forever
Ok(())
}
The Problem:
1. Line 190: Field is named dealine_set (typo)
2. Line 57: Code checks if fund.dealine_set { ... }
3. Line 61: Code sets fund.deadline = deadline;
4. Missing: Never sets fund.dealine_set = true;
Result: The flag stays false forever, so the check on line 57 always passes, allowing unlimited deadline changes.

Risk

Likelihood:

  • Creators have economic incentive to extend deadlines when campaigns approach the deadline without meeting the goal —
    they can continuously call set_deadline() to delay the fundraising period and accumulate more contributions

  • The vulnerability activates on every second call to set_deadline(), making it trivial to exploit; a creator can call the
    function twice in immediate succession and the second call succeeds without any error message

Impact:

  • Contributors' expectations about campaign deadlines are completely undermined — a deadline that was publicly announced
    and set becomes mutable, changing the terms of the agreement mid-campaign

  • Creators can delay refunds indefinitely by extending deadlines, trapping contributor funds and preventing legitimate
    refund claims from occurring when originally promised

  • The deadline becomes a meaningless value that provides no contractual guarantee to contributors, defeating the purpose
    of having a deadline at all

Proof of Concept

This test calls set_deadline() twice in succession, showing that the second call succeeds when it should fail. The first
call sets the deadline and should lock it in place, but because the dealine_set flag is never actually set to true, the
second call passes the same check and overwrites the deadline with a new value. This proves the immutability enforcement
is completely broken.

it("deadline can be set multiple times", async () => {
const creator = provider.wallet;
const [fundPDA] = await findFundPDA("F03Fund", creator.publicKey);
// Create fund
await program.methods.fundCreate("F03Fund", "Test fund", new anchor.BN(1000e9))
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
// First call: Set deadline to T1 (now + 10 seconds)
const deadlineT1 = new anchor.BN(Math.floor(Date.now() / 1000) + 10);
console.log("Setting deadline to T1 (now + 10 sec)...");
await program.methods.setDeadline(deadlineT1)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
})
.rpc();
let fundData = await program.account.fund.fetch(fundPDA);
console.log("First set_deadline: deadline = " + fundData.deadline.toString());
console.log("dealnineSet flag = " + fundData.dealnineSet);
// Second call: Try to set deadline to T2 (now + 60 seconds)
const deadlineT2 = new anchor.BN(Math.floor(Date.now() / 1000) + 60);
console.log("\nSetting deadline to T2 (now + 60 sec)...");
console.log("(This should FAIL but doesn't!)");
// This should fail but doesn't!
await program.methods.setDeadline(deadlineT2)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
})
.rpc();
fundData = await program.account.fund.fetch(fundPDA);
console.log("\nSecond set_deadline SUCCEEDED!");
console.log("Deadline changed from T1 to T2");
console.log("Old deadline: " + deadlineT1.toString());
console.log("New deadline: " + fundData.deadline.toString());
console.log("dealnineSet flag (still): " + fundData.dealnineSet);
// Verify the bug
expect(fundData.deadline.toString()).to.equal(deadlineT2.toString());
console.log("\n❌ BUG CONFIRMED: Deadline is mutable!");
});

Recommended Mitigation

The deadline must be immutable once set to preserve the contract's guarantee that campaigns have fixed expiration dates. A
creator with economic incentive to extend a failing campaign can repeatedly call set_deadline() to push back refund
deadlines indefinitely, manipulating contributor expectations and terms mid-campaign. The fix prevents this by enforcing
single-set semantics.

1. Fix the field name typo (struct definition):
#[account]
#[derive(InitSpace)]
pub struct Fund {
#[max_len(200)]
pub name: String,
#[max_len(5000)]
pub description: String,
pub goal: u64,
pub deadline: u64,
pub creator: Pubkey,
pub amount_raised: u64,
- pub dealine_set: bool, // Typo
+ pub deadline_set: bool,
}
2. Set the flag and fix the check (set_deadline function):
pub fn set_deadline(ctx: Context<FundSetDeadline>, deadline: u64) -> Result<()> {
let fund = &mut ctx.accounts.fund;
- if fund.dealine_set {
+ if fund.deadline_set {
return Err(ErrorCode::DeadlineAlreadySet.into());
}
fund.deadline = deadline;
+ fund.deadline_set = true; // Actually set the flag
Ok(())
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-02] The set_deadline function does not set the dealine_set flag to true

The `set_deadline()` function in the `rustfund` program contains a vulnerability that allows campaign creators to manipulate deadlines indefinitely. While the function correctly checks if `fund.dealine_set` is true before allowing the deadline to be changed, it never sets this flag to true after setting the deadline. ```rust 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(()) } ``` The function is missing a crucial line to update the flag: `fund.dealine_set = true;` This oversight bypasses a key safeguard intended to prevent creators from manipulating deadlines after they've been set. According to the project documentation, this flag is meant to enforce deadline immutability, which is an essential part of the platform's trust model. ### Impact 1. **Refund evasion**: Creators can prevent users from obtaining refunds by continually extending the deadline whenever it approaches. This directly undermines the project's advertised "Refund Mechanism" which promises that "Contributors can get refunds if deadlines are reached and goals aren't met." 2. **Fund locking**: Contributors' funds can be effectively locked indefinitely, as the refund function is contingent upon the deadline being reached: ```rust if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } ``` ### Proof of Concept (PoC) The following test demonstrates how a creator can set the deadline multiple times, effectively bypassing the intended deadline immutability: ```javascript import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { Rustfund } from "../target/types/rustfund"; import { assert } from "chai"; describe("VULN-02: set_deadline vulnerability", () => { // Configures the provider to use the local cluster const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); const program = anchor.workspace.Rustfund as Program<Rustfund>; // Test variables const fundName = "TestFund"; const description = "Testing deadline vulnerability"; const goal = new anchor.BN(1000000); let fundPda: anchor.web3.PublicKey; it("Allows you to modify the deadline several times", async () => { // Derivation of PDA address for financing account [fundPda] = await anchor.web3.PublicKey.findProgramAddress( [Buffer.from(fundName), provider.wallet.publicKey.toBuffer()], program.programId ); // Fund creation await program.rpc.fundCreate(fundName, description, goal, { accounts: { fund: fundPda, creator: provider.wallet.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }, }); // First deadline assignment const deadline1 = new anchor.BN(Math.floor(Date.now() / 1000) + 3600); // 1 hour in the future await program.rpc.setDeadline(deadline1, { accounts: { fund: fundPda, creator: provider.wallet.publicKey, }, }); // Second deadline assignment (which should not be possible if the flag is set to true) const deadline2 = new anchor.BN(Math.floor(Date.now() / 1000) + 7200); // 2 hours into the future await program.rpc.setDeadline(deadline2, { accounts: { fund: fundPda, creator: provider.wallet.publicKey, }, }); // Check that the deadline has been updated to the second value const fundAccount = await program.account.fund.fetch(fundPda); assert.ok( fundAccount.deadline.eq(deadline2), "The deadline may have been modified several times, but vulnerability presents" ); }); }); ``` Save the above test as, for example, tests/02.ts in your project's test directory and run the test : ```Solidity anchor test ``` ### Concrete Impact Example To illustrate the real-world impact of this vulnerability, consider this scenario: - A creator launches a campaign to fund a project with a goal of 100 SOL - The creator sets an initial deadline of 30 days - Contributors collectively deposit 80 SOL (below the goal) - As the deadline approaches, the creator realizes they won't reach the goal - Instead of allowing refunds as promised, the creator extends the deadline by another 30 days - This pattern can repeat indefinitely, effectively locking contributor funds - Even if contributors try to request refunds, they'll be rejected with "DeadlineNotReached" errors ### Recommendation The fix for this vulnerability is straightforward. The `set_deadline()` function should be modified to set the `dealine_set` flag to true after setting the deadline: ```rust 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; // Add this line to fix the vulnerability Ok(()) } ```

Support

FAQs

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

Give us feedback!