On Solana the caller writes your argument list
An EVM contract reads its own storage; a Solana program is handed every account it touches by the caller, so the account list is untrusted input. Count what you accept unchecked.
The hardest habit to unlearn when moving from Solidity to Solana is trusting your own storage. An EVM contract owns its state: a mapping lives at an address the contract controls, and reading it is reading something only the contract could have written. A Solana program owns nothing at run time. Every account it touches, its own state, the user's token account, the mint, the program it will call, arrives in a list that the transaction's author assembled and passed in. The program is handed its argument list by the caller, and the caller may be hostile. Most Solana exploits are not logic bugs in the Solidity sense. They are accounts accepted without proving who owns them, who signed for them and what type they are, and the defence is a number a build pipeline can enforce rather than a longer checklist.
What the audits find
The clearest evidence is in the aggregate audit data. Sec3's Solana Security Ecosystem Review 2025, an empirical analysis of 1,669 findings across 163 multi-auditor reviews, reports that three categories account for 85.5 percent of high and critical findings: business logic errors at 36.9 percent, input validation failures at 27.9 percent and access control weaknesses at 20.7 percent. The second and third categories are, on Solana, overwhelmingly the same mechanism: an account in the instruction's list that the program used without checking its owner, its signer status or its type. Nearly half of the severe findings are that mechanism wearing two labels.
The incident history says the same thing with larger numbers. The Wormhole bridge lost 326 million dollars in February 2022 to a signature verification flaw, an instruction that accepted an account standing in for a system program without checking that it was the real one; Cashio lost 52.8 million a month later to a missing check on the accounts in its mint instruction. Both are input validation in the DefiLlama hacks dataset, both are the caller writing an argument list the program believed, and both would have been caught by a check that fits on one line.
The three gates
Every account in an instruction has to pass three gates before the logic runs. The owner gate: which program owns this account, and is it the one expected? A token account owned by anything other than the token program is a forgery. The signer gate: did the private key for this account sign the transaction? An instruction that moves funds out of an account the caller merely listed, rather than signed for, moves anyone's funds. The type gate: does the account's data begin with the discriminator for the type the program expects? An account of the wrong type, owned by the right program, is how one struct is read as another.
Anchor exists to make the three gates the default. Declaring an account as a typed Account of a given struct checks owner and discriminator; the Signer type checks the signature; constraints on the struct check relationships between accounts, that this token account's mint is that mint, that this authority matches the one stored in state. The escape hatch is UncheckedAccount, and raw AccountInfo, which accept whatever the caller supplied and leave every check to the programmer. They exist for good reasons: an account that only needs its address read, a program the instruction will invoke, an account the logic will validate in a way the attributes cannot express. And every one of them is a place where the argument list is trusted.
The unchecked ratio
So the number is this: across a program, the count of accounts declared as UncheckedAccount or raw AccountInfo, divided by the count of all accounts across all instructions. It is a ratio between zero and one, it is computable with a grep over the account structs, and in my reading of public audit reports it tracks the findings better than any property of the logic does, because it is a direct count of the places where the three gates were skipped.
The ratio belongs in continuous integration with a ceiling and a written justification for every unchecked account. Anchor already requires a doc comment on each UncheckedAccount explaining why it is safe, and the build fails without one; the ratio turns that from a per-field formality into a program-level budget. A pull request that adds an unchecked account raises the ratio, the check reports the new value against the ceiling, and the reviewer reads the justification with the number in front of them. A program whose ratio climbs release by release is one whose surface for the two largest severe-finding categories is growing, whatever its test suite says.
Computing it
The count is a walk over the account structs, which in an Anchor program are the structs annotated as the accounts of an instruction. Each field is one account. A field typed as a checked wrapper, an Account of a named state type, a Signer, a Program, a typed token account, counts as checked; a field typed UncheckedAccount or AccountInfo counts as unchecked. The ratio is the second count over the total, program-wide, and a per-instruction breakdown is worth keeping beside it, because an instruction with three unchecked accounts out of four is a different risk from three unchecked accounts spread across a hundred instructions.
Two refinements keep the number honest. An unchecked account that carries an explicit address or owner constraint in its attributes is still counted as unchecked, because the point is that the type system did not do the work and a reviewer has to; the constraint is the justification, not the check. And the doc comment Anchor requires on each unchecked field is captured alongside the count, so that the CI output is a list: instruction, field, justification, and the ratio at the top. A justification that reads "safe because we only read the key" is fine. One that reads "checked in the handler" sends the reviewer to the handler, which is where the audit findings live.
The ceiling is set per program and lowered over time. A new program starts wherever it starts, the number is recorded, and every release either holds it or explains why it rose. That is the whole discipline, and it costs a script.
The Solidity habit, named
It is worth saying what the EVM habit is, because the ratio is a cure for a specific way of thinking. In Solidity I wrote a flash-loan receiver whose state was its own: the contract read its balances and its flags from storage that no caller could substitute, and the security work was about ordering, what got written before control left. On Solana the same receiver would be handed its state account by the caller, and the first question is not what order to write things in but whether the account being written is the one the program created. The EVM developer's instinct is to reach for the logic; the Solana developer's first job is to establish that the inputs are what they claim to be, and only then to reach for the logic.
That difference is why checklists underperform here. A checklist is a list of things to remember, and the account model produces one of them per account per instruction, which for a program of forty instructions is hundreds of items that all look alike. A ratio is a single number that goes up when any of them is skipped. It does not replace the audit, and it does not catch the business-logic third of the findings. It catches the half that is the caller writing your argument list, and it catches it before the reviewer opens the file.
Get new posts by email
Occasional essays on engineering, AI, and building for the people technology leaves behind.
Subscribe with RSS