dealine_set flag is written once as false and never again, making the DeadlineAlreadySet check unreachableThe program deliberately encodes a one-shot rule for the campaign deadline: a dedicated boolean on the Fund account, a guard that rejects a second call, and a dedicated error variant for it. Once a deadline is announced, contributors should be able to rely on it — it defines when contributions close and when refunds open.
The flag is initialised to false and never set to true by anything. set_deadline writes the new deadline and returns, leaving the flag exactly as it found it. The guard therefore never fires on any input, ErrorCode::DeadlineAlreadySet is unreachable code, and the deadline is freely rewritable for the entire life of the campaign.
The flag appears three times in the entire program, and exactly one of those is a write:
Both gates that depend on the deadline therefore sit under the creator's unilateral, repeatable control:
Two adjacent omissions in the same function compound it, and both survive the obvious fix of simply latching the flag:
No bound on the value. set_deadline accepts any u64, including u64::MAX and any timestamp already in the past. There is no maximum campaign duration, so a deadline can be set that will never be reached.
No ordering constraint. Nothing requires the deadline to be fixed before money arrives. The creator can let contributions accumulate while deadline == 0 — a state in which the fund advertises as maximally permissive, since line 29 admits every contribution and line 69 admits every refund — and only then call set_deadline(u64::MAX), closing the refund window on contributors who are already in. That sequence uses a single, first, entirely "legitimate" call, so latching the flag does not prevent it.
I am reporting these together with the dead guard rather than separately because they are one function, one intent, and one patch: the whole of set_deadline's validation is missing, not one line of it.
Ordinarily "the creator changes a parameter on their own campaign" is designed behaviour and not a finding. That defence does not apply here, because the developer explicitly attempted to forbid it: the dealine_set field exists solely for this restriction, the guard at line 57 exists solely to enforce it, and DeadlineAlreadySet at line 198 exists solely to report it. All three are present and none of them work. This is an invariant the program tries to enforce and fails to, which is a broken check rather than an intentional permission.
fund_create is also permissionless (lines 12-22) — any signer can be a creator — so the actor holding this unintended power is an arbitrary stranger, not a vetted operator.
Likelihood:
The guard fails on 100% of calls. There is no input, ordering or state that makes it fire, because the value it tests can never become true.
Exploiting it needs one instruction, no cost beyond the fee, no timing and no cooperation. It is available for the entire lifetime of every fund ever created.
Impact:
The deadline — the only public commitment a campaign makes about its own lifecycle — carries no guarantee. A contributor who reads fund.deadline before contributing has read a number that can be changed the moment they act on it.
Live consequence: a creator can set a past timestamp and retroactively close their campaign to new contributions, then reopen it, arbitrarily and repeatedly. contribute (line 29) rejects with DeadlineReached for as long as the past value stands.
The refund window is equally under creator control: a deadline pushed to u64::MAX means refund (line 69) rejects with DeadlineNotReached indefinitely.
I set Impact to Low deliberately, and want to be explicit about why rather than let a judge discover it. In this program a separate defect means contribution.amount is only ever assigned 0, so refund moves zero lamports regardless. That makes the refund-blocking consequence above inert today — blocking refund currently blocks a no-op, and no contributor lamports are actually protected by it. The consequence that is genuinely live is the contribution-blocking one, which is a creator griefing their own fundraiser.
So: Likelihood High, because the guard is unconditionally broken; Impact Low, because today's reachable harm is limited. The reason this still matters is that it is a latent hazard sitting directly under the refund mechanism — the moment amounts are recorded correctly, this flag becomes the switch that decides whether contributors can ever exit, and it is currently held by the one party with an interest in it staying off.
Three consecutive set_deadline calls, all accepted, with the flag still false afterwards.
Output:
tests/rustfund.ts calls setDeadline exactly once and asserts nothing (every check is a console.log), so the suite never exercises a second call and cannot observe that the guard is inert.
Raise the flag the guard depends on, bound the value, and forbid changing the terms after contributors have committed money.
Taking the deadline as a parameter of fund_create instead would remove the window entirely and make the field immutable by construction — a fund would then never exist in the deadline == 0 state. Note also that the field name is misspelled (dealine_set); renaming it to deadline_set is worth doing in the same change, since the typo is what makes the missing assignment easy to overlook.
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(()) } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.