Skip to content
Security
Skill

/web3-bug-classes

Complete reference for all 10 DeFi smart contract bug classes. Use this when hunting for specific vulnerability types, need attack patterns for accounting desync, access control, incomplete path, off-by-one, oracle manipulation, ERC4626 vaults, reentrancy, flash loans, signature

BOOST
From plugin
tradecatlabs-vibe-coding-cn
17k18 skills
Install
$ npx -y skills add tradecatlabs/vibe-coding-cn --skill web3-bug-classes --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/web3-bug-classes

Context preview

The summary Claude sees to decide when to auto-load this skill.

Complete reference for all 10 DeFi smart contract bug classes. Use this when hunting for specific vulnerability types, need attack patterns for accounting desync, access control, incomplete path, off-by-one, oracle manipulation, ERC4626 vaults, reentrancy, flash loans, signature

SKILL.md

web3-bug-classes.SKILL.md
name: web3-bug-classes
description: Complete reference for all 10 DeFi smart contract bug classes. Use this when hunting for specific vulnerability types, need attack patterns for accounting desync, access control, incomplete path, off-by-one, oracle manipulation, ERC4626 vaults, reentrancy, flash loans, signature replay, or proxy/upgrade bugs.

BUG CLASSES — DeFi Smart Contract Vulnerabilities

10 bug classes. Each one with root cause, vulnerable code, fix, grep patterns, and real paid examples.

---

1. ACCOUNTING STATE DESYNCHRONIZATION

> #1 Critical bug class — 28% of all Criticals on Immunefi. > Real protocols: Yeet, Alchemix V3, Folks Finance, ResupplyFi, MetaPool

What It Is

Two state variables are supposed to stay in sync. One code path updates variable A but forgets variable B. Later code reads both and makes decisions based on the stale B.

Real Value = A - B
If A is updated but B isn't → Real Value appears larger than it is → phantom value

Root Cause Pattern

// BEFORE (correct state):
// aToken.balanceOf(this) = 1000  (principal + yield)
// totalSupply = 1000              (only principal)
// yield = 1000 - 1000 = 0        ✓ correct

// Attacker triggers startUnstake:
totalSupply -= amount;  // decremented BEFORE transfer
// totalSupply = 900 now
// aToken.balanceOf still = 1000
// yield appears = 1000 - 900 = 100 (PHANTOM)

// Now harvest():
yieldAmount = aToken.balanceOf(this) - totalSupply;
// = 1000 - 900 = 100 (phantom yield — no real yield was earned)
// Protocol harvests 100 of principal and distributes as "yield"

Variants

**Variant 1: Phantom Yield** — totalSupply decremented before transfer

// Yeet protocol (35 duplicate reports):
function startUnstake(uint256 amount) external {
    totalSupply -= amount;  // decremented here, transfer happens later
    // balanceOf(this) - totalSupply now shows phantom yield
}

**Variant 2: Fast Path Skips State Update** — early return bypasses critical updates

// Alchemix V3 claimRedemption:
function claimRedemption(uint256 tokenId) external {
    if (transmuter.balance >= amount) {
        transmuter.transfer(user, amount);
        _burn(tokenId);
        return;  // EARLY RETURN — cumulativeEarmarked, _redemptionWeight, totalDebt never updated
    }
    // SLOW PATH: updates all state vars correctly
    alchemist.redeem(...);
}

**Variant 3: Rewards Accrue to Wrong Accumulator**

// Folks Finance Liquid Staking:
function addRewards(uint256 amount) external {
    algoBalance += amount;        // rewards go here
    // MISSING: TOTAL_ACTIVE_STAKE += amount
}
function withdraw(uint256 shares) external {
    uint256 myAmount = (shares * TOTAL_ACTIVE_STAKE) / totalSupply;
    // TOTAL_ACTIVE_STAKE never got rewards → underflow → freeze
}

**Variant 4: Update Happens in Wrong Order**

// Alchemix:
function deposit(uint256 amount) external {
    _shares = (amount * totalShares) / totalAssets;  // calculated BEFORE deposit
    totalAssets += amount;   // assets added AFTER shares calculated
    totalShares += _shares;  // shares calculation used stale totalAssets → wrong rate
}

Grep Patterns

# List all balance/supply variables
grep -rn "totalSupply\|totalShares\|totalAssets\|totalDebt\|totalCollateral\|cumulativeReward\|rewardPerShare" contracts/ | grep -v "//\|test"

# Find ALL writes to key variables
grep -rn "totalSupply\s*[-+*]=[^=]\|totalSupply\s*=" contracts/
grep -rn "cumulativeRewardPerShare\s*[-+*]=" contracts/

# Find all early returns in claim/redeem functions
grep -rn "\breturn\b" contracts/ -B3 | grep -B3 "if\b"
# For each early return: which state updates are in the normal path but not this one?

Kill Signals

  • Only one variable is involved (no pair to desync)
  • Both paths update all state vars identically
  • Transfer happens AFTER state update in every path (correct CEI)
  • Single-transaction atomicity prevents the window (no intermediate state visible)

Real Paid Examples

| Protocol | Root Cause | |----------|-----------| | Yeet | `startUnstake` decrements totalSupply before transfer → phantom yield | | Alchemix V3 | `claimRedemption` fast path skips 3 state updates → phantom collateral | | Folks Finance | Rewards accrue to `algoBalance` not `TOTAL_ACTIVE_STAKE` → underflow | | ResupplyFi | ERC4626 near-empty vault exchange rate manipulation | | MetaPool | `mint()` skipped receipt check from `_deposit()` |

---

2. ACCESS CONTROL

> #2 Critical bug class — 19% of all Criticals. $953M lost in 2024 alone. > Real protocols: Wormhole ($10M), ZeroLend, Flare FAssets, Parity ($150M frozen)

What It Is

A function that should be restricted is callable by anyone. Or a function checks the wrong condition (existence vs. ownership). Or a modifier uses `if` instead of `require` and silently does nothing for non-admins.

Root Cause Patterns

**Variant 1: Missing Modifier on Sibling Function**

function vote(uint256 tokenId) external onlyNewEpoch(tokenId) {  // guarded
function reset(uint256 tokenId) external onlyNewEpoch(tokenId) { // guarded
function poke(uint256 tokenId) external {                         // NO GUARD
    // Anyone calls poke() unlimited times per epoch
    // poke() distributes FLUX rewards → infinite inflation
}

**Variant 2: Wrong Check — Existence vs. Ownership**

// ZeroLend split() — anyone can steal victim's tokens:
function split(uint256 tokenId, uint256 amount) external {
    _requireOwned(tokenId);  // checks if token EXISTS, not if caller OWNS it
    _burn(tokenId);
    _mint(msg.sender, amount);  // attacker gets tokens they don't own
}

**Variant 3: Tautology in Require**

// Flare FAssets — proof validation always passes:
require(
    sourceAddressesRoot == sourceAddressesRoot,  // always true! comparing to itself
    "Invalid"
);

**Variant 4: Silent Modifier (if vs require)**

// VULNERABLE — non-admi
Read more
Ships withtradecatlabs-vibe-coding-cn

从想法到产品的 AI 结对编程工作流标准:Prompt + Skill + Context + Quality Gate + 工程闭环 <!-- 徽章区域 (BADGES) --> 本仓库的 AI 解读链接:zread.ai/tukuaiai/vibe-coding-cn 🧠 六条核心命题

Get the whole plugin