Ожидаемое поведение: Функция refund позволяет пользователю вернуть свой вклад, физически переводя lamports с PDA-аккаунта фонда обратно пользователю. Функция withdraw позволяет создателю вывести все собранные средства, используя переменную fund.amount_raised как источник правды о сумме доступных к выводу средств.
Конкретная проблема: Функция refund напрямую вычитает lamports из баланса PDA-аккаунта fund, но не декрементирует логическую переменную fund.amount_raised. Это приводит к рассинхронизации (State Desync): переменная fund.amount_raised становится строго больше фактического баланса lamports на PDA-аккаунте. Когда создатель вызывает withdraw, код пытается вычесть завышенное значение amount_raised из реального (меньшего) баланса PDA, что приводит к ошибке ProgramError::InsufficientFunds и permanenty DoS функции вывода.
Likelihood: High
Reason 1: Уязвимость срабатывает при выполнении легитимного сценария использования протокола. Достаточно, чтобы хотя бы один пользователь внес средства и успешно запросил refund (что является нормальным поведением, если дедлайн не был установлен или время пришло). Никакой злонамеренной манипуляции от пользователя не требуется.
Reason 2: Влияние детерминировано. Как только происходит первый refund, переменная fund.amount_raised навсегда расходится с реальным балансом lamports в меньшую сторону. Любая последующая попытка вызова withdraw математически обречена на провал.
Impact: Medium
Impact 1: Permanent DoS (Отказ в обслуживании) функции withdraw. Создатель теряет возможность вывести оставшиеся средства из фонда, даже если на PDA-аккаунте все еще есть lamports от других вкладчиков, не запросивших рефанд.
Impact 2: Зависание средств. Оставшиеся средства вкладчиков на балансе PDA могут оказаться навсегда заблокированными, если баланс PDA фонда опустеет ниже rent-exempt минимума, или если они попытаются вернуть средства одновременно (хотя рефанды могут проходить, пока физически есть lamports, логическая несогласованность делает контракт ненадежным и сломанным).
Сводка:
Файл: programs\rustfund\src\lib.rs (функция refund и функция withdraw)
Likelihood: High
Impact: Medium
# \[H-02] Fund Creator Can't Withdraw If Someone Has Refunded Their Contribution ## Description The `refund` function does not update `fund.amount_raised`, causing an inconsistency between the fund's actual balance and the recorded raised amount. As a result, when the fund creator tries to withdraw funds, the transaction may fail due to insufficient balance, effectively locking funds in the contract. ## Vulnerability Details The issue arises in the `refund` function, which transfers funds back to the contributor but does not update the `amount_raised` field: ```rust pub fn refund(ctx: Context<FundRefund>) -> Result<()> { let amount = ctx.accounts.contribution.amount; if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } 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.contributor.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.contributor.to_account_info().lamports() .checked_add(amount) .ok_or(ErrorCode::CalculationOverflow)?; // Reset contribution amount after refund ctx.accounts.contribution.amount = 0; Ok(()) } ``` The issue becomes evident when the fund creator attempts to withdraw using the following function: ```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(()) } ``` Since `amount_raised` is never updated when a refund occurs, the creator will attempt to withdraw more than what actually exists in the fund, causing an insufficient funds error and failing the transaction. ## Impact - If any contributor requests a refund, the total balance in the fund decreases. However, `fund.amount_raised` remains unchanged, leading to an overestimated available balance. - When the fund creator calls `withdraw`, they attempt to transfer `fund.amount_raised`, which no longer matches the actual available balance. - This results in a failed transaction, effectively locking funds in the contract since the withdraw function will always fail if refunds have been processed. ## Proof of Concept This issue is not currently caught by tests because the `contribute` function itself has a bug (not updating `contribution.amount`), preventing the refund function from executing properly. Once the contribute function is fixed, the issue will be clearly visible in test cases. ## Recommendations The `refund` function must update `fund.amount_raised` to ensure the contract state reflects the actual balance after refunds. ### Fixed Code: ```diff pub fn refund(ctx: Context<FundRefund>) -> Result<()> { let amount = ctx.accounts.contribution.amount; if ctx.accounts.fund.deadline != 0 && ctx.accounts.fund.deadline > Clock::get().unwrap().unix_timestamp.try_into().unwrap() { return Err(ErrorCode::DeadlineNotReached.into()); } 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.contributor.to_account_info().try_borrow_mut_lamports()? = ctx.accounts.contributor.to_account_info().lamports() .checked_add(amount) .ok_or(ErrorCode::CalculationOverflow)?; // Reset contribution amount after refund ctx.accounts.contribution.amount = 0; + // Fix: Decrease the fund's recorded amount_raised + let fund = &mut ctx.accounts.fund; + fund.amount_raised = fund.amount_raised.checked_sub(amount).ok_or(ErrorCode::CalculationOverflow)?; 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.