A native or Pinocchio program has no #[derive(Accounts)] to compare: its account checks are
statements in its code. When --base-src has no accounts struct, solinv diff reads those
statements instead, fn by fn, and names a check a fn made in the base and no longer makes in the
head. Every finding is a warning: it is annotated on the pull request, and the job does not
fail on it — nor when such a program is deleted.
solinv diff --base-src <before>/src --head-src <after>/srcsolinv diff: 0 removed check(s), 3 warning(s)
WARNING init_vault.admin_ai (lib.rs:232): `require_signer(admin_ai)?` is gone or its result is not propagated: `admin_ai` no longer has to sign
WARNING init_vault.vault_ai (lib.rs:232): `if &expected_vault != vault_ai.key() { return Err(..) }` is gone: `vault_ai`'s key is no longer compared with `expected_vault`
WARNING deposit.token_program_ai (lib.rs:316): `if token_program_ai.key() != &TOKEN_2022_PROGRAM_ID { return Err(..) }` is gone: `token_program_ai`'s key is no longer compared with `TOKEN_2022_PROGRAM_ID`
(psyto/princeps-solana's vault, with those three checks deleted by hand.)
A check is a condition that rejects the call — if <cond> { return Err(..) }, or
if <cond> { .. } else { Err(..) } on !cond — read as what it checks about which account:
| Written | Read as |
|---|---|
!a.is_signer(), !a.is_signer |
a signs |
!a.is_writable(), !a.executable() |
a is writable / a program |
a.owner() != id, *a.owner != id, !a.is_owned_by(id), !a.owned_by(id) |
a's owner is id |
a.key() != x, &x != a.key(), a.address() != x, !pubkey_eq(a.key(), x), !cmp_pubkeys(..), sol_memcmp(a.key, x, 32) != 0 |
a's key is x |
- The block may log first:
{ msg!(".."); return Err(..) },sol_log(..),solana_program::msg!. - Each
||part is a check of its own;!(p && q)is!p || !q. A part joined by&&to anything but a flag is not read (!a.is_signer() && !b.is_signer()lets either sign). - Spelling does not matter:
&,*,.as_ref(),.to_bytes(),Pubkey::new_from_array(..),unsafe { .. },likely(..)/unlikely(..), andprogram_id/crate::ID/IDfor the program itself. - A one-expression fn called in a condition is read through:
if !owner_ok(o, a.key)withfn owner_ok(x, y) -> bool { x == y }isa.key != o. - A helper the fn calls and propagates (
?,.unwrap(), returned) counts as the checks it makes, on the arguments passed, to 4 calls deep:require_signer(payer)?ispayersigns. A flag parameter (if expect && !a.is_signer()) counts when the call passestrue. A helper handed an account's key (require_admin(header.admin, a.key)) checks that account. - A helper that stops checking is named once, on itself, when it checks its parameter outright;
when it checks only through a flag or a key it is handed, each call that still runs is named
instead (
`check(user,true)?` still runs, but no longer checks this). - An account is named by its place in the slice when the fn binds it from one
(
let vault = &accounts[1];,let [payer, vault, ..] = accounts), soread(&accounts[4])andlet oracle = &accounts[4]; read(oracle)are the same check. - A
matchwhose arms are enum variants (Instruction::Deposit { .. } => { .. }, or=> process_deposit(accounts)?) is compared arm by arm, each as its own instruction, with the checks the fn makes before thematch.
A check moved into a helper, out of one into every caller, or from one spelling to another is not
reported. Nor is one on a fn or arm whose first statement now returns Err (the instruction was
turned off), or on an account the fn no longer takes at all: not a parameter, its place in the
slice not bound to a name (let _oracle = &accounts[7] does not count) nor indexed, and the slice
not handed on whole. When the same account is still compared, with one new
thing, it says so: `price_ai`'s owner is now compared with `PYTH_RECEIVER_PROGRAM_ID` instead of `PYTH_PROGRAM_ID`.
- Which account a variable holds, other than through its place in the slice. A fn renamed or moved to another module is not compared at all.
- A name it cannot resolve to one fn: another crate's helper, a fn two modules share, a method on a value of unknown type. Its checks are not counted, in either version.
- A fn's checks are one set. Outside instruction
matcharms, a check one branch drops and another branch still makes on the same account is not seen. - Checks of data (a discriminator, a length, a field), of lamports, and anything written as
matchorlet .. elserather thanif. - Nesting: a check inside
if flag { .. }counts as made on every call. - A
matchon a tag byte (match data[0] { 0 => .., 1 => .. }) is not split into arms: the fn's checks are one set, so a check one arm drops and another still makes is not seen. - Given a directory explicitly, the CLI exits 1 when it finds neither an accounts struct, an
entrypoint, nor a check it can read. The Action passes it only directories with an entrypoint
outside comments (its comment stripping can be misled by
/*or//inside a string).
2026-11-06 · every first-parent commit that touches a src directory, compared with its parent,
as in history-scan.md:
solinv diff --base-src <parent>/<dir>/src --head-src <commit>/<dir>/src| Repository | Commits | Reported | Commits flagged | Real loosenings |
|---|---|---|---|---|
aeyakovenko/percolator-prog (to 69174f6, 2026-04-13; since rewritten as v16_program.rs) |
573 | 37 | 13 | 7 reports in 3 commits |
| solana-program/token (SPL Token and p-token) | 475 | 4 | 4 | 1 |
| psyto/princeps-solana | 22 | 1 | 1 | 1 |
| psyto/openhl-solana | 94 | 1 | 1 | 1 |
| Total | 1,164 | 43 | 19 | 10 reports in 6 commits |
About 1 report in 4 is a real loosening, and 6 commits in 19. The real ones:
- percolator-prog 3a3225a: the owner checks of
WithdrawCollateralandTradeNoCpi(if Pubkey::new_from_array(owner) != *a_user.key) commented out; restored later. - percolator-prog bcbb5a6:
TradeCpino longer requires the LP owner to sign ("LP owner should not need to sign"). - percolator-prog c865a22:
ResolveMarketstops propagating the oracle read, so any error from it — including a wrong oracle account — skips the settlement guard (meant for a stale oracle). - token bf5d08f (p-token):
revokechecks the delegate instead of the owner when the delegate signs ("self-revoke delegate"). - princeps-solana e6a097f:
close_repolets the lender close after expiry, so the borrower key check becameis_borrower || (expired && is_lender). - openhl-solana 2fa82eb:
CreateSlabinitializes a slab the client created from a keypair instead of creating it at a PDA of the market, so the slab's key is no longer compared with that PDA (intended: it must now sign, be owned by the program, and be the right size).
The rest: an oracle migration that replaced the oracle key check with a feed id in its data (14 of
the 37, one commit), checks rewritten as Kani-provable struct predicates or a helper comparing a
field of the data instead, a variable renamed (config → cfg, dest_account →
destination_account), a check replaced by a PDA key check, a feature removed with the check
under it, an account left in place but no longer used while the slice is still handed on
(expect_len(accounts, 8)), and an intermediate commit whose call did not match its helper's
parameters.
An independent review then tried to break it with 27 base/head pairs (an authority account used
only to sign, a helper gutted to Ok(()), a trailing return Err, a match arm turned into a
call, a field name equal to an account name, non-ASCII text in a condition); each is a test in
crates/solinv-static/tests/native_cases/. Fixing the misses it found took the reports above
from 35 to 42 (8 real to 9).
Putting the action on openhl-solana then showed that a check whose block logs before it fails
({ msg!(".."); return Err(..) }) was not read at all: 78 of the 192 account checks in its
openhl-core, and deleting if !user_ai.is_signer { msg!(..); return Err(..) } from Deposit was
not reported. v0.1.5 reads them (test n03_logs_before_err); the three histories above report
exactly the same with it. And the action found no native program on GitHub's runners before v0.1.6
(its detector had a {} that GNU find rejects).
Nothing else in the three histories was reported; what it misses there is not measured. Planted
removals: the unit tests in crates/solinv-static/src/native.rs, the three above on
princeps-solana's vault, and the Deposit signer check on openhl-solana's core.