anchor-audit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +28 -0
  2. package/dist/auditor.d.ts +16 -0
  3. package/dist/auditor.js +235 -0
  4. package/dist/auditor.js.map +1 -0
  5. package/dist/index.d.ts +19 -0
  6. package/dist/index.js +96 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/metadata.d.ts +25 -0
  9. package/dist/metadata.js +48 -0
  10. package/dist/metadata.js.map +1 -0
  11. package/dist/reporter.d.ts +18 -0
  12. package/dist/reporter.js +177 -0
  13. package/dist/reporter.js.map +1 -0
  14. package/dist/rules-loader.d.ts +12 -0
  15. package/dist/rules-loader.js +65 -0
  16. package/dist/rules-loader.js.map +1 -0
  17. package/dist/scanner.d.ts +6 -0
  18. package/dist/scanner.js +42 -0
  19. package/dist/scanner.js.map +1 -0
  20. package/package.json +41 -0
  21. package/rules/001-missing-signer-check.md +57 -0
  22. package/rules/002-missing-owner-check.md +53 -0
  23. package/rules/003-missing-discriminator-check.md +53 -0
  24. package/rules/004-account-substitution.md +54 -0
  25. package/rules/005-sysvar-spoofing.md +57 -0
  26. package/rules/006-missing-rent-exemption-check.md +47 -0
  27. package/rules/007-account-aliasing.md +53 -0
  28. package/rules/008-uninitialized-account-use.md +53 -0
  29. package/rules/009-missing-mut-constraint.md +52 -0
  30. package/rules/010-missing-close-constraint.md +48 -0
  31. package/rules/011-pda-seed-collision.md +49 -0
  32. package/rules/012-missing-bump-validation.md +55 -0
  33. package/rules/013-non-canonical-bump-accepted.md +52 -0
  34. package/rules/014-predictable-pda.md +57 -0
  35. package/rules/015-insecure-pda-across-upgrades.md +54 -0
  36. package/rules/016-bump-mismatch.md +49 -0
  37. package/rules/017-arbitrary-cpi.md +56 -0
  38. package/rules/018-cpi-confused-deputy.md +50 -0
  39. package/rules/019-missing-program-id-check-spl.md +51 -0
  40. package/rules/020-reentrancy-via-cpi.md +50 -0
  41. package/rules/021-untrusted-callback.md +49 -0
  42. package/rules/022-cpi-with-attacker-accounts.md +58 -0
  43. package/rules/023-lamport-overflow.md +50 -0
  44. package/rules/024-token-amount-overflow.md +52 -0
  45. package/rules/025-precision-loss.md +42 -0
  46. package/rules/026-rounding-direction.md +43 -0
  47. package/rules/027-token-decimal-mismatch.md +50 -0
  48. package/rules/028-integer-cast-truncation.md +42 -0
  49. package/rules/029-off-by-one.md +45 -0
  50. package/rules/030-missing-authorization.md +51 -0
  51. package/rules/031-reinitialization-attack.md +50 -0
  52. package/rules/032-closed-account-revival.md +49 -0
  53. package/rules/033-init-if-needed-misuse.md +66 -0
  54. package/rules/034-missing-has-one.md +55 -0
  55. package/rules/035-insecure-admin-transfer.md +49 -0
  56. package/rules/036-missing-pause-guards.md +50 -0
  57. package/rules/037-clock-manipulation.md +53 -0
  58. package/rules/038-missing-address-validation.md +53 -0
  59. package/rules/039-constraint-evaluation-stage.md +54 -0
  60. package/rules/040-realloc-zero-init.md +53 -0
  61. package/rules/041-missing-payer-on-init.md +59 -0
  62. package/rules/042-incorrect-space-allocation.md +53 -0
  63. package/rules/043-account-vs-account-info.md +53 -0
  64. package/rules/044-token-account-owner-unverified.md +56 -0
  65. package/rules/045-token-mint-unverified.md +58 -0
  66. package/rules/046-ata-assumption-errors.md +53 -0
  67. package/rules/047-token-program-id-hardcoded.md +55 -0
  68. package/rules/048-compute-budget-abuse.md +51 -0
  69. package/rules/049-log-spam-dos.md +49 -0
  70. package/rules/050-stack-overflow-deep-cpi.md +48 -0
  71. package/rules/INDEX.md +65 -0
  72. package/rules/README.md +40 -0
@@ -0,0 +1,53 @@
1
+ # Rule 043: `Account` vs `AccountInfo` Misuse
2
+
3
+ **Severity:** High
4
+ **Category:** Constraints
5
+
6
+ ## Description
7
+ Choosing `AccountInfo<'info>` / `UncheckedAccount<'info>` where `Account<'info, T>` (or a typed wrapper like `Program`, `Signer`, `Sysvar`, `InterfaceAccount`) belongs throws away Anchor's automatic validation: owner check, discriminator check, and deserialization. Reaching for the untyped variant "to make it compile" silently disables the protections that prevent owner-spoofing (Rule 002) and type-cosplay (Rule 003).
8
+
9
+ ## Vulnerable pattern
10
+ ```rust
11
+ #[derive(Accounts)]
12
+ pub struct Use<'info> {
13
+ /// CHECK: it's our config
14
+ pub config: AccountInfo<'info>, // no owner/discriminator/type check
15
+ pub user: Signer<'info>,
16
+ }
17
+
18
+ pub fn use_config(ctx: Context<Use>) -> Result<()> {
19
+ let config = Config::try_from_slice(&ctx.accounts.config.data.borrow())?; // unsafe
20
+ require_keys_eq!(config.admin, ctx.accounts.user.key());
21
+ Ok(())
22
+ }
23
+ ```
24
+
25
+ ## Why this is dangerous
26
+ With `AccountInfo`, nothing verifies that `config` is owned by this program or is actually a `Config` — the attacker forges its bytes (Rule 002) or passes a different account type with a compatible layout (Rule 003). The single type choice is the difference between Anchor enforcing three invariants and the program enforcing none.
27
+
28
+ ## Fix pattern
29
+ ```rust
30
+ #[derive(Accounts)]
31
+ pub struct Use<'info> {
32
+ // Account<'info, Config> checks owner == program ID, discriminator, and
33
+ // deserializes safely.
34
+ #[account(has_one = admin)]
35
+ pub config: Account<'info, Config>,
36
+ pub admin: Signer<'info>,
37
+ }
38
+ ```
39
+ Reserve `AccountInfo`/`UncheckedAccount` for genuinely opaque accounts, and when used, document the manual checks in the `/// CHECK:` comment and perform them.
40
+
41
+ ## Detection heuristic
42
+ - `AccountInfo`/`UncheckedAccount` whose data is deserialized into a known program type
43
+ - `/// CHECK:` comments that don't describe a real, performed validation
44
+ - Program/token/sysvar accounts typed as `AccountInfo` instead of `Program`/`Account<TokenAccount>`/`Sysvar`
45
+ - Typed wrappers avoided "to fix a lifetime/borrow error" without restoring the checks manually
46
+
47
+ ## References
48
+ - Anchor docs — account types (https://www.anchor-lang.com/docs/account-types)
49
+ - The Anchor Book — AccountInfo and UncheckedAccount (https://book.anchor-lang.com/anchor_in_depth/the_accounts_struct.html)
50
+ - Neodyme — Solana common pitfalls (https://neodyme.io/en/blog/solana_common_pitfalls/)
51
+
52
+ ## Real-world exploits (if any)
53
+ No single attributed exploit for the type choice alone; it is the enabling mistake behind owner-check and type-cosplay exploits (Rules 002, 003) repeatedly flagged in audits.
@@ -0,0 +1,56 @@
1
+ # Rule 044: Token Account Owner Unverified
2
+
3
+ **Severity:** High
4
+ **Category:** SPL Token
5
+
6
+ ## Description
7
+ An SPL token account has an `owner` field (the wallet/PDA authority that controls it) distinct from the account's program owner. Code that accepts a token account and acts on it — crediting a user, treating it as a vault — without verifying its `owner` matches the expected authority lets an attacker pass a token account controlled by someone else, or by the program when it shouldn't be.
8
+
9
+ ## Vulnerable pattern
10
+ ```rust
11
+ #[derive(Accounts)]
12
+ pub struct Stake<'info> {
13
+ pub user: Signer<'info>,
14
+ // No check that user_token.owner == user.key()
15
+ #[account(mut)]
16
+ pub user_token: Account<'info, TokenAccount>,
17
+ #[account(mut)]
18
+ pub stake_account: Account<'info, StakeAccount>,
19
+ }
20
+
21
+ pub fn stake(ctx: Context<Stake>, amount: u64) -> Result<()> {
22
+ // Credits the signer based on a token account they may not own
23
+ ctx.accounts.stake_account.staked += amount;
24
+ Ok(())
25
+ }
26
+ ```
27
+
28
+ ## Why this is dangerous
29
+ The attacker references a token account they don't control (or whose balance they can't actually move) to claim staking credit, or substitutes the program's own vault as the "source" to get credited without depositing. The token account's `owner`/`authority` must be tied to the principal the instruction credits or debits.
30
+
31
+ ## Fix pattern
32
+ ```rust
33
+ #[derive(Accounts)]
34
+ pub struct Stake<'info> {
35
+ pub user: Signer<'info>,
36
+ #[account(mut, token::authority = user)] // owner must be `user`
37
+ pub user_token: Account<'info, TokenAccount>,
38
+ #[account(mut, has_one = user)]
39
+ pub stake_account: Account<'info, StakeAccount>,
40
+ }
41
+ ```
42
+ Use `token::authority = ...` (or check `token_account.owner == expected`) on every token account whose control matters.
43
+
44
+ ## Detection heuristic
45
+ - `Account<'info, TokenAccount>` used in logic without `token::authority` / an `owner` equality check
46
+ - Crediting/debiting a principal based on a token account not constrained to that principal
47
+ - Vault/source token accounts not pinned via `token::authority = <pda>` or `address =`
48
+ - `.owner` of a token account read but compared to nothing
49
+
50
+ ## References
51
+ - anchor_spl docs — token::authority constraint (https://docs.rs/anchor-spl/latest/anchor_spl/)
52
+ - SPL Token docs — account owner vs authority (https://spl.solana.com/token)
53
+ - Neodyme — Solana common pitfalls (https://neodyme.io/en/blog/solana_common_pitfalls/)
54
+
55
+ ## Real-world exploits (if any)
56
+ No single attributed public headline exploit; unverified token-account ownership is a frequent high-severity audit finding in staking and vault programs.
@@ -0,0 +1,58 @@
1
+ # Rule 045: Token Mint Unverified
2
+
3
+ **Severity:** High
4
+ **Category:** SPL Token
5
+
6
+ ## Description
7
+ A token account is tied to exactly one mint, but code that accepts "a token account" without constraining its `mint` allows an attacker to pass an account for a *different*, worthless mint. The program credits or values the deposit as if it were the expected (valuable) token, because it never checked which token the account actually holds.
8
+
9
+ ## Vulnerable pattern
10
+ ```rust
11
+ #[derive(Accounts)]
12
+ pub struct Deposit<'info> {
13
+ pub user: Signer<'info>,
14
+ // No check that user_token.mint == expected USDC mint
15
+ #[account(mut)]
16
+ pub user_token: Account<'info, TokenAccount>,
17
+ #[account(mut)]
18
+ pub vault: Account<'info, TokenAccount>,
19
+ }
20
+
21
+ pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
22
+ // Transfers `amount` of WHATEVER mint user_token holds, credits as USDC
23
+ token::transfer(/* user_token -> vault */, amount)?;
24
+ ctx.accounts.position.usdc_balance += amount;
25
+ Ok(())
26
+ }
27
+ ```
28
+
29
+ ## Why this is dangerous
30
+ The attacker mints a worthless token, deposits it, and is credited a USDC balance one-for-one, then withdraws real USDC. Without a mint check, the program cannot tell a valuable token from a fake one. The transfer must also target a vault of the *same* mint, or accounting diverges from the actual held assets.
31
+
32
+ ## Fix pattern
33
+ ```rust
34
+ #[derive(Accounts)]
35
+ pub struct Deposit<'info> {
36
+ pub user: Signer<'info>,
37
+ #[account(mut, token::mint = usdc_mint, token::authority = user)]
38
+ pub user_token: Account<'info, TokenAccount>,
39
+ #[account(mut, token::mint = usdc_mint)]
40
+ pub vault: Account<'info, TokenAccount>,
41
+ #[account(address = config.usdc_mint)]
42
+ pub usdc_mint: Account<'info, Mint>,
43
+ }
44
+ ```
45
+
46
+ ## Detection heuristic
47
+ - Token accounts used without a `token::mint = ...` constraint or `.mint` equality check
48
+ - Deposit/withdraw/swap logic that credits a specific asset from an unconstrained token account
49
+ - Vault and user token accounts not constrained to the *same* expected mint
50
+ - A `Mint` account referenced but never pinned via `address =` to the configured mint
51
+
52
+ ## References
53
+ - anchor_spl docs — token::mint constraint (https://docs.rs/anchor-spl/latest/anchor_spl/)
54
+ - SPL Token docs — token account mint binding (https://spl.solana.com/token)
55
+ - Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
56
+
57
+ ## Real-world exploits (if any)
58
+ No single attributed public headline exploit; missing mint validation is a recurring high/critical finding in audits of deposit-based DeFi programs.
@@ -0,0 +1,53 @@
1
+ # Rule 046: Associated Token Account Assumption Errors
2
+
3
+ **Severity:** Medium
4
+ **Category:** SPL Token
5
+
6
+ ## Description
7
+ Code assumes a token account is *the* canonical associated token account (ATA) for a given wallet+mint — correctly derived, already existing, and the only one — without enforcing it. A wallet can hold many token accounts for the same mint, and only the ATA is at the deterministic derived address. Trusting an unconstrained "ATA" lets an attacker pass a non-canonical token account, and assuming existence causes failures or, worse, silent misrouting.
8
+
9
+ ## Vulnerable pattern
10
+ ```rust
11
+ #[derive(Accounts)]
12
+ pub struct Payout<'info> {
13
+ /// CHECK: assumed to be the recipient's ATA, but not derived/verified
14
+ #[account(mut)]
15
+ pub recipient_ata: AccountInfo<'info>,
16
+ pub recipient: SystemAccount<'info>,
17
+ pub mint: Account<'info, Mint>,
18
+ // ...
19
+ }
20
+ ```
21
+
22
+ ## Why this is dangerous
23
+ The attacker passes a token account they control (for the right mint but at a non-ATA address, or owned by a different wallet) as `recipient_ata`, redirecting a payout. Conversely, assuming the ATA already exists makes the instruction fail when it doesn't, bricking the flow — or pushes developers to create it without checking who pays. ATA identity must be derived and checked, not assumed.
24
+
25
+ ## Fix pattern
26
+ ```rust
27
+ #[derive(Accounts)]
28
+ pub struct Payout<'info> {
29
+ #[account(
30
+ mut,
31
+ associated_token::mint = mint,
32
+ associated_token::authority = recipient, // enforces canonical ATA
33
+ )]
34
+ pub recipient_ata: Account<'info, TokenAccount>,
35
+ pub recipient: SystemAccount<'info>,
36
+ pub mint: Account<'info, Mint>,
37
+ // For creation: use `init` with associated_token::* + the ATA program.
38
+ }
39
+ ```
40
+
41
+ ## Detection heuristic
42
+ - Accounts named `*_ata` typed as `AccountInfo` or `TokenAccount` without `associated_token::*` constraints
43
+ - Token destinations assumed canonical without `associated_token::mint`/`authority` (or a `get_associated_token_address` comparison)
44
+ - Code that assumes an ATA exists with no `init`/`init_if_needed` or existence handling
45
+ - ATA derivation done off-chain and trusted on-chain without re-derivation
46
+
47
+ ## References
48
+ - anchor_spl docs — associated_token constraints (https://docs.rs/anchor-spl/latest/anchor_spl/associated_token/)
49
+ - SPL Associated Token Account program docs (https://spl.solana.com/associated-token-account)
50
+ - Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
51
+
52
+ ## Real-world exploits (if any)
53
+ No single attributed public exploit; ATA assumption errors are common medium audit findings, especially payout redirection via non-canonical token accounts.
@@ -0,0 +1,55 @@
1
+ # Rule 047: Token Program ID Hardcoded vs. Validated
2
+
3
+ **Severity:** Medium
4
+ **Category:** SPL Token
5
+
6
+ ## Description
7
+ With SPL Token and Token-2022 both in use, a program must be deliberate about which token program it targets. Two failure modes: (1) hardcoding `spl_token::ID` while accepting mints/accounts that actually belong to Token-2022 (or vice-versa), causing CPI failures or wrong assumptions; and (2) accepting the token program as an unvalidated account so the wrong (or fake) program is used. The program ID must match the program that actually owns the token accounts involved.
8
+
9
+ ## Vulnerable pattern
10
+ ```rust
11
+ pub fn transfer_out(ctx: Context<TransferOut>, amount: u64) -> Result<()> {
12
+ // Hardcodes legacy SPL Token, but the mint is a Token-2022 mint, so the
13
+ // accounts are owned by the Token-2022 program — this CPI targets the
14
+ // wrong program ID for these accounts.
15
+ let cpi = CpiContext::new(
16
+ ctx.accounts.token_program.to_account_info(), // assumed spl_token::ID
17
+ Transfer { /* ... */ },
18
+ );
19
+ token::transfer(cpi, amount)?;
20
+ Ok(())
21
+ }
22
+ ```
23
+
24
+ ## Why this is dangerous
25
+ Mismatching the token program against the token accounts' actual owner program makes transfers fail (availability) or, when combined with unvalidated program accounts, lets a fake token program be injected (Rule 019). Hardcoding one variant silently breaks compatibility for the other and can be steered by an attacker choosing a Token-2022 mint where SPL Token is assumed.
26
+
27
+ ## Fix pattern
28
+ ```rust
29
+ #[derive(Accounts)]
30
+ pub struct TransferOut<'info> {
31
+ // Interface accepts either SPL Token or Token-2022, verified to match
32
+ // the accounts' owning program.
33
+ pub token_program: Interface<'info, TokenInterface>,
34
+ #[account(mut)]
35
+ pub from: InterfaceAccount<'info, TokenAccount>,
36
+ #[account(mut)]
37
+ pub to: InterfaceAccount<'info, TokenAccount>,
38
+ pub authority: Signer<'info>,
39
+ }
40
+ // Use anchor_spl::token_interface CPIs so the call routes to the correct program.
41
+ ```
42
+
43
+ ## Detection heuristic
44
+ - Hardcoded `spl_token::ID` / `Program<'info, Token>` while the program intends to support Token-2022 (or vice-versa)
45
+ - Token accounts as `Account<TokenAccount>` (legacy) when mints may be Token-2022 — use `InterfaceAccount`
46
+ - Token program passed as `AccountInfo` without an ID check (overlaps Rule 019)
47
+ - No verification that `token_program.key()` equals the owner program of the passed token accounts
48
+
49
+ ## References
50
+ - anchor_spl docs — token_interface / Interface (https://docs.rs/anchor-spl/latest/anchor_spl/token_interface/)
51
+ - SPL Token-2022 docs (https://spl.solana.com/token-2022)
52
+ - Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
53
+
54
+ ## Real-world exploits (if any)
55
+ No single attributed public exploit; token-program mismatch and Token-2022 compatibility gaps are increasingly common medium audit findings.
@@ -0,0 +1,51 @@
1
+ # Rule 048: Compute Budget Abuse (Unbounded Work)
2
+
3
+ **Severity:** Medium
4
+ **Category:** Runtime
5
+
6
+ ## Description
7
+ Each Solana transaction has a compute-unit budget. An instruction that iterates over an attacker-controllable, unbounded collection — `remaining_accounts`, a `Vec` field that can grow without limit, or a loop whose count comes from input — can be pushed to exceed the budget and fail. If a critical operation (liquidation, settlement, crank) lives behind such a loop, the attacker grows the data until the operation can no longer complete, a denial-of-service.
8
+
9
+ ## Vulnerable pattern
10
+ ```rust
11
+ pub fn distribute(ctx: Context<Distribute>) -> Result<()> {
12
+ // `holders` can grow unboundedly as users join; eventually this loop
13
+ // exceeds the compute budget and every distribute() call fails.
14
+ for holder in ctx.accounts.registry.holders.iter() {
15
+ pay(holder)?;
16
+ }
17
+ Ok(())
18
+ }
19
+ ```
20
+
21
+ ## Why this is dangerous
22
+ Once the collection is large enough, the instruction always runs out of compute and reverts, permanently bricking the path. If liquidations or withdrawals depend on it, funds can be frozen. An attacker may intentionally inflate the collection (cheap entries) to trigger the DoS, or simply rely on organic growth crossing the limit.
23
+
24
+ ## Fix pattern
25
+ ```rust
26
+ // Paginate / bound the work per call, tracking progress in state:
27
+ pub fn distribute(ctx: Context<Distribute>, start: u32, count: u32) -> Result<()> {
28
+ require!(count <= MAX_PER_CALL, ErrorCode::BatchTooLarge);
29
+ let end = (start + count).min(ctx.accounts.registry.holders.len() as u32);
30
+ for i in start..end {
31
+ pay(&ctx.accounts.registry.holders[i as usize])?;
32
+ }
33
+ ctx.accounts.registry.cursor = end;
34
+ Ok(())
35
+ }
36
+ ```
37
+ Cap collection sizes at insertion and design cranks to process bounded batches.
38
+
39
+ ## Detection heuristic
40
+ - Loops over `remaining_accounts`, `Vec`/`String` fields, or input-controlled counts with no per-call cap
41
+ - Account structs holding unbounded collections that are iterated in a single instruction
42
+ - Critical operations (liquidate, settle, distribute) gated behind whole-collection iteration
43
+ - No pagination/cursor for processing large datasets
44
+
45
+ ## References
46
+ - Solana docs — compute budget and limits (https://solana.com/docs/core/fees#compute-budget)
47
+ - Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
48
+ - Sec3 — denial-of-service via compute exhaustion (https://www.sec3.dev/blog)
49
+
50
+ ## Real-world exploits (if any)
51
+ No single attributed public exploit; unbounded-iteration DoS is a recognized medium audit finding for registries, reward distributors, and cranks.
@@ -0,0 +1,49 @@
1
+ # Rule 049: Log Spam / Excessive Logging DoS
2
+
3
+ **Severity:** Low
4
+ **Category:** Runtime
5
+
6
+ ## Description
7
+ `msg!` and other logging consume compute units and contribute to transaction log size, which is itself bounded. Logging attacker-controlled or unbounded data (a user-supplied string, every element of a collection, large buffers) lets an attacker inflate compute/log usage to make an instruction fail, or simply wastes the budget so legitimately-needed work no longer fits. Logging inside hot loops compounds the cost.
8
+
9
+ ## Vulnerable pattern
10
+ ```rust
11
+ pub fn record(ctx: Context<Record>, memo: String) -> Result<()> {
12
+ // Attacker-controlled, unbounded string logged verbatim; large memos
13
+ // burn compute and bloat logs, and can push the tx over its limits.
14
+ msg!("memo: {}", memo);
15
+ for item in ctx.accounts.list.items.iter() {
16
+ msg!("item: {:?}", item); // logging in a loop multiplies the cost
17
+ }
18
+ Ok(())
19
+ }
20
+ ```
21
+
22
+ ## Why this is dangerous
23
+ Excessive logging can exhaust the compute budget (causing the instruction to fail) and inflate transaction logs, degrading RPC/indexer performance for everyone reading the program's output. When a required instruction logs unbounded input, an attacker can deny its use; even absent an attacker, it raises costs and can intermittently break the path.
24
+
25
+ ## Fix pattern
26
+ ```rust
27
+ pub fn record(ctx: Context<Record>, memo: String) -> Result<()> {
28
+ require!(memo.len() <= MAX_MEMO_LEN, ErrorCode::MemoTooLong);
29
+ // Avoid logging raw user data; log bounded, structured summaries only.
30
+ msg!("record: memo_len={}", memo.len());
31
+ // Do not log inside large loops; emit a single summary if needed.
32
+ Ok(())
33
+ }
34
+ ```
35
+ Prefer structured Anchor events (`emit!`) over verbose `msg!`, and never log full attacker-controlled buffers.
36
+
37
+ ## Detection heuristic
38
+ - `msg!` formatting attacker-controlled strings/buffers without a length cap
39
+ - Logging inside loops over collections or `remaining_accounts`
40
+ - Verbose debug logging (`{:?}` on large structs) left in production paths
41
+ - No bound on user-supplied fields that are subsequently logged
42
+
43
+ ## References
44
+ - Solana docs — program logging and limits (https://solana.com/docs/programs/debugging#logging)
45
+ - Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
46
+ - Sec3 — compute and log DoS (https://www.sec3.dev/blog)
47
+
48
+ ## Real-world exploits (if any)
49
+ No single attributed public exploit; log/compute spam is a low-severity hygiene and availability finding in audits.
@@ -0,0 +1,48 @@
1
+ # Rule 050: Stack / CPI Depth Exhaustion
2
+
3
+ **Severity:** Low
4
+ **Category:** Runtime
5
+
6
+ ## Description
7
+ The Solana runtime caps cross-program invocation depth (CPI calls can nest only a limited number of levels) and each program has a bounded stack frame size (large stack-allocated structures/arrays can blow the frame). Designs that chain many CPIs, or that place large data on the stack (especially recursively or in deep call chains), can hit these limits and fail at runtime — bricking instructions that depend on the full chain.
8
+
9
+ ## Vulnerable pattern
10
+ ```rust
11
+ // Deeply nested CPI chain: A -> B -> C -> D ... approaching the CPI depth
12
+ // limit. If the chain needs one more hop than allowed, the whole flow fails.
13
+ pub fn route(ctx: Context<Route>) -> Result<()> {
14
+ invoke(&next_program_ix, accounts)?; // which itself CPIs further down
15
+ Ok(())
16
+ }
17
+
18
+ // Large stack allocation inside a call that's already deep in a CPI chain:
19
+ let buffer = [0u8; 16_384]; // big stack frame, risks stack overflow
20
+ ```
21
+
22
+ ## Why this is dangerous
23
+ If an operation's success depends on a CPI chain near the depth limit, any addition (a new integration, a wrapper) tips it over and the instruction reverts — an availability failure that can freeze dependent funds. Oversized stack frames in deep chains can abort the program. Attackers may also craft inputs that force the deepest path.
24
+
25
+ ## Fix pattern
26
+ ```rust
27
+ // Keep CPI chains shallow; flatten orchestration into the top-level program
28
+ // rather than relaying through intermediate programs where avoidable.
29
+ // Move large data off the stack onto the heap or into accounts:
30
+ let buffer = vec![0u8; 16_384]; // heap-allocated; small stack frame
31
+
32
+ // Process multi-step flows as separate top-level instructions instead of
33
+ // one deep nested chain.
34
+ ```
35
+
36
+ ## Detection heuristic
37
+ - CPI chains that relay through several intermediate programs (each adding a level)
38
+ - Large fixed-size arrays/structs allocated on the stack (`[u8; N]`, big local structs), especially in deep call paths or recursion
39
+ - Recursive program logic without a strict depth bound
40
+ - Designs assuming an arbitrary number of nested CPIs will succeed
41
+
42
+ ## References
43
+ - Solana docs — CPI depth and stack limits (https://solana.com/docs/core/cpi)
44
+ - Solana docs — program runtime limits (https://docs.solanalabs.com/runtime/programming-model/runtime)
45
+ - Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
46
+
47
+ ## Real-world exploits (if any)
48
+ No single attributed public exploit; depth/stack limits surface as availability/robustness findings in audits of programs with deep integration chains.
package/rules/INDEX.md ADDED
@@ -0,0 +1,65 @@
1
+ # Rule index
2
+
3
+ 50 rules across 8 categories. Severity reflects typical impact when the pattern is present and exploitable; always judge in context.
4
+
5
+ | ID | Rule | Severity | Category |
6
+ |----|------|----------|----------|
7
+ | 001 | [Missing signer check](./001-missing-signer-check.md) | Critical | Account validation |
8
+ | 002 | [Missing owner check](./002-missing-owner-check.md) | Critical | Account validation |
9
+ | 003 | [Missing discriminator check (type cosplay)](./003-missing-discriminator-check.md) | High | Account validation |
10
+ | 004 | [Account substitution](./004-account-substitution.md) | High | Account validation |
11
+ | 005 | [Sysvar spoofing](./005-sysvar-spoofing.md) | High | Account validation |
12
+ | 006 | [Missing rent-exemption check](./006-missing-rent-exemption-check.md) | Low | Account validation |
13
+ | 007 | [Account aliasing (duplicate mutable accounts)](./007-account-aliasing.md) | High | Account validation |
14
+ | 008 | [Uninitialized account use](./008-uninitialized-account-use.md) | High | Account validation |
15
+ | 009 | [Missing `mut` constraint](./009-missing-mut-constraint.md) | Medium | Account validation |
16
+ | 010 | [Missing or improper close constraint](./010-missing-close-constraint.md) | Medium | Account validation |
17
+ | 011 | [PDA seed collision](./011-pda-seed-collision.md) | High | PDA |
18
+ | 012 | [Missing bump validation](./012-missing-bump-validation.md) | Medium | PDA |
19
+ | 013 | [Non-canonical bump accepted](./013-non-canonical-bump-accepted.md) | High | PDA |
20
+ | 014 | [Predictable / attacker-controlled PDA seeds](./014-predictable-pda.md) | High | PDA |
21
+ | 015 | [Insecure PDA layout across upgrades](./015-insecure-pda-across-upgrades.md) | Medium | PDA |
22
+ | 016 | [Stored bump mismatch](./016-bump-mismatch.md) | Medium | PDA |
23
+ | 017 | [Arbitrary CPI (unvalidated target)](./017-arbitrary-cpi.md) | Critical | CPI |
24
+ | 018 | [CPI confused deputy](./018-cpi-confused-deputy.md) | High | CPI |
25
+ | 019 | [Missing program ID check on SPL CPIs](./019-missing-program-id-check-spl.md) | High | CPI |
26
+ | 020 | [Reentrancy via CPI](./020-reentrancy-via-cpi.md) | High | CPI |
27
+ | 021 | [Untrusted callback execution](./021-untrusted-callback.md) | High | CPI |
28
+ | 022 | [CPI invoked with attacker-controlled accounts](./022-cpi-with-attacker-accounts.md) | High | CPI |
29
+ | 023 | [Lamport arithmetic overflow / underflow](./023-lamport-overflow.md) | High | Math |
30
+ | 024 | [Token amount arithmetic overflow](./024-token-amount-overflow.md) | High | Math |
31
+ | 025 | [Precision loss (division before multiplication)](./025-precision-loss.md) | Medium | Math |
32
+ | 026 | [Incorrect rounding direction](./026-rounding-direction.md) | Medium | Math |
33
+ | 027 | [Token decimal mismatch](./027-token-decimal-mismatch.md) | Medium | Math |
34
+ | 028 | [Integer cast truncation](./028-integer-cast-truncation.md) | Medium | Math |
35
+ | 029 | [Off-by-one errors](./029-off-by-one.md) | Low | Math |
36
+ | 030 | [Missing authorization on privileged instruction](./030-missing-authorization.md) | Critical | Auth |
37
+ | 031 | [Reinitialization attack](./031-reinitialization-attack.md) | High | Auth |
38
+ | 032 | [Closed account revival](./032-closed-account-revival.md) | High | Auth |
39
+ | 033 | [`init_if_needed` misuse](./033-init-if-needed-misuse.md) | High | Auth |
40
+ | 034 | [Missing `has_one` relationship enforcement](./034-missing-has-one.md) | High | Auth |
41
+ | 035 | [Insecure admin transfer (no acceptance handshake)](./035-insecure-admin-transfer.md) | Medium | Auth |
42
+ | 036 | [Missing pause / freeze guards](./036-missing-pause-guards.md) | Low | Auth |
43
+ | 037 | [Clock / time-based logic without bounds](./037-clock-manipulation.md) | Medium | Auth |
44
+ | 038 | [Missing `address` validation on fixed-identity accounts](./038-missing-address-validation.md) | Medium | Constraints |
45
+ | 039 | [Constraint evaluation stage (pre- vs post-state)](./039-constraint-evaluation-stage.md) | Medium | Constraints |
46
+ | 040 | [`realloc` without zero-init](./040-realloc-zero-init.md) | Medium | Constraints |
47
+ | 041 | [`init` without `payer` (or wrong payer)](./041-missing-payer-on-init.md) | Low | Constraints |
48
+ | 042 | [Incorrect `space` allocation](./042-incorrect-space-allocation.md) | Medium | Constraints |
49
+ | 043 | [`Account` vs `AccountInfo` misuse](./043-account-vs-account-info.md) | High | Constraints |
50
+ | 044 | [Token account owner unverified](./044-token-account-owner-unverified.md) | High | SPL Token |
51
+ | 045 | [Token mint unverified](./045-token-mint-unverified.md) | High | SPL Token |
52
+ | 046 | [Associated token account assumption errors](./046-ata-assumption-errors.md) | Medium | SPL Token |
53
+ | 047 | [Token program ID hardcoded vs. validated](./047-token-program-id-hardcoded.md) | Medium | SPL Token |
54
+ | 048 | [Compute budget abuse (unbounded work)](./048-compute-budget-abuse.md) | Medium | Runtime |
55
+ | 049 | [Log spam / excessive logging DoS](./049-log-spam-dos.md) | Low | Runtime |
56
+ | 050 | [Stack / CPI depth exhaustion](./050-stack-overflow-deep-cpi.md) | Low | Runtime |
57
+
58
+ ## Severity counts
59
+
60
+ | Severity | Count |
61
+ |----------|-------|
62
+ | Critical | 4 |
63
+ | High | 21 |
64
+ | Medium | 17 |
65
+ | Low | 8 |
@@ -0,0 +1,40 @@
1
+ # Rule catalog
2
+
3
+ One markdown file per rule, named `NNN-kebab-case-name.md` (e.g. `001-missing-signer-check.md`). The full list with severities lives in [INDEX.md](./INDEX.md).
4
+
5
+ The catalog is consumed by both the Claude Code skill ([SKILL.md](../SKILL.md)) and the CLI (`cli/src/rules-loader.ts`), so every file must follow this exact template:
6
+
7
+ ````markdown
8
+ # Rule NNN: <Title>
9
+
10
+ **Severity:** Critical | High | Medium | Low
11
+ **Category:** Account validation | PDA | CPI | Math | Auth | Constraints | SPL Token | Runtime
12
+
13
+ ## Description
14
+ One paragraph explaining the vulnerability and why it matters.
15
+
16
+ ## Vulnerable pattern
17
+ ```rust
18
+ // Minimal Rust/Anchor snippet showing the bug
19
+ ```
20
+
21
+ ## Why this is dangerous
22
+ Explain attacker action and impact in 2 to 4 sentences.
23
+
24
+ ## Fix pattern
25
+ ```rust
26
+ // Minimal Rust/Anchor snippet showing the corrected code
27
+ ```
28
+
29
+ ## Detection heuristic
30
+ Bullet list of things an agent should look for in source code to flag this rule.
31
+
32
+ ## References
33
+ - Source 1 (URL)
34
+ - Source 2 (URL)
35
+
36
+ ## Real-world exploits (if any)
37
+ Optional: brief mention of public exploits matching this pattern.
38
+ ````
39
+
40
+ Content must be sourced (Neodyme, Sec3, Helius, Solana Cookbook, Cyfrin Updraft, the Anchor book, public audit reports) and cited in the References section — never invented.