RustFund

First Flight #36
Beginner FriendlyRust
100 EXP
View results
Submission Details
Severity: high
Valid

Multiple Issues in Fund Initialization and Validation

Summary

The fund_create function contains multiple security flaws, including improper validation, incorrect field handling, and missing constraints. These issues can lead to incorrect fund setups, security vulnerabilities, and broken logic that affects contributor trust.

Vulnerabilities' Details

  1. Deadline Can Be Set After Initializing a Fund Campaign:

    • Allows campaign creators to modify the deadline post-creation, leading to potential abuse.

  2. Missing Validation for goal:

    • Users can create campaigns with a zero goal, making fund utilization ambiguous.

  3. Unchecked Input Length for name and description:

    • Lacks constraints on empty or excessively long input values, leading to runtime errors.

  4. PDA Seed Constraint Violation:

    • The fund name (up to 200 bytes) combined with the creator’s address in the PDA seed violates Solana’s 32-byte per-seed limit, causing errors.

Impact

  • Fund Manipulation – Creators can modify deadlines, extend fundraising indefinitely, or withdraw funds without meeting goals.

  • Financial Risk – Contributors may never receive refunds if deadlines are changed.

  • Runtime Errors – Unvalidated inputs increase the likelihood of unexpected crashes, affecting UX and gas costs.

  • Incomplete Fund Details – Allows empty names and descriptions, reducing transparency in crowdfunding.

Tools Used

Manual Code Review

Anchor Test Cases

PoC

describe("Fund Create Function Tests", () => {
it("1- If GOAL IS NOT SET, WILL NOT REVERT!", async () => {
const invalidGoal = new anchor.BN(0);
try {
[fundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(fundName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(fundName, description, invalidGoal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const fund = await program.account.fund.fetch(fundPDA);
console.log("\n✅ Fund Created with Zero Goal:");
console.log(` - Fund Name: ${fund.name}`);
console.log(` - Fund Goal: ${fund.goal}`);
console.log(" - Expected Behavior: Fund creation did not revert with zero goal.");
} catch (err) {
console.error("\n❌ Unexpected Error: Fund creation reverted with zero goal.");
throw err;
}
});
it("2- Empty NAME WILL NOT REVERT", async () => {
const emptyName = "";
try {
[fundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(emptyName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(emptyName, description, goal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const fund = await program.account.fund.fetch(fundPDA);
console.log("\n✅ Fund Created with Empty Name:");
console.log(` - Fund Name: '${fund.name}'`);
console.log(" - Expected Behavior: Fund creation did not revert with empty name.");
} catch (err) {
console.error("\n❌ Unexpected Error: Fund creation reverted with empty name.");
throw err;
}
});
it("3- Empty DESCRIPTION WILL NOT REVERT", async () => {
const emptyDescription = "";
try {
[fundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(fundName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(fundName, emptyDescription, goal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
const fund = await program.account.fund.fetch(fundPDA);
console.log("\n✅ Fund Created with Empty Description:");
console.log(` - Fund Description: '${fund.description}'`);
console.log(" - Expected Behavior: Fund creation did not revert with empty description.");
} catch (err) {
console.error("\n❌ Unexpected Error: Fund creation reverted with empty description.");
throw err;
}
});
it("4- DESCRIPTION > 5000 WILL REVERT BECAUSE OF RANGE ERROR", async () => {
const longDescription = "a".repeat(5001); // Exceeds max length of 5000
try {
[fundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(fundName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(fundName, longDescription, goal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.error("\n❌ Unexpected Behavior: Fund creation succeeded with a description longer than 5000 characters.");
} catch (err) {
console.log("\n✅ Fund Creation Reverted as Expected:");
console.log(" - Reason: Description exceeds maximum allowed length (5000 characters).");
expect(err.toString()).to.include("encoding overruns Buffer");
}
});
it("5- NAME + CREATOR ADDRESS = SEED > 32 BYTES WILL REVERT WITH EXCEED MAX SEED", async () => {
const longName = "a".repeat(33); // Exceeds max seed length of 32 bytes
try {
[fundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(longName), creator.publicKey.toBuffer()],
program.programId
);
await program.methods
.fundCreate(longName, description, goal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.error("\n❌ Unexpected Behavior: Fund creation succeeded with a name exceeding 32 bytes.");
} catch (err) {
console.log("\n✅ Fund Creation Reverted as Expected:");
console.log(" - Reason: Name + creator address exceeds maximum seed length (32 bytes).");
expect(err.toString()).to.include("Max seed length exceeded");
}
});
it("6- Duplicate FundCrowd WILL REVERT (Same Name and Creator Address) PDA UNIQUENESS", async () => {
try {
[fundPDA] = await PublicKey.findProgramAddress(
[Buffer.from(fundName), creator.publicKey.toBuffer()],
program.programId
);
// First fund creation
await program.methods
.fundCreate(fundName, description, goal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("\n✅ First Fund Created Successfully.");
// Attempt duplicate fund creation
await program.methods
.fundCreate(fundName, description, goal)
.accounts({
fund: fundPDA,
creator: creator.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.error("\n❌ Unexpected Behavior: Duplicate fund creation succeeded.");
} catch (err) {
console.log("\n✅ Duplicate Fund Creation Reverted as Expected:");
console.log(" - Reason: Duplicate fund creation is prevented by PDA uniqueness.");
}
});
});

Test Validations Are:

rustfund
Fund Create Function Tests
✅ Fund Created with Zero Goal:
- Fund Name: firstflight FundA
- Fund Goal: 0
- Expected Behavior: Fund creation did not revert with zero goal.
✔ 1- If GOAL IS NOT SET, WILL NOT REVERT! (422ms)
✅ Fund Created with Empty Name:
- Fund Name: ''
- Expected Behavior: Fund creation did not revert with empty name.
✔ 2- Empty NAME WILL NOT REVERT (335ms)
✅ Fund Created with Empty Description:
- Fund Description: ''
- Expected Behavior: Fund creation did not revert with empty description.
✔ 3- Empty DESCRIPTION WILL NOT REVERT (372ms)
✅ Fund Creation Reverted as Expected:
- Reason: Description exceeds maximum allowed length (5000 characters).
✔ 4- DESCRIPTION > 5000 WILL REVERT BECAUSE OF RANGE ERROR
✅ Fund Creation Reverted as Expected:
- Reason: Name + creator address exceeds maximum seed length (32 bytes).
✔ 5- NAME + CREATOR ADDRESS = SEED > 32 BYTES WILL REVERT WITH EXCEED MAX SEED
✅ Duplicate Fund Creation Reverted as Expected:
- Reason: Duplicate fund creation is prevented by PDA uniqueness.
✔ 6- Duplicate FundCrowd WILL REVERT (Same Name and Creator Address) PDA UNIQUENESS

Recommendations (Final Comprehensive Version)

pub fn fund_create(
ctx: Context<FundCreate>,
name: String,
description: String,
goal: u64,
deadline: u64
) -> Result<()> {
let fund = &mut ctx.accounts.fund;
// Goal validation: Ensure goal is not zero
if goal == 0 {
return Err(ErrorCode::InvalidEmptyGoal.into());
}
// Deadline validation: Ensure deadline is in the future
if deadline <= Clock::get()?.unix_timestamp as u64 {
return Err(ErrorCode::InvalidDeadline.into());
}
// Name validation: Ensure name is not empty and fits PDA seed constraints (≤ 32 bytes)
if name.is_empty() {
return Err(ErrorCode::EmptyName.into());
}
if name.len() > 32 {
return Err(ErrorCode::NameTooLong.into());
}
// Description validation: Ensure description is not empty and does not exceed max length (5000)
if description.is_empty() {
return Err(ErrorCode::EmptyDescription.into());
}
if description.len() > 5000 {
return Err(ErrorCode::DescriptionTooLong.into());
}
// Initialize fund
fund.name = name;
fund.description = description;
fund.goal = goal;
fund.deadline = deadline;
fund.creator = ctx.accounts.creator.key();
fund.amount_raised = 0;
fund.deadline_set = true; // Ensuring deadline is locked
Ok(())
}
Updates

Appeal created

bube Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

[Invalid] Lack of validation of the `deadline` parameter in `set_deadline` function

The creator has an incentive to pay attention to the deadline and provide correct data. If the `deadline` is set in the past, the campaign will be completed. If there are any funds the creator or the contributors (depending on the success of the campaign) can receive them. It is the creator's responsibility to set correct deadline, otherwise the creator can create a new campaign. There is no impact on the protocol from this missing check, so I consider this to be an informational issue.

No minimal amount for the `goal` in `fund_create` is greater than 0

If the `goal` is 0, the campaign goal is achieved immediately and the creator can withdraw the contributors funds. The contributors select themself which campaign to support, therefore I think Low severity is appropriate here.

[Invalid] Lack of length validation of `name` and `description` in `fund_create` function

There is a validation for the lengths of `name` and `description` in `fund_create` function: ``` pub struct Fund { #[max_len(200)] pub name: String, #[max_len(5000)] ..... } ``` Anchor will check for the lengths of these parameters and the function will fail if they have more characters than the constraints.

Support

FAQs

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