Bug classes in smart contracts and Solana programs
1. TL;DR
Section titled “1. TL;DR”Most catastrophic DeFi bugs are not exotic cryptographic breaks — they are a small number of recurring patterns repeated across thousands of contracts. On Ethereum, the dominant families are unsafe external calls (reentrancy, including through “safe” ERC-721/1155 transfer hooks) and indirection bugs where a seemingly harmless assumption about identity or state quietly becomes exploitable when contracts compose. On Solana, the account model itself is the attack surface: because any account, owned by any program, can be passed into an instruction, the dominant bug family is a missing or incomplete validation check — of a signer, an owner, an account’s type, or the identity of a program being invoked. Both ecosystems converge on the same underlying lesson: a component that is individually correct can still be unsafe once something else is allowed to call it, pass data into it, or share its execution context.
2. Explain it simply
Section titled “2. Explain it simply”Analogy
Section titled “Analogy”Think of a smart contract as a shop with two standing assumptions it rarely questions: that whoever hands over a form is who they claim to be, and that whoever it calls out to for a quick errand will come straight back without wandering off and touching the till. Reentrancy is what happens when the shop calls out to a customer mid-transaction — “here’s your receipt” — and the customer, instead of just taking it, uses that open moment to slip back in through a side door and ask for another refund before the shop has updated its books. Missing-signer and missing-owner bugs are the same shop accepting a receipt-shaped piece of paper from anyone, without checking it was actually signed by the person it claims to be from or issued by the shop itself. Both failures share one root cause: the shop treated a boundary — a callback, or an incoming piece of data — as safe to trust without checking it.
Programs that hold money have to be very careful about two things: who is asking them to do something, and whether they finish one task completely before starting another. A common mistake is a program that starts sending money out, then lets the receiver “interrupt” it to ask for more money again before the program has written down that it already paid the first time — so the receiver can ask for money over and over inside one single request. Another common mistake is a program that checks a piece of information — like an ID card — but never checks who actually issued that ID card, so someone can hand over a fake one that looks right and get treated as trusted.
Step-by-step walkthrough
Section titled “Step-by-step walkthrough”Scenario: a simple ETH vault contract lets anyone deposit ETH and withdraw it later; it holds 110 ETH total (10 ETH belongs to an attacker who deposited moments earlier, 100 ETH belongs to other users). Its withdraw() function sends ETH to the caller before setting their recorded balance to zero — a real, historically common bug pattern (the same shape as the 2016 DAO exploit).
- Before. Vault balance: 110 ETH total. The attacker’s recorded internal balance: 10 ETH. Attacker’s wallet: 0 ETH (just deposited).
- Attacker calls
withdraw(10 ETH). The vault checks the attacker’s recorded balance (10 ETH — sufficient), then sends 10 ETH to the attacker’s contract before zeroing the recorded balance. - The attacker’s contract re-enters. Receiving ETH triggers the attacker’s fallback function, which immediately calls
withdraw(10 ETH)again. Because the vault never got the chance to finish step 2 and set the balance to zero, the check still reads “10 ETH available” — so it sends another 10 ETH. - This repeats. Each re-entrant call sends another 10 ETH before any balance is updated. After 11 total sends (10 ETH × 11 = 110 ETH), the vault is empty.
- After. Vault balance: 0 ETH. Attacker’s wallet: 110 ETH (their original 10 ETH plus 100 ETH belonging to everyone else). The recorded balances inside the contract are now meaningless — every other depositor’s “balance” still shows a number, but there is no ETH left to honor it.
Common misconceptions
Section titled “Common misconceptions”- Myth: A function named
safeTransferorsafeMintis safer than the plain version. Reality: The “safe” ERC-721/1155 functions perform a callback to check the recipient can handle the token — and that callback is itself an unsafe external call an attacker-controlled recipient can use to re-enter (samczsun, 2021-08-13). - Myth: Reentrancy is a single well-understood bug that’s been “solved.” Reality: samczsun prefers the broader term unsafe external calls, because the same root cause — handing control to attacker-influenced code mid-execution — keeps resurfacing in new forms, including through libraries and standards designed years after the original DAO hack (samczsun, 2021-08-13, 2021-08-17).
- Myth: Solana’s account model is safe by default because accounts are typed data structures. Reality: On-chain, an account is just a byte array with an owner field; nothing stops a program from being handed an account of the wrong type or one it doesn’t own unless it explicitly checks (Helius, n.d.; sealevel-attacks, accessed 2026-08).
- Myth: If a contract’s core logic is individually audited and correct, composing it with other audited contracts is safe. Reality: samczsun’s “Two Rights Might Make A Wrong” is precisely the case where two individually-reasonable libraries (a Dutch-auction contract and a generic batch-call mixin) combined to create a $350M vulnerability neither had alone (samczsun, 2021-08-17).
- Myth: A bug has to be new code to be dangerous. Reality: samczsun’s four-year-old EToken2 bug had sat, unexploited, in contracts managing over $1B for four years before anyone found it (samczsun, 2021-04-19).
If you only remember one thing
Section titled “If you only remember one thing”Almost every catastrophic bug is a boundary problem — a place where a contract hands control to, or trusts data from, something it did not fully verify — not a failure of the core business logic itself.
3. How it works
Section titled “3. How it works”Ethereum: unsafe external calls
Section titled “Ethereum: unsafe external calls”The classic reentrancy shape (§2) generalizes to what samczsun calls unsafe external calls: any time control flow passes to a contract address that isn’t fully trusted, that code can do “whatever it wants,” including calling back into the original contract before it finishes updating its own state (samczsun, 2021-08-13). This is not limited to .call{value: x}(): the ERC-721 and ERC-1155 standards deliberately added recipient-callback checks (_checkOnERC721Received, _doSafeTransferAcceptanceCheck) so tokens can’t get stuck in a contract that can’t handle them — but that same callback is a reentrant hook. samczsun’s two case studies show the pattern in practice: Hashmasks’ mintNFT() capped purchases at 20 per call, but its _safeMint() call let a malicious recipient re-enter mintNFT() from inside the callback, minting 39 masks instead of 20 before the loop’s own counter caught up; the ENS Name Wrapper’s wrap() function minted an ERC-1155 token representing a domain before verifying the caller actually owned the underlying ENS name, and the mint’s safety callback let an attacker take control of the domain during the callback window, before the ownership check that was supposed to protect it ever ran (samczsun, 2021-08-13).
A second, distinct Ethereum bug family is indirection through identity: a system correctly enforces a rule syntactically but the rule doesn’t mean what it appears to. samczsun’s four-year-old EToken2 bug is the sharpest example: the contract’s _transfer function operated on holder IDs (uint), not addresses, and included a _grantAccess function meant to let a user recover access if they granted it to the wrong address by mistake — but it never revoked the granter’s access to the recipient’s new holder ID in return. The exploit was not to grant your own funds away, but to realize that granting access created a mutual backdoor: the attacker could front-run any new user’s first transaction, silently grant access to their own not-yet-used holder ID, and thereby gain permanent, invisible control of every future asset that holder ID accumulated (samczsun, 2021-04-19). The “Two Rights Might Make A Wrong” case is a related composability failure rather than a single-function bug: BoringBatchable’s delegatecall-based batch function correctly preserved msg.sender, and a Dutch-auction contract’s commitEth function correctly checked msg.value — but combined, delegatecall’s preservation of msg.value across every batched call meant one ETH payment could be counted repeatedly, letting an attacker bid the same ETH an unlimited number of times (samczsun, 2021-08-17).
Solana: missing checks and account confusion
Section titled “Solana: missing checks and account confusion”Solana’s runtime passes an instruction’s accounts to a program as raw, program-controlled input — “Solana is Attacker-Controlled”: a malicious client can supply any account it wants, owned by any program, with any data, as long as it satisfies the instruction’s declared account count and mutability, not its meaning (Helius, n.d.). The catalog of resulting bug classes, documented in near-identical form by both the Helius guide and the coral-xyz/sealevel-attacks repository (whose stated purpose is “examples of common exploits unique to the Solana programming model and recommended idioms for avoiding these attacks using the Anchor framework,” accessed 2026-08-30), includes:
- Missing signer check. A privileged instruction (e.g. changing an admin) checks that a supplied account’s public key matches the stored admin, but never checks that account actually signed the transaction (
AccountInfo::is_signer) — letting anyone submit the admin’s public key as a plain argument and pass the check without ever holding the admin’s private key (Helius, n.d.; Neodyme, 2021-08-20). - Missing owner check. An instruction reads fields from an account (e.g. a config account’s
adminfield) without checking the account’sownerfield actually matches the expected program — letting an attacker supply their own fabricated account, populated with whatever data they like, in place of the real one (Helius, n.d.; Neodyme, 2021-08-20). - Type cosplay (account confusion). Without an explicit type discriminator checked at deserialization, a program can be tricked into interpreting one account type (e.g. an ordinary user account) as another (e.g. an admin account) if their underlying byte layouts happen to overlap (Helius, n.d.).
- Arbitrary CPI. A program performs a cross-program invocation (CPI) — calling another on-chain program — based on a caller-supplied program ID without checking it matches the program actually intended, letting an attacker substitute their own malicious program in place of a trusted one (Helius, n.d.).
- Bump seed canonicalization. Program-derived addresses (PDAs) are computed from seeds plus a “bump” value; using a caller-supplied bump instead of always deriving the one canonical (highest-valid) bump lets an attacker create multiple valid-looking PDAs for what should be one unique identity (Helius, n.d.).
- Duplicate mutable accounts. An instruction expecting two distinct mutable accounts (e.g. a reward account and a bonus account) doesn’t check they’re actually different — an attacker who passes the same account for both gets it updated twice (Helius, n.d.).
- Integer overflow/underflow. Rust’s overflow panics are compiled out in release mode by default, and Solana’s
cargo build-bpfbuilds in release mode — so unchecked arithmetic on token balances silently wraps instead of erroring (Helius, n.d.; Neodyme, 2021-08-20).
Neodyme’s independent audit-derived list converges on the same core five (missing ownership check, missing signer check, integer overflow/underflow, arbitrary signed program invocation, and account-type confusion), summarizing the throughline bluntly: “your contract should only trust accounts owned by itself,” and “always (!) verify the pubkey of any program you invoke via the invoke_signed() API” (Neodyme, 2021-08-20).
4. Worked numeric example
Section titled “4. Worked numeric example”Continuing the vault reentrancy scenario from §2, computed end to end.
Let be the vault’s total ETH balance and the attacker’s recorded internal balance, both starting at , . The buggy withdraw(amount) function performs, in order: (1) check ; (2) send amount ETH to msg.sender via a low-level call; (3) set . Because step 2 triggers the attacker’s fallback function before step 3 executes, a re-entrant call to withdraw(10) sees the stale, not-yet-decremented and passes its check again.
| Call # | Balance check ( before send) | ETH sent this call | Vault balance after send | recorded after (never actually reached, due to reentrancy) |
|---|---|---|---|---|
| 1 (outer) | 10 | 10 | 100 | (unreached) |
| 2 (re-entrant) | 10 | 10 | 90 | (unreached) |
| 3 | 10 | 10 | 80 | (unreached) |
| 4 | 10 | 10 | 70 | (unreached) |
| 5 | 10 | 10 | 60 | (unreached) |
| 6 | 10 | 10 | 50 | (unreached) |
| 7 | 10 | 10 | 40 | (unreached) |
| 8 | 10 | 10 | 30 | (unreached) |
| 9 | 10 | 10 | 20 | (unreached) |
| 10 | 10 | 10 | 10 | (unreached) |
| 11 | 10 | 10 | 0 | (unreached) |
At call 11, the vault’s actual ETH balance reaches exactly 0, so the 11th send exhausts the contract; a 12th attempted call would fail simply because there is no ETH left to transfer, not because any check caught the problem. Total sent to the attacker: ETH — their own 10 ETH plus all 100 ETH belonging to other depositors — for the cost of one initial 10 ETH deposit and the gas for 11 nested calls. The fix is a one-line reordering: set before sending ETH (checks-effects-interactions), or wrap the function in a reentrancy guard that reverts any nested call to the same function while the first is still executing.
5. Where it’s used
Section titled “5. Where it’s used”Ethereum
Section titled “Ethereum”- OpenZeppelin
ReentrancyGuard— anonReentrantmodifier that reverts any nested call into a guarded function while it’s already executing; the most widely deployed defense against the pattern in §2–§4. OpenZeppelin docs - Slither, Mythril, and other static analyzers — automated tools that flag unchecked external calls, unprotected
delegatecall, and state-updated-after-external-call patterns before deployment. - Checks-effects-interactions as a coding convention — the pattern samczsun’s fix for the vault scenario embodies: validate, update internal state, then perform any external call, rather than the reverse order.
- Anchor’s
#[account]type-safety layer — while Anchor is a Solana framework, its underlying idea (typed wrappers that fail closed on the wrong owner/discriminator) has clear Ethereum-side analogues in typed, audited base contracts (OpenZeppelin’sERC721/ERC1155implementations) that centralize the exact_safeMint/_checkOnERC721Receivedlogic samczsun analyzed, so a single upstream fix propagates to every consumer.
Solana
Section titled “Solana”- Anchor framework —
Signer<'info>enforces a signer check automatically;Account<'info, T>verifies both ownership and an account-type discriminator during deserialization, directly closing the missing-signer, missing-owner, and type-cosplay classes described in §3 (Helius, n.d.). - coral-xyz/sealevel-attacks — a public, MIT-licensed repository of minimal, intentionally-vulnerable example programs paired with an Anchor-based fix for each bug class in §3, maintained as a teaching reference rather than production code (accessed 2026-08-30).
- Neodyme’s audit practice and public writeups — Neodyme states its audits have “helped prevent the potential theft of roughly USD 1 billion worth of assets” (Neodyme, 2021-08-20), and its “Solana Security Workshop” is listed by Helius as a recommended follow-on resource for developers.
- OtterSec, Sec3 X-Ray, and Immunefi’s Solana bounty program — third-party auditors and automated vulnerability scanners specifically targeting the account-validation bug classes catalogued above (Helius, n.d., “Additional Resources”).
6. Risks, attacks, and incidents
Section titled “6. Risks, attacks, and incidents”- The DAO, June 2016 (Ethereum L1), $60M (3.6M ETH, as of 2016-06). The foundational reentrancy incident: a withdraw function sent ETH before zeroing the caller’s balance, letting a recursive external call drain the contract — the direct ancestor of the bug pattern in §2–§4.
- Parity multisig wallet, July 2017 and November 2017 (Ethereum L1), $30M then $280M frozen. Two separate access-control failures in the same codebase: first, an uninitialized-then-hijacked wallet library let an attacker become “owner” and drain funds directly; four months later, the same underlying library, once fixed to require explicit initialization, was itself left uninitialized, so a user could claim ownership of the library and self-destruct it — permanently freezing every wallet that still depended on it via
delegatecall. Root cause: extracting shared logic into a library that anyone can call directly turns a “call this once to initialize” convention into a standing vulnerability. - Cream Finance, 27 August 2021 (Ethereum L1), $18.8M–$29M (as of 2021-08). A reentrancy exploit via the AMP token’s ERC-777-style transfer hook — proof that “safe,” standard-compliant token transfer callbacks (§3) reintroduce reentrancy even in a lending protocol whose core logic had otherwise guarded against it.
- Badger DAO, 2 December 2021 (Ethereum L1), $120M (as of 2021-12). Not a smart-contract bug at all: a compromised Cloudflare API key let attackers inject malicious token-approval prompts into Badger’s legitimate front-end for roughly three weeks before detection — a reminder that the bug classes in this page cover on-chain logic, but the website serving that logic to users is a separate, equally critical trust boundary.
- Munchables, 26 March 2024 (Blast L2), $62.5M (as of 2024-03; later fully returned). A developer who had been hired as a normal team member and granted legitimate upgrade authority used that access to drain the protocol directly — an access-control failure with no exploited code bug at all, since the “attacker” held every credential a real admin would.
- Cashio, 22 March 2022 (Solana), $52.8M. The Solana-side mirror of a missing-owner-check bug: the protocol never validated that a supplied collateral account was actually issued by the real Saber program, so an attacker minted unlimited CASH stablecoin against a forged, worthless account — directly the “Missing Ownership Check” pattern from §3.
- Crema Finance, 3 July 2022 (Solana), $8.78M. A flash-loan-funded attacker fabricated a fake “tick account” for the concentrated-liquidity pool that the program never verified as genuinely belonging to it, letting the attacker claim inflated fees — most funds were later returned for a bounty.
- Wormhole token bridge, 2 February 2022 (Solana-side program), $326M. A deprecated Solana sysvar-verification function let an attacker forge a guardian signature check and mint 120,000 wETH with no real collateral locked — a missing/bypassable verification bug at bridge scale; the full trust-model analysis is covered in /cross-chain/bridge-designs/ and the incident is tabulated in /security/incident-timeline/.
- Bybit, 21 February 2025 (Ethereum, Safe multisig), $1.46B. Not a Solidity or Sealevel bug, but the same “trust the boundary, not just the logic” lesson at hardware-wallet scale: malicious JavaScript injected into the Safe{Wallet} front-end altered what multisig signers saw on their hardware-wallet screens, so they cryptographically signed a transaction different from the one they believed they were approving.
- Mango Markets, October 2022 (Solana) and Loopscale, April 2025 (Solana) — both oracle/pricing-input bugs rather than raw account-validation bugs; covered in full on /lending/collateral-ltv-health/ and /lending/risk-engines/ respectively, and included here only as cross-references since their root cause (a technically correct formula fed a manipulated or spoofed input) is adjacent to, but distinct from, the missing-check bug classes in §3.
7. Open problems
Section titled “7. Open problems”- Can type systems eliminate whole bug classes, or only catch known ones? Anchor’s discriminator and typed-account wrappers close missing-owner and type-cosplay bugs by construction, but arbitrary-CPI and bump-seed-canonicalization bugs still require the developer to remember to add an explicit check — no source reviewed here claims Anchor makes these classes structurally impossible, only harder to forget.
- Does static analysis scale with composability? samczsun’s cases (MISO, ENS Name Wrapper) were found by manual expert review, not automated tooling, and both involved a combination of individually-reasonable components — a pattern that whole-program static analyzers struggle to flag when each component passes its own isolated audit.
- Is release-mode Solana compilation still a live footgun? The overflow/underflow class exists specifically because
cargo build-bpfcompiles in release mode by default (Helius, n.d.) — whether newer Solana build tooling has changed this default, and how many deployed programs still rely on the now-standardchecked_add/checked_submitigation rather than the compiler catching the mistake for them, is not resolved in this page’s sources. - How much of “bug class” education actually reaches developers before deployment? Neodyme, Helius, and sealevel-attacks all independently catalog nearly the same list of Solana bug classes years apart, and Cashio (2022) and Crema (2022) — both squarely missing-owner-check bugs — happened after some of this educational material already existed, suggesting a persistent gap between documented best practice and shipped code.
8. Ethereum vs Solana
Section titled “8. Ethereum vs Solana”| Aspect | Ethereum | Solana |
|---|---|---|
| Dominant bug family | Unsafe external calls (reentrancy) and identity/indirection bugs in composed contracts | Missing or incomplete account validation (signer, owner, type, program identity) |
| Root architectural cause | Any external call can transfer control to attacker-influenced code mid-execution | Any account, owned by any program, can be passed as instruction input; the runtime enforces structure, not meaning |
| Canonical framework-level fix | nonReentrant modifiers, checks-effects-interactions convention, typed base contracts (OpenZeppelin) | Anchor’s Signer<'info> and Account<'info, T> wrappers enforcing checks by default |
| Canonical teaching resource | Individual practitioner writeups (samczsun’s Paradigm posts) rather than one repository | A single, purpose-built repository (coral-xyz/sealevel-attacks) cataloguing each class with a matching fix |
| Representative incident | The DAO (2016), Cream Finance reentrancy (2021) | Cashio (2022), Crema Finance (2022) |
Both ecosystems’ worst bugs trace back to the same idea — a program trusted something at its boundary that it should have verified — but the shape of the boundary differs. Ethereum’s boundary is temporal: control leaves the contract and comes back, and anything can happen in between. Solana’s boundary is structural: data arrives as an untyped account, and nothing about the runtime call itself guarantees it means what the program assumes. Anchor’s popularity is best understood as an attempt to make Solana’s structural boundary look more like a checked, typed interface — closing by convention the exact gap that Ethereum’s ecosystem still relies on discipline (checks-effects-interactions) and after-the-fact tooling (static analyzers, audits) to catch.
9. Reference doc
Section titled “9. Reference doc”The reference
Section titled “The reference”Two Rights Might Make A Wrong — samczsun (Paradigm), 17 August 2021. paradigm.xyz/writing/two-rights-might-make-a-wrong
Summary of the reference
Section titled “Summary of the reference”The post opens with the composability warning that frames the whole piece: “A common misconception in building software is that if every component in a system is individually verified to be safe, the system itself is also safe… it only takes one vulnerability to cause serious financial damage.” samczsun narrates, essentially in real time with timestamps, discovering a critical bug in SushiSwap’s MISO fundraising platform. Investigating an unrelated Telegram discussion, he examines MISO’s DutchAuction contract and notices it imports BoringBatchable, a mixin that lets any contract accept batched calls via repeated delegatecalls to itself. He recalls a structurally identical bug from a year earlier (an Opyn hack that reused a single ETH payment across multiple option exercises via msg.value in a loop) and realizes delegatecall preserves both msg.sender and msg.value — meaning a single ETH payment, batched across many calls to commitEth, could be counted as a fresh payment every time, letting an attacker bid for free. Escalating further, he discovers the auction’s refund logic would return all of a bidder’s ETH once the hard cap was exceeded rather than rejecting the excess — turning a “bid for free” bug into a “drain the entire $350M auction” bug.
The second half of the post is an operational narrative, not a technical one: samczsun contacts the Sushi team within minutes, a war room forms with Paradigm colleagues and Immunefi, and the group considers three options (ignore it, exploit it themselves via Flashbots to rescue funds, or use admin permissions to buy the remaining allocation and finalize the auction). They choose the third option, execute a flash-loan-funded rescue purchase, then discover a second, still-live batch auction with no hard cap (so no drainable refund bug, but still $8M at risk) and patch it live using an unrelated points-list hook repurposed as an improvised circuit breaker. The entire process — from discovery to patched — took about five hours, protecting $350M with zero actual funds lost.
samczsun closes with two lessons stated directly: first, that msg.value-based payment checks are dangerous inside any loop or batchable context because the value persists across delegatecalls; second, and more broadly, “safe components can come together to make something unsafe” — there is no single checklist item that would have caught this, only awareness that new components change what interactions are possible.
Key quotes
Section titled “Key quotes”“A common misconception in building software is that if every component in a system is individually verified to be safe, the system itself is also safe.” (opening paragraph)
“Inside a delegatecall,
msg.senderandmsg.valueare persisted.” (§ The Discovery)
“I was looking at a 350 million dollar bug.” (§ The Discovery)
“Second, safe components can come together to make something unsafe… even safe contract-level components can be mixed in a way that produces unsafe contract-level behavior.” (§ The Reflection)
How to read the original
Section titled “How to read the original”Background needed: what delegatecall does differently from a normal external call (it runs the target’s code in the caller’s storage and message context), and roughly what a Dutch auction and a batch auction are. What to skip on a first pass: the minute-by-minute operational timestamps and the “who joined which Zoom room” narrative — they convey urgency but aren’t needed to understand the bug. The hardest part to internalize is why BoringBatchable looked correct in isolation: its implementation (a loop of delegatecalls with careful success/revert handling) is genuinely a reasonable, common pattern; the vulnerability only exists in combination with a second contract’s separate assumption that msg.value means “one fresh payment” — neither assumption is wrong on its own.
What changed since
Section titled “What changed since”- OpenZeppelin and other library maintainers have since added explicit warnings against combining
msg.value-based accounting with any batchable/delegatecall-based mixin, directly citing incidents like this one. - samczsun’s own companion posts from the same week (“The Dangers of Surprising Code” and, months later, “Uncovering a Four Year Old Bug”) extend the same composability warning to ERC-721/1155 safe-transfer callbacks and to identity-indirection bugs respectively — together the three posts form a loose trilogy on how “individually correct” components fail when combined, all summarized in §3 of this page.
- Solana’s ecosystem, maturing after these Ethereum-side incidents, converged on a structurally different mitigation (Anchor’s typed account wrappers, §5) rather than repeating the “audit each component, hope composability is safe” approach — arguably a direct lesson learned from this era of Ethereum incidents.
Secondary references
Section titled “Secondary references”- samczsun, “The Dangers of Surprising Code” (Paradigm, 2021-08-13) — read for the ERC-721/1155 safe-transfer reentrancy cases (Hashmasks, ENS Name Wrapper) referenced in §3.
- samczsun, “Uncovering a Four Year Old Bug” (Paradigm, 2021-04-19) — read for the EToken2 identity-indirection case, the sharpest example in this page’s sources of a bug surviving years of production use.
- Neodyme, “Solana Smart Contracts: Common Pitfalls and How to Avoid Them” (2021-08-20) — read for the audit-firm perspective on the same five core Solana bug classes, independently converging with sealevel-attacks and Helius’s catalog.
The reference
Section titled “The reference”sealevel-attacks (coral-xyz) — GitHub repository, “Common Security Exploits and Protections on Solana,” accessed 2026-08-30. github.com/coral-xyz/sealevel-attacks
Summary of the reference
Section titled “Summary of the reference”Unlike the narrative Paradigm posts above, this reference is a structured teaching repository rather than an essay. Its stated purpose is to provide “examples of common exploits unique to the Solana programming model and recommended idioms for avoiding these attacks using the Anchor framework,” and it explicitly states the examples are “purposefully not complete” — each program in the repository “is meant to showcase a specific issue and recommended fix in isolation,” mirroring exactly the “individually correct, composably dangerous” theme of the Paradigm reference above, but inverted: here, each example is deliberately isolated so learners can study one bug class at a time before reasoning about how classes interact in a real, composed program. The repository (663 stars as of this fetch) organizes its examples as paired Solana programs — an insecure version demonstrating the vulnerability and a secure version demonstrating the Anchor-idiomatic fix — for the same bug classes documented in detail in §3 of this page via the Helius guide: missing signer and owner checks, type cosplay, arbitrary CPI, bump seed canonicalization, duplicate mutable accounts, and related account-validation failures. Its role in the Solana security ecosystem is closer to a shared reference implementation than an argument: the Helius Hitchhiker’s Guide, Neodyme’s audit writeups, and Anchor’s own documentation all point back to this repository (or an equivalent list of the same categories) as the canonical enumeration developers and auditors are expected to check against.
Key quotes
Section titled “Key quotes”“Examples of common exploits unique to the Solana programming model and recommended idioms for avoiding these attacks using the Anchor framework.” (repository description)
“The examples in this repo are purposefully not complete.” (README introduction)
“Each program here is meant to showcase a specific issue and recommended fix in isolation.” (README introduction)
“☠️ Common Security Exploits and Protections on Solana” (repository tagline)
How to read the original
Section titled “How to read the original”Background needed: basic Rust and enough Anchor familiarity to read #[account(...)] constraint syntax, since the repository communicates primarily through paired insecure/secure code examples rather than prose. What to skip on a first pass: trying to read every example front-to-back — the repository is meant to be consulted per bug class, not read linearly. The hardest part for an Ethereum-background reader is recalibrating intuition about what “type safety” means: Rust’s compiler enforces types at compile time, but an Anchor Account<'info, T> is still checking a runtime byte-layout discriminator against attacker-supplied data, not verifying a genuine type-system guarantee the way it might first appear.
What changed since
Section titled “What changed since”- Anchor’s
Account<'info, T>wrapper,Signer<'info>type, and#[account(owner = ...)]constraint (documented in detail in the Helius guide summarized in §3) have become the default idiomatic fix for most of this repository’s bug classes, to the point that many are now caught automatically by using Anchor rather than requiring a developer to remember the manual check the repository demonstrates. - Real incidents postdating this repository’s core examples — Cashio (March 2022) and Crema Finance (July 2022), both covered in §6 — show that documentation and reference implementations existing is not the same as every deployed program using them; both incidents are squarely missing-owner-check-style bugs of exactly the kind this repository catalogs.
10. Sources
Section titled “10. Sources”- Two Rights Might Make A Wrong — samczsun (Paradigm) — 2021-08-17 — https://www.paradigm.xyz/writing/two-rights-might-make-a-wrong
- The Dangers of Surprising Code — samczsun (Paradigm) — 2021-08-13 — https://www.paradigm.xyz/writing/the-dangers-of-surprising-code
- Uncovering a Four Year Old Bug — samczsun (Paradigm) — 2021-04-19 — https://www.paradigm.xyz/writing/uncovering-a-four-year-old-bug
- A Hitchhiker’s Guide to Solana Program Security — Helius — n.d. — https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security
- sealevel-attacks — coral-xyz (GitHub) — accessed 2026-08-30 — https://github.com/coral-xyz/sealevel-attacks
- Solana Smart Contracts: Common Pitfalls and How to Avoid Them — Neodyme — 2021-08-20 — https://neodyme.io/en/blog/solana_common_pitfalls/
- The DAO hack (background, §6) — CoinDesk (secondary) — 2023-05-09 — https://www.coindesk.com/consensus-magazine/2023/05/09/coindesk-turns-10-how-the-dao-hack-changed-ethereum-and-crypto
- A Postmortem on the Parity Multi-Sig Library Self-Destruct — Parity Technologies — 2017-11-15 — https://medium.com/paritytech/a-postmortem-on-the-parity-multi-sig-library-self-destruct-63daca3a4cf7
- Explained: The Cream Finance Hack (August 2021) — Halborn (secondary) — 2021 — https://halborn.com/explained-the-cream-finance-hack-august-2021/
- BadgerDAO Reveals Details of How It Was Hacked for $120M — CoinDesk (secondary) — 2021-12-10 — https://www.coindesk.com/business/2021/12/10/badgerdao-reveals-details-of-how-it-was-hacked-for-120m
- Munchables Exploited for $62M — CoinDesk (secondary) — 2024-03-27 — https://www.coindesk.com/tech/2024/03/27/munchables-exploited-for-62m-ether-linked-to-rogue-north-korean-team-member
- Explained: The Cashio Hack (March 2022) — Halborn (secondary) — accessed 2026-08-30 — https://www.halborn.com/blog/post/explained-the-cashio-hack-march-2022
- Explained: The Crema Finance Hack (July 2022) — Halborn (secondary) — accessed 2026-08-30 — https://www.halborn.com/blog/post/explained-the-crema-finance-hack-july-2022
- FBI Confirms North Korea’s Lazarus Group as Bybit Hackers — Infosecurity Magazine (secondary) — 2025-02 — https://www.infosecurity-magazine.com/news/fbi-confirms-north-koreas-lazarus/