Rust Fund

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

`set_deadline` never sets `dealine_set = true` one-shot guard broken

rustfund::lib::set_deadline · Confidence: 70 · Severity: Medium (Stage-4 calibrated High → Stage-5 platform downgrade for centralization-adjacency)

Validation summary

Stage Verdict Note
2 — Audit candidate at High, conf 95 Invariant, Auth/Account/Signer, Execution Trace, Vector Scan (V34)
3 — PoC ADVANCE Tier 2 (TS test calling setDeadline three times in succession)
4 — Adversarial Pass D calibrated High (CONFIRMED) Challenge 2 (centralization-adjacency) noted but does not invalidate; TR-2 (code bug, not trust issue) applies
5 — Platform DOWNGRADE(Medium) score 70/100 CodeHawks centralization rule: admin-only paths typically Medium unless user-impact propagation is foregrounded
6 — Program in-scope same chain
7 — Duplication no hard duplicates; field typo dealine_set confirms no prior fix

Root cause

set_deadline (programs/rustfund/src/lib.rs:55-63) checks fund.dealine_set to gate the function as one-shot, but never writes fund.dealine_set = true. A grep for dealine_set returns three sites — the field declaration, the false initialization in fund_create, and the if check — but no true assignment anywhere in the program. The one-shot guarantee is not enforced.

Location

  • rustfund::lib::set_deadlineprograms/rustfund/src/lib.rs:55-63 (specifically: missing fund.dealine_set = true after lib.rs:61)

Exploit path

  1. Creator calls fund_create(...)fund.dealine_set = false.

  2. Creator calls set_deadline(d1)if fund.dealine_set evaluates to false; function writes fund.deadline = d1; returns Ok. fund.dealine_set is still false.

  3. Creator calls set_deadline(d2) — same flow, function rewrites fund.deadline = d2. Repeats indefinitely. The DeadlineAlreadySet error never fires.

  4. Combined with F-04: creator can also delay calling set_deadline while contributions are accepted indefinitely. Combined with F-02-fixed: creator can extend deadline forever to block contributors' refunds.

Proof of concept

  • tier: 2 (TS test)

  • file: argus/20260504T004143Z/3-poc/F-03/poc.ts

  • reproduction: argus/20260504T004143Z/3-poc/F-03/repro.md

  • proven impact: Three consecutive set_deadline calls (with firstDeadline, secondDeadline, then a deadline in the past) all succeed. After all three, fund.dealine_set is still false, and fund.deadline reflects the third value.

F-04 reproduction

// PoC for F-04 — `contribute` and `refund` deadline-checks short-circuit
// when `fund.deadline == 0`, i.e. when the creator has not yet called set_deadline.
//
// This test demonstrates the contribute side of the bug.
// (The refund side is exercised in F-02.poc.ts, which calls refund() while deadline=0.)
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Rustfund } from "../target/types/rustfund";
import { PublicKey, Keypair, LAMPORTS_PER_SOL, SystemProgram } from "@solana/web3.js";
import { expect } from "chai";
describe("F-04 deadline=0 short-circuits the deadline guard", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.Rustfund as Program<Rustfund>;
const creator = provider.wallet;
const contributor = Keypair.generate();
const fundName = "F-04-deadline-zero-fund";
const description = "F-04 demonstrates deadline=0 bypass";
const goal = new anchor.BN(LAMPORTS_PER_SOL);
let fundPDA: PublicKey;
let contributionPDA: PublicKey;
before(async () => {
const sig = await provider.connection.requestAirdrop(contributor.publicKey, 2 * LAMPORTS_PER_SOL);
await provider.connection.confirmTransaction(sig);
[fundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(fundName), creator.publicKey.toBuffer()],
program.programId
);
[contributionPDA] = await PublicKey.findProgramAddress(
[fundPDA.toBuffer(), contributor.publicKey.toBuffer()],
program.programId
);
});
it("contribute is accepted indefinitely while deadline=0", async () => {
await program.methods
.fundCreate(fundName, description, goal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: SystemProgram.programId,
})
.rpc();
let fund = await program.account.fund.fetch(fundPDA);
expect(fund.deadline.toNumber()).to.equal(0);
// Even though no deadline has been set, contribute() succeeds.
await program.methods
.contribute(new anchor.BN(0.1 * LAMPORTS_PER_SOL))
.accounts({
fund: fundPDA,
contributor: contributor.publicKey,
contribution: contributionPDA,
systemProgram: SystemProgram.programId,
})
.signers([contributor])
.rpc();
fund = await program.account.fund.fetch(fundPDA);
expect(fund.amountRaised.toNumber()).to.equal(0.1 * LAMPORTS_PER_SOL);
// And contribute can be called repeatedly while deadline is still 0 —
// the deadline-reached guard never fires.
await program.methods
.contribute(new anchor.BN(0.2 * LAMPORTS_PER_SOL))
.accounts({
fund: fundPDA,
contributor: contributor.publicKey,
contribution: contributionPDA,
systemProgram: SystemProgram.programId,
})
.signers([contributor])
.rpc();
fund = await program.account.fund.fetch(fundPDA);
expect(fund.amountRaised.toNumber()).to.equal(0.3 * LAMPORTS_PER_SOL);
expect(fund.deadline.toNumber()).to.equal(0); // still 0 — no deadline ever set
});
});

Setup

Same as F-01.

Steps

  1. Copy argus/20260504T004143Z/3-poc/F-04/poc.ts to tests/poc-F-04.ts.

  2. Run anchor test.

Expected output (buggy code)

F-04 deadline=0 short-circuits the deadline guard
✔ contribute is accepted indefinitely while deadline=0

The refund-side branch of F-04 is observable in F-02/poc.ts second it block: refund executes successfully despite fund.deadline === 0.

Expected output (with suggested fix applied)

Replace the deadline check in contribute (programs/rustfund/src/lib.rs:29-31) and refund (lib.rs:69-71) with:

// In contribute:
require!(fund.dealine_set, ErrorCode::DeadlineNotSet);
let now: u64 = Clock::get()?.unix_timestamp.try_into().map_err(|_| ErrorCode::CalculationOverflow)?;
require!(now < fund.deadline, ErrorCode::DeadlineReached);
// In refund:
require!(ctx.accounts.fund.dealine_set, ErrorCode::DeadlineNotSet);
let now: u64 = Clock::get()?.unix_timestamp.try_into().map_err(|_| ErrorCode::CalculationOverflow)?;
require!(now >= ctx.accounts.fund.deadline, ErrorCode::DeadlineNotReached);

(also requires the F-03 fix that flips dealine_set = true).

After the fix, the test FAILS — contribute errors with DeadlineNotSet until the creator has invoked set_deadline first.

Impact

The protocol has no on-chain notion of "deadline finalized." Creator can extend the refund window arbitrarily, blocking refunds (chain with F-02-fixed); shrink it abruptly, blocking in-flight contributions; or oscillate, denying any predictable timeline. Maps to CodeHawks Medium (indirect impact, specific conditions).

Recommendation

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());
}
+ let now: u64 = Clock::get()?.unix_timestamp.try_into().map_err(|_| ErrorCode::CalculationOverflow)?;
+ require!(deadline > now, ErrorCode::DeadlineInPast);
fund.deadline = deadline;
+ fund.dealine_set = true;
Ok(())
}

(Optional: also rename the dealine_set field to deadline_set in a future commit; the typo signals low review density and may itself be worth flagging.)

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 8 days 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!