The amount_raised
field in the Fund
account, intended to track the total SOL contributed to a crowdfunding campaign, can become inconsistent with the actual SOL balance of the fund account if SOL is sent directly to the fund’s address outside the FundContribute
function. This is possible because Solana allows native SOL transfers to any account’s public key (a Program Derived Address, or PDA, in this case) via standard transactions, bypassing the contract’s logic. Since amount_raised
is only updated within FundContribute
, direct transfers are not accounted for, leading to discrepancies that affect withdrawal and refund operations. This design flaw could result in inaccurate accounting, unexpected behavior, or loss of funds, making it a notable issue.
The vulnerability arises because the contract assumes all SOL contributions flow through the FundContribute
function, which updates amount_raised
. However, in Solana, any user or program can send SOL directly to the fund’s PDA (derived from [name.as_bytes(), creator.key().as_ref()]
) without invoking the contract, and the contract does not reconcile these external deposits with amount_raised
.
The root cause is the contract’s reliance on amount_raised
as a manually updated counter rather than using the fund account’s actual lamports balance (fund.to_account_info().lamports()
). Here’s the relevant code:
FundContribute
fund.amount_raised
is incremented by the amount
transferred via the contract.
Direct SOL transfers to fund.to_account_info().key()
(the PDA) are not captured, as they don’t trigger this function.
FundWithdraw
Withdrawal uses fund.amount_raised
, not the actual balance, potentially leaving untracked SOL behind or failing if amount_raised
exceeds the balance.
The fund account is a PDA, owned by the program, and its public key is derivable from the seeds. Anyone can send SOL to it using a native transfer (e.g., via solana transfer
CLI or a wallet).
The actual balance (lamports
) increases with direct transfers, but amount_raised
remains unchanged unless FundContribute
is called.
A fund is created with goal = 100 SOL
and amount_raised = 0
.
A user sends 50 SOL directly to the fund’s PDA via a native transfer.
fund.to_account_info().lamports()
reflects ~50 SOL (plus rent exemption), but fund.amount_raised
remains 0.
A contributor uses FundContribute
to send 30 SOL, updating amount_raised
to 30 SOL.
The creator calls FundWithdraw
:
amount = fund.amount_raised = 30 SOL
, withdrawing only 30 SOL.
20 SOL (50 - 30) remains in the account, untracked and inaccessible unless manually withdrawn later.
This discrepancy shows amount_raised
does not reflect the true balance, leading to potential confusion or loss.
The impact of this issue is moderate but depends on usage and intent:
Inaccurate Accounting: amount_raised
underreports the true funds available, misleading contributors and the creator about the campaign’s progress toward the goal
.
Loss of Fund: In FundWithdraw
, only amount_raised
is transferred, leaving any directly deposited SOL in the account. Without a closure mechanism, this SOL is stuck unless manually handled.
The analysis was conducted using:
Manual Code Review
To address this issue, the contract should reconcile amount_raised
with the actual balance or redesign its fund-tracking approach.
FundWithdraw
Modify FundWithdraw
to use the fund’s lamports balance instead of amount_raised
, ensuring all SOL (including direct transfers) is withdrawable:
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.