Thanks for sentio — ran 0.2.9 across the programs of an open-source Rust
privacy framework on Solana (Groth16 shielded pool + Token-2022 confidential
transfers, live on mainnet). It's a genuinely useful pre-audit pass. Sharing
three false-positive patterns from a ZK/privacy codebase that might help tuning,
plus one real finding it correctly caught.
First, the true positive, since credit is due: SW005 flagged a raw += / -=
on try_borrow_mut_lamports() in a withdraw path. That one was legitimate — we
switched it to checked_sub/checked_add. So the tool paid for itself. The rest
below are just tuning signal.
FP 1 — SW024 (division by zero) doesn't resolve compile-time consts
pub const ROOT_RING_SIZE: usize = 30;
// ...
pool.root_ring_head = (pool.root_ring_head + 1) % ROOT_RING_SIZE as u32;
SW024 flags % ROOT_RING_SIZE because the divisor is a non-literal, but it's a
module-level const that can never be zero. Resolving const values in scope
before flagging a non-literal divisor would remove this class of FP.
FP 2 — SW002 (missing owner check) on accounts secured by a ZK proof or used only as a seed
In privacy/ZK programs, some accounts are intentionally UncheckedAccount with
a documented /// CHECK: rationale, because their integrity comes from
somewhere the AST can't see:
/// CHECK: the recipient's pubkey is reduced mod the BN254 scalar field and
/// bound to the Groth16 proof as a public input, so a front-runner who swaps
/// this field invalidates the proof.
#[account(mut)]
pub recipient: UncheckedAccount<'info>,
Here an owner check would be meaningless — recipient is a payout target, and
front-run protection is the proof binding (Tornado-style). Same for a relayer
payout account, and for an account used only as a PDA seed (to_owner whose
value is validated by the derived PDA's bump).
Two ideas: (a) lower confidence / severity when an UncheckedAccount carries a
documented /// CHECK: comment, and (b) don't flag accounts that are only ever
used as a payout target or as a PDA seed input (never read as data). These are
the dominant SW002 FPs in ZK code.
FP 3 — SW027 (missing event) doesn't recognize msg! program logs
pub fn init_vault(ctx: Context<InitVault>) -> Result<()> {
ctx.accounts.vault.bump = ctx.bumps.vault;
msg!("conf-vault-init:{}", ctx.accounts.vault.key()); // <-- observability
Ok(())
}
SW027 looks for Anchor emit! events, but a large share of Solana programs
(including much of SPL) expose state changes through structured msg! program
logs that off-chain indexers parse directly — no Anchor event involved. Treating
a msg! in the same handler as an observability sink would clear this FP.
Context
The codebase is public if a concrete repro helps:
https://github.com/koshak01/tidex6 — the findings above are in
programs/tidex6-verifier, programs/tidex6-wusdc-pool, and
programs/tidex6-confidential-amounts. Happy to provide anything useful.
Take or leave any of this — just wanted to give back after the tool caught a
real one for us.
Thanks for sentio — ran 0.2.9 across the programs of an open-source Rust
privacy framework on Solana (Groth16 shielded pool + Token-2022 confidential
transfers, live on mainnet). It's a genuinely useful pre-audit pass. Sharing
three false-positive patterns from a ZK/privacy codebase that might help tuning,
plus one real finding it correctly caught.
First, the true positive, since credit is due: SW005 flagged a raw
+=/-=on
try_borrow_mut_lamports()in a withdraw path. That one was legitimate — weswitched it to checked_sub/checked_add. So the tool paid for itself. The rest
below are just tuning signal.
FP 1 — SW024 (division by zero) doesn't resolve compile-time consts
SW024 flags
% ROOT_RING_SIZEbecause the divisor is a non-literal, but it's amodule-level
constthat can never be zero. Resolvingconstvalues in scopebefore flagging a non-literal divisor would remove this class of FP.
FP 2 — SW002 (missing owner check) on accounts secured by a ZK proof or used only as a seed
In privacy/ZK programs, some accounts are intentionally
UncheckedAccountwitha documented
/// CHECK:rationale, because their integrity comes fromsomewhere the AST can't see:
Here an owner check would be meaningless —
recipientis a payout target, andfront-run protection is the proof binding (Tornado-style). Same for a relayer
payout account, and for an account used only as a PDA seed (
to_ownerwhosevalue is validated by the derived PDA's
bump).Two ideas: (a) lower confidence / severity when an
UncheckedAccountcarries adocumented
/// CHECK:comment, and (b) don't flag accounts that are only everused as a payout target or as a PDA seed input (never read as data). These are
the dominant SW002 FPs in ZK code.
FP 3 — SW027 (missing event) doesn't recognize
msg!program logsSW027 looks for Anchor
emit!events, but a large share of Solana programs(including much of SPL) expose state changes through structured
msg!programlogs that off-chain indexers parse directly — no Anchor event involved. Treating
a
msg!in the same handler as an observability sink would clear this FP.Context
The codebase is public if a concrete repro helps:
https://github.com/koshak01/tidex6 — the findings above are in
programs/tidex6-verifier,programs/tidex6-wusdc-pool, andprograms/tidex6-confidential-amounts. Happy to provide anything useful.Take or leave any of this — just wanted to give back after the tool caught a
real one for us.