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:
FundContributefund.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.
FundWithdrawWithdrawal 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.
FundWithdrawModify 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.