DEV Community

Cover image for The Solana Security Checklist I Built After Breaking Solana Programs
Elizabeth Afolabi
Elizabeth Afolabi

Posted on

The Solana Security Checklist I Built After Breaking Solana Programs

During Arc 12 of the #100DaysOfSolana. I stopped trying to make my programs work. I started trying to break them. I wrote adversarial tests, generated random inputs with fuzz testing, reproduced real-world exploits like Wormhole and Cashio, and intentionally passed the wrong accounts into my instructions.

The biggest lesson wasn't how exploits happen. It was how surprisingly small the missing validation can be. By the end of the arc, I realized I wasn't just learning security techniques; I was building a checklist I'll probably use every time I write an Anchor program.

This checklist is for anyone building Solana programs with Anchor. It's not meant to replace an audit, but it is a practical list of questions I'll run through before every mainnet deployment to catch the kinds of mistakes that are easy to overlook.

Earlier in the challenge, I assumed that if my instruction received an account and deserialized it successfully, everything was probably fine. This arc completely changed that.

On Solana, any account can be passed into an instruction.

Deserializing an account only tells you how to read its bytes. It doesn't tell you:

  • who owns it
  • whether it's the account you expected
  • whether it should be allowed here

That's why so much of Solana security is simply validating assumptions before your business logic ever runs.

My Solana Program Security Checklist

 

1. Account Validation

The biggest lesson from reproducing the Wormhole and Cashio exploits was how small the root cause actually was. In both cases, the vulnerable program trusted an account before verifying that it was owned by the expected program. Because of the lack of validation, an attacker was able to provide a counterfeit account that appeared genuine enough to deserialize but was not the account intended by the program itself. In Wormhole, that error resulted in a loss of around $326 million. Cashio experienced a similar problem, leading to approximately $52 million being emptied.

Rebuilding the vulnerable pattern locally made this much more tangible than simply reading the post-mortems. It reinforced a simple rule I'll carry into every program I write: never trust an account just because it was passed into your instruction. Validate it first.

Checklist

  • Use typed Account<'info, T> whenever possible.
  • Only use UncheckedAccount when absolutely necessary.
  • If using UncheckedAccount, verify its owner manually.
  • Validate any remaining_accounts before using them.

2. Authority and signer checks

One of Anchor's biggest strengths is that it turns many security checks into declarative constraints.

Typed accounts, Signer, has_one, seeds, and bump allow you to express who can access an instruction and which accounts are valid directly in the account context. Anchor then verifies those constraints before your instruction handler runs, reducing the amount of manual validation you need to write yourself.

That doesn't mean authorization becomes automatic. Anchor can enforce the constraints you declare, but it's still your responsibility to declare the right ones.

Checklist

  • Every privileged instruction requires the expected signer.
  • Authority relationships are enforced with has_one or explicit constraints.
  • PDA accounts use the correct seeds and bump constraints.
  • Never trust a public key alone, verify that the required signer is actually present.

3. Think like an attacker

Instead of asking "Does this instruction work?", I started asking: How would I try to break it?. That single change produced much better tests.

Checklist

  • Test wrong signers.
  • Test substituted accounts.
  • Test over-withdrawals.
  • Assert the exact Anchor/runtime error, not just that the transaction failed.

4. Fuzz what humans wouldn't think of

At first, I thought testing meant coming up with the right test cases myself. But there's a limit to how many cases one person can realistically think of. There are simply too many possible inputs to write tests for manually. Instead of choosing every input myself, I let the computer generate thousands of different combinations and try them against the program. Fuzzing explores that space much faster and can uncover edge cases I'd probably never think to test on my own.

Checklist

  • Fuzz balance changes.
  • Fuzz arithmetic.
  • Fuzz edge-case values.
  • Keep the fuzz tests after the bug is fixed.

5. Let Anchor help - but know its limits

Anchor protects me from an entire class of mistakes by enforcing owner validation, account discriminators, signer constraints, PDA seeds and bumps, and reinitialization checks. But it can't decide:

  • whether my arithmetic is correct
  • whether my business logic is safe
  • whether a CPI should happen or
  • whether an unchecked account should be trusted

Anchor is a powerful safety net, but it only enforces the constraints you define. Writing secure programs still means understanding what Anchor validates for you and what it doesn't.
 

Closing Remarks

I think about security a little differently now. It's less about writing clever defenses and more about consistently validating assumptions before your program trusts anything.

This checklist is one I'll keep coming back to before every deployment. I'm sure it isn't complete, and I expect it to grow as I learn more and build more complex programs.

If you spot something I've missed or have a security check that's become part of your own workflow, I'd love to hear it. Security checklists are living documents, and they're better when they're improved by the community.

Top comments (0)