Full Report
When making a Cross Program Invocation (CPI) in Solana via invoke or invoke_signed, you provide a set of accounts to be used. In raw Solana, you pass in AccountInfo directly, which is a handle to the in-memory runtime state. In Anchor, you pass in Account., which is a deserialized version of T and acts as a cached value. Native Solana programs do not operate on the ledger directly. Instead, accounts are loaded into the runtime as a working set. Instructions mutate this in-memory state. Many things, like lamports, are read directly from the runtime state every time. If you reborrow the data, then the underlying bytes will also be updated. In Anchor, the type T on the AccountInfo is a deserialized snapshot of the account data bytes. At the start of the instruction, Anchor constructs the accounts by deserializing them in a generated handler from the info.data on the account. This means that the data is copied onto the stack/heap as a Rust value and is NOT a live reference to the runtime bytes. At the end of the instruction, Anchor will serialize the data structure and write it back to the runtime. In practice, this has a strange consequence: if a CPI modifies an account, the cached version will have stale data. For instance, for balance on a token account, a token transfer would show the same balance before and after the CPI, regardless of whether the account balance changed. To solve this problem, Anchor accounts have reload(). This will reload the data from storage via re-reading and deserializing the data within AccountInfo.data. The account data is now no longer stale. The author gives some tips on when to call reload(). It's required when A) a CPI can be used to mutate account data, B) the account needs to be read/validated later and C) you are reading a cached struct. If lamports or native runtime fields are being read, then reloading isn't necessary. Overall, a great post on Solana CPI reloading and why it must be done. I had always wondered why lamports didn't need to be reloaded but the data did; now I know!
Analysis Summary
# Best Practices: Anchor Account State Syncing after CPI
## Overview
These practices address the **"Stale Cache" vulnerability** in Solana programs using the Anchor framework. In Anchor, the `Account<T>` type is a deserialized snapshot of account data stored on the stack/heap. When a Cross-Program Invocation (CPI) is made, the underlying blockchain state (the runtime bytes) may change, but the local Rust struct remains unchanged. Failing to sync these can lead to logic errors, failed validations, or unauthorized state transitions based on outdated information.
## Key Recommendations
### Immediate Actions
1. **Identify CPI impact:** Audit all `invoke` or `anchor_spl` calls to identify which accounts are modified by the external program (e.g., Token Program, System Program).
2. **Insert `reload()` calls:** Immediately after a CPI, if you intend to read or validate fields from an affected `Account<T>`, call `account.reload().unwrap()`.
### Short-term Improvements (1-3 months)
1. **Standardize Validation Patterns:** Ensure that any business logic involving balances (e.g., `token_account.amount`) or status flags are only checked *after* a `reload()` if a CPI preceded the check.
2. **Code Review Checklist:** Update internal security review checklists to include a "Stale Account Data" check for every Anchor instruction containing a CPI.
### Long-term Strategy (3+ months)
1. **Architectural Decoupling:** Design instructions so that state-changing CPIs are the final action in a handler, reducing the need for post-CPI validation of cached structs.
2. **Custom Wrapper Exploration:** For complex programs, consider wrapping `Account` access in getters that check for a "dirty" state or automatically handle re-deserialization.
## Implementation Guidance
### For Small Organizations
- Focus on the "Rule of Thumb": If you call a CPI and then use a `.` (dot) operator to access a field (like `vault.amount`), you **must** call `reload()` first.
### For Medium Organizations
- Implement automated linting or static analysis tools that flag uses of `Account<T>` variables following a CPI call within the same scope.
### For Large Enterprises
- Establish a signed "Security Pattern Library." Document the difference between `AccountInfo` (live runtime reference) and `Account<T>` (cached snapshot) to prevent junior developers from assuming `Account<T>` is a live reference.
## Configuration Examples
### Correcting Stale Data after Token Transfer
In this example, an SPL token transfer is made. To check the new balance accurately, `reload()` is required.
rust
// 1. Initial CPI to transfer tokens
anchor_spl::token::transfer(cpi_ctx, amount)?;
// 2. STALE: vault.amount still reflects the balance BEFORE the transfer
// 3. FIX: Reload the account from the runtime AccountInfo
vault.reload().map_err(|_| ErrorCode::ReloadFailed)?;
// 4. VALID: vault.amount now reflects the truth
if vault.amount > threshold {
// secure logic here
}
## Compliance Alignment
- **NIST SP 800-53 (SI-10):** Information Input Validation. Ensuring the data being validated is current and hasn't been tampered with or desynchronized during the execution flow.
- **CIS Software Development Benchmark:** Focuses on minimizing logic errors by ensuring state consistency between the execution environment and application memory.
## Common Pitfalls to Avoid
- **The "Lamport Illusion":** Do not assume that because `lamports` update automatically (they are read directly from `AccountInfo`), the rest of the account data does too. Only native runtime fields are "live."
- **Manual Deserialization:** If you manually deserialize `AccountInfo.data` into a custom struct before a CPI, that struct will also be stale after the CPI.
- **Ignoring Result:** Always handle the result of `.reload()`. While rare, failing to reload successfully should result in an instruction error to prevent processing stale data.
## Resources
- **Anchor Documentation:** [docs.rs/anchor-lang/latest/anchor_lang/accounts/account/struct.Account.html](https://docs.rs/anchor-lang/latest/anchor_lang/accounts/account/struct.Account.html)
- **Solana Programming Model:** [docs.solana.com/developing/programming-model/accounts](https://docs.solana.com/developing/programming-model/accounts)
- **TaiChi Audit Blog (Source):** [taichiaudit[.]com/blog](https://taichiaudit.com/blog)