Full Report
Solana is a proof of stake network. So, the more value you provide in Solana, the more power you have in the voting process. With 2/3 of the control, changes to the state can be made. Clearly, ensuring that the staking and voting power is done properly is important. To stake funds, a user 1) creates an account 2) delegates the account) and 3) becomes activated. However, parsing all of the staked chain state every block would be incredibly inefficient. So, instead, a cache or running total is kept instead. If something relevant to the cache has changed then it makes an update to the cache. Solana allows active stake accounts to be merged. This will close one account and add the stakes to the other account without cooldown. When doing this, it does the detection by checking if the closed account has zero funds in it. Normally, this is the case, since the staking program address will do this. However, there is a logic bug here - it's possible to add funds to the old staking account so that it's not properly reaped. If this is done, then the key isn't removed from the cache! So, we can reuse the same staked values in multiple accounts by exploiting this logic flaw. To exploit, here are the steps: Create two staking accounts. Consolidate one account into the other. Add one lamport into the closed account. Solana core doesn't update the cache for the closed program because it has value. Recreate the vote account. The delegation is still there and the cache still doesn't get updated properly. To fix the vulnerability, the account is attempted to be deserialized instead of a zero funds check. Overall, a super interesting post on the desync between reality and the understanding of reality.
Analysis Summary
# Vulnerability: Staking Cache Desynchronization via Stake Account Merging
## CVE Details
- CVE ID: Not explicitly provided in the text (Assigned Post-Discovery)
- CVSS Score: Not explicitly provided in the text
- CWE: CWE-824: Improper Access of Index or History (Related to flawed state tracking)
## Affected Systems
- Products: Solana Core
- Versions: Versions prior to the commit referenced in the fix (pre-7b365c564f2c168e6e900e98ab49070a0ca16f6e).
- Configurations: Systems utilizing the stake account merge functionality where the closed account's lamports are subsequently refilled.
## Vulnerability Description
The Solana staking system relies on a cache/running total for efficient tracking of stake and voting power (delegations). When an active stake account is merged, one account is closed, and its stake is added to another without a cooldown/warm-up period. The system traditionally determined if a closed account should be removed from the cache by checking if its `lamports` were zero.
The vulnerability lies in the assumption that once the merge instruction completes, the closed account will remain empty. An attacker can execute a subsequent instruction to deposit lamports back into the seemingly "closed" account. Because the lamport check fails (`account.lamports() != 0`), the stake delegation associated with the closed account key is *not* removed from the internal cache (`self.stake_delegations`). This results in a **desynchronization** where the lamports from the merged stake are counted in the destination account, but the original stake entry remains active in the cache for the now-repopulated, merged-from account. This effectively allows the same staked value to be counted towards the voting power of the validator through multiple cache entries.
## Exploitation
- Status: Proof-of-Concept available (Described attack steps outlined below)
- Complexity: Medium (Requires specific timing and sequencing of transactions using the merge operation followed by re-funding the closed account)
- Attack Vector: Network (Remote transaction submission)
**Exploit Summary:**
1. Create two stake accounts.
2. Consolidate one account into the other (triggering the cache update for the recipient, but the removal check for the source fails if the source account is immediately refilled).
3. Add a minimal amount (e.g., one lamport) back into the source account that was supposed to be closed.
4. The Solana core cache is not updated, retaining the old delegation entry.
5. Recreate the vote account delegation using the source key; the cache remains incorrectly updated, allowing the stake amount to be counted multiple times toward the validator's effective stake.
6. The exploit can be repeatedly optimized to achieve $O(n^2)$ total effective stake given enough time and initial lamports.
## Impact
- Confidentiality: No direct impact.
- Integrity: **High**. Allows an attacker to artificially inflate the delegated stake associated with a stake account, thereby gaining disproportionate voting power and control over network consensus decisions (potentially exceeding the 2/3 threshold required for full control).
- Availability: **High**. Malicious control over consensus could lead to severe disruption or halting of the network.
## Remediation
### Patches
- The vulnerability was fixed in Solana Core commit: `7b365c564f2c168e6e900e98ab49070a0ca16f6e`.
- The fix replaces the simple `account.lamports() == 0` check with a check to see if the account's delegation state can be successfully deserialized (i.e., `delegation.is_none()`).
### Workarounds
(No specific user-side workarounds were provided, as this relies on a core state management logic flaw. Upgrading is the definitive solution.)
## Detection
- Indicators of Compromise: Validators showing an effective stake significantly higher than the actual SOL deposited in their constituent stake accounts, particularly after recent stake merges or account closures.
- Detection methods and tools: Monitoring the delta between the sum of lamports locked in reported stake accounts and the total stake reflected in the network's internal voting power cache/state.
## References
- Vendor Advisories: Disclosure was made via the Solana bug bounty program on 09/14/2021.
- Relevant links - defanged:
- Article: hxxps://neodyme.io/en/blog/solana_core_2
- Fix Commit: hxxps://github.com/solana-labs/solana/commit/7b365c564f2c168e6e900e98ab49070a0ca16f6e