Securing Solana Programs: A Battle-Tested Checklist

Shipping a Solana program to mainnet requires a rigorous security posture, especially for developers transitioning from web2. This checklist, compiled from firsthand experience with exploitable bugs, offers a concrete set of checks to perform before every deployment. Each item is designed to be verifiable with a simple yes/no answer by inspecting the code, ensuring that security isn't left to vague notions of "carefulness."

The core principle is that critical security logic should be self-evident in the code. If an item requires more than a straightforward code review of account structures and handler logic, the program needs refactoring. This approach stems directly from reproducing bugs that led to significant losses, including those affecting Wormhole and Cashio, proving that these are not theoretical risks but tangible threats.

Account Validation: The First Line of Defense

Every deserialized account must have its owner verified. For accounts deserialized using Anchor's typed Account<'info, T>, this verification is automatic. However, when working with raw AccountInfo or UncheckedAccount types, explicit owner checks are mandatory. This prevents malicious actors from substituting legitimate accounts with their own, potentially draining funds or manipulating program state.

Consider the structure of your account structs and the associated handler logic. If a custom deserialization or direct account manipulation is involved, ensure a clear, code-level check verifies the program ID or owner address. This is not merely a suggestion; it's a critical safeguard against account hijacking.

Program ID Verification

Just as accounts need owner verification, the program ID itself must be validated. Ensure that the program ID being invoked or interacted with is indeed the expected program. This guards against scenarios where an attacker might trick your program into interacting with a malicious program masquerading as a legitimate one. This check is particularly important in cross-program invocations.

Signer and Writable Checks

Solana's transaction model relies heavily on explicit permissions for accounts. Every account that is expected to be signed (is_signer) or modified (is_writable) must be checked accordingly. A program should never assume an account is a signer if it's not explicitly marked as such in the transaction, nor should it attempt to write to an account that isn't marked as writable.

Failing to enforce these checks can lead to unexpected behavior. For instance, attempting to write to a read-only account will result in a runtime error, but more subtle bugs can arise if the program logic incorrectly assumes a signer's authority. Always verify that the is_signer and is_writable flags align with the program's intended operations.

Rent Exemption Checks

Solana accounts require a minimum balance to cover rent, which is periodically charged by the network. Your program should ensure that any account it creates or to which it transfers ownership of lamports is rent-exempt. This prevents accounts from being closed by the system due to insufficient balance, which could lead to data loss or program failure.

When creating new accounts, ensure they are seeded with enough SOL to be rent-exempt. If your program manages account lifecycles, implement logic to check and maintain the rent exemption status of critical accounts.

Integer Overflow and Underflow Protection

Arithmetic operations on integer types are a common source of vulnerabilities in smart contracts across all blockchains. On Solana, where operations might occur with large numbers or in rapid succession, integer overflows (exceeding the maximum value) and underflows (going below the minimum value) can lead to significant discrepancies in balances or state. This is precisely how the Cashio exploit occurred, where incorrect arithmetic led to a massive drain of funds.

Use checked arithmetic operations provided by Rust's standard library (e.g., checked_add, checked_sub) or libraries specifically designed for safe arithmetic in smart contracts. Proptest or other fuzzing techniques are invaluable for discovering edge cases that might not be apparent during manual code review.

Reentrancy Guards

While Solana's account model and transaction processing significantly reduce the risk of traditional reentrancy attacks seen in Ethereum, certain patterns can still be vulnerable. If your program makes external calls (cross-program invocations) and then modifies state based on the outcome, ensure that the state modifications happen *before* the external call, or implement a reentrancy guard mechanism. This prevents a malicious program from recursively calling back into your program with altered state before the initial invocation has completed.

A simple guard might involve a boolean flag within an account's state. Set the flag to `true` before an external call and reset it to `false` afterward. If the flag is already `true` when the program is re-entered, halt execution. This is less about preventing a direct call back into the same function and more about preventing a malicious actor from exploiting the time between an external call returning and your program's state being fully updated.

Owner Checks for Critical State Transitions

Any state transition that has significant financial or operational implications must be protected by an owner check. This means that only the designated owner (typically the program's creator or a designated admin account) should be able to trigger these critical actions. This is a fundamental security principle that prevents unauthorized modifications to sensitive program parameters or asset transfers.

For example, if your program has a function to withdraw funds, change ownership, or update configuration, always verify that the caller's public key matches the `owner` field in the relevant state account. This is a direct lesson from the Wormhole exploit, where a lack of proper checks allowed unauthorized state changes.

Program Authority and Upgradeability

If your program is upgradeable (using the upgradeable BPF loader), ensure that the authority to upgrade the program is tightly controlled. The `program_data` account's authority should be a multisig or a highly secured key. Accidental or malicious upgrades can introduce vulnerabilities or completely alter program behavior.

For non-upgradeable programs, this is less of a concern, but the initial deployment authority should still be managed with care. Understand the implications of program upgradeability and secure the associated authority keys diligently.

Data Serialization and Deserialization Robustness

Ensure that all data being serialized and deserialized is handled robustly. This includes validating the length of data buffers and checking for expected formats. Malformed data can lead to panics or unexpected behavior, potentially opening up attack vectors. Always use secure deserialization methods and validate input lengths.

Testing and Fuzzing

Manual code review is essential, but it's not sufficient. Comprehensive unit tests, integration tests, and property-based testing (fuzzing) are critical. Fuzzing, in particular, uses automated tools like `proptest` to generate a vast number of random inputs to uncover edge cases and bugs that human testers might miss. The bugs mentioned throughout this checklist were often discovered through such rigorous testing methodologies.

Before deploying to mainnet, ensure your test suite covers all identified vulnerabilities and edge cases. Run fuzzing campaigns to stress-test your program's arithmetic, state transitions, and input handling.

Final Deployment Check

Before hitting the deploy button for mainnet, run through this checklist one final time. Treat each item as a non-negotiable requirement. The cost of a security vulnerability on mainnet far outweighs the time invested in these checks. Shipping secure code is the responsibility of every developer building on Solana.