Rust Fund

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

Root + Impact: [OP-001] Отсутствие проверки дедлайна при выводе средств (Creator Rugpull)

Root + Impact: [OP-001] Отсутствие проверки дедлайна при выводе средств (Creator Rugpull)

Description

  • Ожидаемое поведение: Протокол краудфандинга должен позволять создателю фонда вывести собранные средства (amount_raised) на свой кошелек. При этом подразумевается, что средства заблокированы до наступления дедлайна (fund.deadline), чтобы пользователи имели временное окно для возврата своих взносов (refund), если проект не состоялся.

  • Конкретная проблема: В функции withdraw полностью отсутствует проверка временного ограничения (fund.deadline). Создатель может вызвать функцию вывода средств в любой момент времени, даже до наступления дедлайна. Это позволяет создателю мгновенно опустошить PDA-аккаунт фонда, что приведет к тому, что вкладчики физически не смогут вызвать refund (транзакция будет отклонена из-за нехватки lamports на контракте).

// programs/rustfund/src/lib.rs
pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let amount = ctx.accounts.fund.amount_raised;
// @> ОТСУТСТВУЕТ ПРОВЕРКА: if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get()?.unix_timestamp ...
// @> Создатель выводит средства без ожидания дедлайна
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.fund.to_account_info().lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;
**ctx.accounts.creator.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.creator.to_account_info().lamports()
.checked_add(amount)
.ok_or(ErrorCode::CalculationOverflow)?;
Ok(())
}
#[derive(Accounts)]
pub struct FundWithdraw<'info> {
// @> Отсутствует проверка временных ограничений, есть только проверка создателя
#[account(mut, seeds = [fund.name.as_bytes(), creator.key().as_ref()], bump,has_one = creator)]
pub fund: Account<'info, Fund>,
#[account(mut)]
pub creator: Signer<'info>,
pub system_program: Program<'info, System>,
}

Risk

Likelihood: High

  • Reason 1: Создатель фонда имеет прямой контроль над вызовом функции withdraw и экономически мотивирован вывести средства как можно скорее, чтобы избежать возврата средств пользователям в случае отмены проекта.

  • Reason 2: Для эксплуатации не требуется выполнения сложных условий или взлома криптографии. Вектор активируется одной транзакцией от создателя в любой момент после того, как на балансе PDA фонда скопятся lamports от вкладчиков.

Impact: High

  • Impact 1: Полная и безвозвратная потеря средств всеми вкладчиками. После вывода средств создателем, баланс PDA фонда (fund.to_account_info().lamports) становится равен нулю (или опускается ниже rent-exempt минимума), из-за чего операция checked_sub(amount) в функции refund всегда будет возвращать ошибку ProgramError::InsufficientFunds.

  • Impact 2: Нарушается базовая логика и доверие к протоколу краудфандинга. Протокол перестает выполнять свою основную функцию — удержание средств до дедлайна (escrow), превращаясь в прямую передачу средств создателю без гарантий возврата.

Proof of Concept

// 1. Создатель создает фонд с целью 1 SOL (1_000_000_000 lamports)
// 2. Пользователь вносит 0.5 SOL (500_000_000 lamports) через `contribute`.
// Баланс PDA фонда (lamports) = 500_000_000.
// fund.amount_raised = 500_000_000.
// 3. Создатель вызывает `withdraw` до наступления дедлайна.
// - amount = fund.amount_raised (500_000_000)
// - PDA фонда баланс становится 0.
// - Создатель получает +0.5 SOL.
// 4. Пользователь пытается сделать `refund`.
// - amount = contribution.amount (500_000_000)
// - В коде refund: `ctx.accounts.fund.to_account_info().lamports()` = 0.
// - 0 - 500_000_000 -> функция checked_sub возвращает None.
// - Транзакция отклоняется с ProgramError::InsufficientFunds. Средства потеряны.

Recommended Mitigation

pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> {
let fund = &ctx.accounts.fund;
+ // Проверяем, что дедлайн установлен и уже прошел
+ if fund.deadline == 0 || fund.deadline > Clock::get()?.unix_timestamp.try_into().unwrap() {
+ return Err(ErrorCode::DeadlineNotReached.into());
+ }
+
let amount = ctx.accounts.fund.amount_raised;
**ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? =
ctx.accounts.fund.to_account_info().lamports()
.checked_sub(amount)
.ok_or(ProgramError::InsufficientFunds)?;

Сводка:

  • Файл: programs\rustfund\src\lib.rs (функция withdraw и структура FundWithdraw)

  • Likelihood: High

  • Impact: High

Updates

Lead Judging Commences

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

[H-01] No check for if campaign reached deadline before withdraw

## Description A Malicious creator can withdraw funds before the campaign's deadline. ## Vulnerability Details There is no check in withdraw if the campaign ended before the creator can withdraw funds. ```Rust pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> { let amount = ctx.accounts.fund.amount_raised; **ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.fund.to_account_info().lamports() .checked_sub(amount) .ok_or(ProgramError::InsufficientFunds)?; **ctx.accounts.creator.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.creator.to_account_info().lamports() .checked_add(amount) .ok_or(ErrorCode::CalculationOverflow)?; Ok(()) } ``` ## Impact A Malicious creator can withdraw all the campaign funds before deadline which is against the intended logic of the program. ## Recommendations Add check for if campaign as reached deadline before a creator can withdraw ```Rust pub fn withdraw(ctx: Context<FundWithdraw>) -> Result<()> { //add this if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } //stops here let amount = ctx.accounts.fund.amount_raised; **ctx.accounts.fund.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.fund.to_account_info().lamports() .checked_sub(amount) .ok_or(ProgramError::InsufficientFunds)?; **ctx.accounts.creator.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.creator.to_account_info().lamports() .checked_add(amount) .ok_or(ErrorCode::CalculationOverflow)?; Ok(()) } ``` ## POC keep everything in `./tests/rustfund.rs` up on to `Contribute to fund` test, then add the below: ```TypeScript it("Creator withdraws funds when deadline is not reached", async () => { const creatorBalanceBefore = await provider.connection.getBalance(creator.publicKey); const fund = await program.account.fund.fetch(fundPDA); await new Promise(resolve => setTimeout(resolve, 150)); //default 15000 console.log("goal", fund.goal.toNumber()); console.log("fundBalance", await provider.connection.getBalance(fundPDA)); console.log("creatorBalanceBefore", await provider.connection.getBalance(creator.publicKey)); await program.methods .withdraw() .accounts({ fund: fundPDA, creator: creator.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }) .rpc(); const creatorBalanceAfter = await provider.connection.getBalance(creator.publicKey); console.log("creatorBalanceAfter", creatorBalanceAfter); console.log("fundBalanceAfter", await provider.connection.getBalance(fundPDA)); }); ``` this outputs: ```Python goal 1000000000 fundBalance 537590960 creatorBalanceBefore 499999999460946370 creatorBalanceAfter 499999999960941400 fundBalanceAfter 37590960 ✔ Creator withdraws funds when deadline is not reached (398ms) ``` We can notice that the creator withdraws funds from the campaign before the deadline.

Support

FAQs

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

Give us feedback!