<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Viktor Andriichuk</title>
    <description>The latest articles on DEV Community by Viktor Andriichuk (@vandriichuk).</description>
    <link>https://dev.to/vandriichuk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F696332%2F3b259e65-8644-43f3-bfa1-b5f2e4b14839.png</url>
      <title>DEV Community: Viktor Andriichuk</title>
      <link>https://dev.to/vandriichuk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vandriichuk"/>
    <language>en</language>
    <item>
      <title>Common Solana Program Vulnerabilities and How to Catch Them</title>
      <dc:creator>Viktor Andriichuk</dc:creator>
      <pubDate>Tue, 28 Jul 2026 20:21:00 +0000</pubDate>
      <link>https://dev.to/vandriichuk/common-solana-program-vulnerabilities-and-how-to-catch-them-2fc7</link>
      <guid>https://dev.to/vandriichuk/common-solana-program-vulnerabilities-and-how-to-catch-them-2fc7</guid>
      <description>&lt;p&gt;Most Solana exploits don't come from exotic cryptography or clever math. They come from a handful of well-understood mistakes that show up again and again — usually because Solana's account model puts the burden of validation on you, the program author, rather than the runtime.&lt;/p&gt;

&lt;p&gt;If you write Solana programs in Rust or Anchor, this is the list of bugs worth internalizing. For each one below you'll find why it happens, a vulnerable example, and the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Solana programs break differently
&lt;/h2&gt;

&lt;p&gt;On Ethereum, a contract's storage is bound to the contract. On Solana, programs are stateless: all state lives in separate accounts that the caller passes in with each instruction. That design is fast and flexible, but it has a consequence that trips up almost every new Solana developer:&lt;/p&gt;

&lt;p&gt;The runtime does not guarantee that an account is what you think it is. Anyone can pass any account into your instruction. If your program doesn't explicitly check that an account is a signer, is owned by the right program, is the right type, or was derived from the right seeds — an attacker will pass one that isn't.&lt;/p&gt;

&lt;p&gt;Almost every vulnerability class below is a variation on that single theme: validate the accounts you were handed.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Missing signer checks
&lt;/h2&gt;

&lt;p&gt;The most common and most expensive bug. If an account is supposed to authorize an action, it must sign the transaction. If you forget to enforce that, anyone can act as anyone.&lt;/p&gt;

&lt;p&gt;Vulnerable&lt;br&gt;
&lt;code&gt;#[derive(Accounts)]&lt;br&gt;
pub struct Withdraw&amp;lt;'info&amp;gt; {&lt;br&gt;
    #[account(mut)]&lt;br&gt;
    pub vault: Account&amp;lt;'info, Vault&amp;gt;,&lt;br&gt;
    /// CHECK: authority is not constrained — anyone can pass any account here&lt;br&gt;
    pub authority: AccountInfo&amp;lt;'info&amp;gt;,&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Because authority is a plain AccountInfo with no signer constraint, an attacker passes the real owner's public key without their signature and drains the vault.&lt;/p&gt;

&lt;p&gt;Fixed&lt;br&gt;
&lt;code&gt;#[derive(Accounts)]&lt;br&gt;
pub struct Withdraw&amp;lt;'info&amp;gt; {&lt;br&gt;
    #[account(mut, has_one = authority)]&lt;br&gt;
    pub vault: Account&amp;lt;'info, Vault&amp;gt;,&lt;br&gt;
    pub authority: Signer&amp;lt;'info&amp;gt;,&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Signer&amp;lt;'info&amp;gt; forces the account to have signed the transaction, and has_one = authority ensures it's the specific authority stored on the vault — not just any signer.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Missing owner checks
&lt;/h2&gt;

&lt;p&gt;Every account has an owner program. If you read data from an account without verifying who owns it, an attacker can hand you a look-alike account they created and fully control.&lt;/p&gt;

&lt;p&gt;Anchor's Account&amp;lt;'info, T&amp;gt; checks the owner and the 8-byte discriminator automatically. The bug appears when you drop down to raw AccountInfo or UncheckedAccount and deserialize manually.&lt;/p&gt;

&lt;p&gt;Vulnerable&lt;br&gt;
&lt;code&gt;// Manually deserializing without checking the owner&lt;br&gt;
let vault = Vault::try_from_slice(&amp;amp;account.data.borrow())?;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Fixed — let Anchor enforce it&lt;br&gt;
&lt;code&gt;#[account(owner = crate::ID)]&lt;br&gt;
pub vault: Account&amp;lt;'info, Vault&amp;gt;,&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you must use AccountInfo, check account.owner == &amp;amp;crate::ID yourself before trusting a single byte.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Account type confusion ("type cosplay")
&lt;/h2&gt;

&lt;p&gt;Two account types with the same memory layout can be swapped for each other if you don't distinguish them. Anchor prevents this with an 8-byte discriminator prepended to every account — but only if you use typed Account. Deserialize raw, and a UserConfig can masquerade as an AdminConfig.&lt;/p&gt;

&lt;p&gt;Fix: always use typed Anchor accounts (Account&amp;lt;'info, T&amp;gt;), which verify the discriminator, instead of decoding AccountInfo data by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. PDA seed and bump mistakes
&lt;/h2&gt;

&lt;p&gt;Program Derived Addresses (PDAs) are deterministic accounts your program controls. Two things go wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Seeds not validated. If you accept a PDA without re-deriving it from the expected seeds, an attacker passes a different PDA they can influence.&lt;/li&gt;
&lt;li&gt;Non-canonical bump. find_program_address returns the canonical bump. If you store a bump and don't validate against the canonical one, an attacker may derive a valid-but-different address with another bump.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fixed with Anchor constraints&lt;br&gt;
&lt;code&gt;#[account(&lt;br&gt;
    seeds = [b"vault", user.key().as_ref()],&lt;br&gt;
    bump = vault.bump,&lt;br&gt;
)]&lt;br&gt;
pub vault: Account&amp;lt;'info, Vault&amp;gt;,&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Anchor re-derives the address from the seeds and checks the stored canonical bump for you. Store the canonical bump at init time and always validate against it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Unchecked / arbitrary CPIs
&lt;/h2&gt;

&lt;p&gt;When your program calls another via a Cross-Program Invocation, you must verify which program you're calling. If the target program is just an account passed in without a check, an attacker substitutes a malicious program that does whatever they want with the accounts you forwarded.&lt;/p&gt;

&lt;p&gt;Vulnerable&lt;br&gt;
&lt;code&gt;// The program being invoked is whatever was passed in — never verified&lt;br&gt;
invoke(&amp;amp;ix, &amp;amp;[account_a, account_b, some_program])?;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Fixed: pin the program to a known ID (Anchor's typed CPI does this):&lt;/p&gt;

&lt;p&gt;Fixed&lt;br&gt;
&lt;code&gt;require_keys_eq!(token_program.key(), anchor_spl::token::ID);&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Integer overflow and underflow
&lt;/h2&gt;

&lt;p&gt;This one is Rust-specific and catches people coming from other languages. Solana programs compile in release mode, where arithmetic overflow silently wraps instead of panicking. balance - amount can underflow to a huge number; reward * multiplier can wrap to a small one.&lt;/p&gt;

&lt;p&gt;Vulnerable&lt;br&gt;
&lt;code&gt;vault.balance = vault.balance - amount; // underflows silently in release mode&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Fixed&lt;br&gt;
&lt;code&gt;vault.balance = vault.balance&lt;br&gt;
    .checked_sub(amount)&lt;br&gt;
    .ok_or(ErrorCode::MathOverflow)?;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Use checked_* (or saturating_* where appropriate) for every arithmetic operation on values that matter. You can also set overflow-checks = true in your Cargo.toml profile, but explicit checked math is clearer and lets you return a proper error.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Duplicate mutable accounts
&lt;/h2&gt;

&lt;p&gt;If an instruction takes two accounts of the same type and mutates both, an attacker can pass the same account twice. Logic that assumes they're distinct (e.g. transferring between two vaults) breaks in the attacker's favor.&lt;/p&gt;

&lt;p&gt;Fix&lt;br&gt;
&lt;code&gt;#[account(constraint = vault_a.key() != vault_b.key())]&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Account reinitialization and revival
&lt;/h2&gt;

&lt;p&gt;Closing an account and reopening it, or reinitializing an already-initialized account, can reset state an attacker wants reset. Use Anchor's init (which fails if the account already exists) rather than init_if_needed unless you fully understand the implications, and use the close constraint to close accounts safely rather than zeroing lamports by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to catch these before you deploy
&lt;/h2&gt;

&lt;p&gt;The pattern across all eight is the same: your program must validate every account it's given. Three habits prevent most of them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prefer Anchor's typed accounts and constraints (Signer, Account, has_one, seeds, bump, owner). They turn most of the checks above into declarative one-liners the framework enforces for you.&lt;/li&gt;
&lt;li&gt;Use checked arithmetic everywhere on security-relevant values.&lt;/li&gt;
&lt;li&gt;Review every AccountInfo / UncheckedAccount / /// CHECK: in your codebase — each one is a place where you've opted out of Anchor's automatic safety and taken the validation burden on yourself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Manual review catches a lot, but it's slow and it misses things — especially the boring ones, which are exactly the expensive ones. That's the gap &lt;a href="https://vaultlint.com/" rel="noopener noreferrer"&gt;VaultLint&lt;/a&gt; is built for: an AI security linter that reads Rust and Anchor programs the way an auditor would and flags missing signer checks, PDA mistakes, unsafe CPIs, and overflow before you ship — so a full audit can focus on the hard stuff. It complements a manual audit; it doesn't replace one.&lt;/p&gt;

&lt;p&gt;Ship fewer bugs. Catch the common, drainable mistakes in the PR, not on the first mainnet block.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>crypto</category>
      <category>rust</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Audit an Anchor Program: A Practical Checklist</title>
      <dc:creator>Viktor Andriichuk</dc:creator>
      <pubDate>Sun, 26 Jul 2026 18:33:00 +0000</pubDate>
      <link>https://dev.to/vandriichuk/how-to-audit-an-anchor-program-a-practical-checklist-e6a</link>
      <guid>https://dev.to/vandriichuk/how-to-audit-an-anchor-program-a-practical-checklist-e6a</guid>
      <description>&lt;p&gt;Auditing a Solana program isn't about reading every line top to bottom and hoping something jumps out. It's about knowing the small set of places where Anchor programs actually go wrong and checking each one deliberately. This guide walks through that process in the order an experienced reviewer would follow, and ends with a checklist you can paste into your review notes.&lt;/p&gt;

&lt;p&gt;Anchor does a lot of validation for you — but only when you use its typed accounts and constraints. Most Anchor vulnerabilities are places where the author quietly opted out of that protection. So a large part of auditing an Anchor program is finding where the safety rails were removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Map the instructions and their authority
&lt;/h2&gt;

&lt;p&gt;Before reading any logic, list every instruction and answer one question for each: who is allowed to call this, and how is that enforced?&lt;/p&gt;

&lt;p&gt;For every privileged instruction (withdraw, close, update-config, mint, set-authority), find the account that authorizes it and confirm it's declared as Signer&amp;lt;'info&amp;gt; and tied to the state it acts on with has_one or an explicit constraint. If the authority is an AccountInfo or UncheckedAccount, that's your first finding.&lt;/p&gt;

&lt;p&gt;Good: authority must sign AND must match the stored authority&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#[account(mut, has_one = authority)]&lt;br&gt;
pub config: Account&amp;lt;'info, Config&amp;gt;,&lt;br&gt;
pub authority: Signer&amp;lt;'info&amp;gt;,&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Grep for the escape hatches
&lt;/h2&gt;

&lt;p&gt;The fastest way to find risk in an Anchor codebase is to search for the places where automatic validation was bypassed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AccountInfo and UncheckedAccount — no owner, type, or discriminator check.&lt;/li&gt;
&lt;li&gt;/// CHECK: comments — every one marks an account the author told Anchor to trust blindly. Read each and decide whether the accompanying validation is actually sufficient.&lt;/li&gt;
&lt;li&gt;init_if_needed — reinitialization risk; confirm it can't be abused to reset state.&lt;/li&gt;
&lt;li&gt;Raw invoke / invoke_signed — manual CPIs where the target program may not be verified.&lt;/li&gt;
&lt;li&gt;try_from_slice / manual deserialization — bypasses discriminator and owner checks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each hit is a place to slow down. Anchor's defaults are safe; these are where safety was turned off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Verify account constraints
&lt;/h2&gt;

&lt;p&gt;Go through each #[derive(Accounts)] struct and confirm the constraints match the intent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signer checks — is every authorizing account a Signer?&lt;/li&gt;
&lt;li&gt;has_one — does state that stores an authority/owner/mint enforce it matches the passed account?&lt;/li&gt;
&lt;li&gt;seeds + bump — are PDAs re-derived from the correct seeds, and is the stored canonical bump validated?&lt;/li&gt;
&lt;li&gt;owner — for any account not using typed Account, is the owner checked?&lt;/li&gt;
&lt;li&gt;mut — are exactly the accounts that get written marked mutable, and no more?&lt;/li&gt;
&lt;li&gt;address = ... — for programs, sysvars, and known accounts, is the expected address pinned?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Check the CPIs
&lt;/h2&gt;

&lt;p&gt;For every cross-program invocation, confirm the program being called is verified, not just passed in. Prefer Anchor's typed CPI (CpiContext with a typed program account) over raw invoke. If raw invoke is used, the target program ID must be checked explicitly:&lt;/p&gt;

&lt;p&gt;Pin the program to a known ID&lt;/p&gt;

&lt;p&gt;&lt;code&gt;require_keys_eq!(token_program.key(), anchor_spl::token::ID);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Also check that invoke_signed uses the correct PDA seeds and that you aren't accidentally signing for an account you don't control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Audit the arithmetic
&lt;/h2&gt;

&lt;p&gt;Solana programs run in release mode, so overflow wraps silently. Search for bare +, -, * on balances, amounts, rewards, shares, and timestamps. Each should be checked_* (returning an error on overflow) or saturating_* where wrapping to a bound is intended. Watch for as casts that can truncate (u64 as u32), and for divisions that can round in the protocol's disfavor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Review account lifecycle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Initialization: does init prevent re-initializing existing accounts? Is the payer and space correct?&lt;/li&gt;
&lt;li&gt;Closing: are accounts closed with Anchor's close constraint (which zeroes data and reclaims rent safely) rather than by hand? Manual closing invites revival attacks.&lt;/li&gt;
&lt;li&gt;Rent: are accounts that must persist rent-exempt?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 7: Look for duplicate-account and account-ordering assumptions
&lt;/h2&gt;

&lt;p&gt;If an instruction takes two accounts of the same type and mutates both, confirm they're required to be distinct (constraint = a.key() != b.key()). If logic assumes a particular relationship between accounts (e.g. "this token account belongs to this user"), confirm that relationship is enforced with a constraint, not just assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Anchor audit checklist
&lt;/h2&gt;

&lt;p&gt;Copy this into your review:&lt;/p&gt;

&lt;p&gt;Anchor program audit checklist&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every privileged instruction has a Signer that is tied to state (has_one / constraint)&lt;/li&gt;
&lt;li&gt;[ ] No unexplained AccountInfo / UncheckedAccount on authority accounts&lt;/li&gt;
&lt;li&gt;[ ] Every /// CHECK: is justified and backed by real validation&lt;/li&gt;
&lt;li&gt;[ ] PDAs use seeds + canonical bump; bump is stored and validated&lt;/li&gt;
&lt;li&gt;[ ] Typed Account used wherever possible (owner + discriminator checked)&lt;/li&gt;
&lt;li&gt;[ ] All CPIs verify the target program ID (typed CPI or explicit check)&lt;/li&gt;
&lt;li&gt;[ ] invoke_signed uses correct, controlled PDA seeds&lt;/li&gt;
&lt;li&gt;[ ] All arithmetic on values-of-record uses checked_* / saturating_*&lt;/li&gt;
&lt;li&gt;[ ] No silent truncation in &lt;code&gt;as&lt;/code&gt; casts&lt;/li&gt;
&lt;li&gt;[ ] init prevents reinitialization; init_if_needed justified if present&lt;/li&gt;
&lt;li&gt;[ ] Accounts closed via Anchor &lt;code&gt;close&lt;/code&gt;, not manual lamport draining&lt;/li&gt;
&lt;li&gt;[ ] Same-type mutable accounts required to be distinct&lt;/li&gt;
&lt;li&gt;[ ] mut applied only to accounts actually written&lt;/li&gt;
&lt;li&gt;[ ] Known programs/sysvars pinned with address = ...&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where automation fits
&lt;/h2&gt;

&lt;p&gt;A human auditor is irreplaceable for logic bugs and economic design flaws. But the checklist above is mostly mechanical — exactly the kind of review that's tedious to do by hand on every PR and easy to let slip under deadline. That's where an automated pass earns its keep.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://vaultlint.com/" rel="noopener noreferrer"&gt;VaultLint&lt;/a&gt; runs this style of review automatically: it reads your Anchor and native Solana programs, flags the missing constraints, unverified CPIs, and unchecked arithmetic, and tells you the file, the line, why it's dangerous, and how to fix it — in CI, on every PR. It's a linter, not a replacement for a full audit: it clears out the common, expensive mistakes early so your paid audit can spend its hours on the hard, protocol-specific stuff.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>rust</category>
      <category>security</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
