Web3 Smart Contract Audit
awarexone/Agentic-Bug-Hunter
Guides smart contract audits and bounty target selection with ten DeFi bug classes, kill signals, a Foundry PoC template and grep patterns.
Reference for ten classes of DeFi smart contract bugs, each with root cause, vulnerable code, fix, grep patterns and paid examples, for audits and bug bounty reviews.
$ npx skills add tradecatlabs/vibe-coding-cn --skill web3-bug-classes -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install tradecatlabs/vibe-coding-cn web3-bug-classes --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/tradecatlabs/vibe-coding-cn.git skills-src && mkdir -p .claude/skills && cp -r skills-src/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes .claude/skills/web3-bug-classes && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "web3-bug-classes" agent skill from https://github.com/tradecatlabs/vibe-coding-cn/tree/develop/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes into .claude/skills/web3-bug-classes/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "web3-bug-classes", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/tradecatlabs/vibe-coding-cn/tree/develop/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classesType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add tradecatlabs/vibe-coding-cn --skill web3-bug-classes -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install tradecatlabs/vibe-coding-cn web3-bug-classes --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/tradecatlabs/vibe-coding-cn.git skills-src && mkdir -p .agents/skills && cp -r skills-src/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes .agents/skills/web3-bug-classes && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "web3-bug-classes" agent skill from https://github.com/tradecatlabs/vibe-coding-cn/tree/develop/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes into .agents/skills/web3-bug-classes/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "web3-bug-classes", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add tradecatlabs/vibe-coding-cn --skill web3-bug-classes -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install tradecatlabs/vibe-coding-cn web3-bug-classes --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/tradecatlabs/vibe-coding-cn.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes .cursor/skills/web3-bug-classes && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "web3-bug-classes" agent skill from https://github.com/tradecatlabs/vibe-coding-cn/tree/develop/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes into .cursor/skills/web3-bug-classes/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "web3-bug-classes", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/tradecatlabs/vibe-coding-cn.git --path research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add tradecatlabs/vibe-coding-cn --skill web3-bug-classes -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install tradecatlabs/vibe-coding-cn web3-bug-classes --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/tradecatlabs/vibe-coding-cn.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes .gemini/skills/web3-bug-classes && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "web3-bug-classes" agent skill from https://github.com/tradecatlabs/vibe-coding-cn/tree/develop/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes into .gemini/skills/web3-bug-classes/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "web3-bug-classes", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install tradecatlabs/vibe-coding-cn web3-bug-classesInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add tradecatlabs/vibe-coding-cn --skill web3-bug-classes -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/tradecatlabs/vibe-coding-cn.git skills-src && mkdir -p .github/skills && cp -r skills-src/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes .github/skills/web3-bug-classes && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "web3-bug-classes" agent skill from https://github.com/tradecatlabs/vibe-coding-cn/tree/develop/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes into .github/skills/web3-bug-classes/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "web3-bug-classes", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add tradecatlabs/vibe-coding-cn --skill web3-bug-classes -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install tradecatlabs/vibe-coding-cn web3-bug-classes --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/tradecatlabs/vibe-coding-cn.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes .opencode/skills/web3-bug-classes && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "web3-bug-classes" agent skill from https://github.com/tradecatlabs/vibe-coding-cn/tree/develop/research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes into .opencode/skills/web3-bug-classes/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "web3-bug-classes", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
web3-bug-classesReference for ten classes of DeFi smart contract bugs, each with root cause, vulnerable code, fix, grep patterns and paid examples, for audits and bug bounty reviews.
A long reference to ten classes of DeFi smart contract vulnerabilities, written for people auditing contracts or hunting bounties. Each class comes with its root cause, vulnerable Solidity code, the fix, grep patterns for finding it and real paid examples. The classes named in the description are accounting desync, access control, incomplete paths, off-by-one errors, oracle manipulation, ERC4626 vaults, reentrancy, flash loans, signature replay and proxy or upgrade bugs.
The first class, accounting state desynchronization, is described as the top critical bug class on Immunefi. It explains how two variables that should stay in sync drift apart when one code path updates one and skips the other, with variants such as a fast path that skips an update, rewards credited to the wrong accumulator and updates made in the wrong order. Each class also lists kill signals that rule a suspect out. The excerpt is cut off after the first class.
10 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 81fc7ae. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are solidity and bash).
From the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
TARGET_TOKENFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
DeFi Smart Contract Bug Classes loads about 10k tokens when it runs. Until then it costs about 82 tokens; SKILL.md has 1,565 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from tradecatlabs/vibe-coding-cn at commit 81fc7ae, republished under its MIT licence (© tradecatlabs). 1,565 words, ~10,176 tokens.
.claude/skills/web3-bug-classes/SKILL.md (or your agent's skills folder).10 bug classes. Each one with root cause, vulnerable code, fix, grep patterns, and real paid examples.
#1 Critical bug class — 28% of all Criticals on Immunefi. Real protocols: Yeet, Alchemix V3, Folks Finance, ResupplyFi, MetaPool
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// 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"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
}# 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?| 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 Critical bug class — 19% of all Criticals. $953M lost in 2024 alone. Real protocols: Wormhole ($10M), ZeroLend, Flare FAssets, Parity ($150M frozen)
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.
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-admin silently gets through:
modifier onlyAdmin() {
if (msg.sender == admin) {
_; // only executes body for admin
}
// non-admin: modifier body skipped, function STILL EXECUTES
}
// CORRECT:
modifier onlyAdmin() {
require(msg.sender == admin, "Not admin");
_;
}Variant 5: Uninitialized Proxy — initialize() Callable by Anyone
contract Vault {
address public owner;
function initialize(address _owner) public { // MISSING: initializer modifier
owner = _owner; // anyone can call this and become owner
}
}
// Fix: constructor() { _disableInitializers(); }# Find sibling function families — do ALL have the same modifier set?
grep -rn "function vote\|function poke\|function reset\|function update\|function claim\|function harvest" contracts/ -A2
# Ownership check pattern — existence vs ownership?
grep -rn "_requireOwned\|ownerOf\|_isApprovedOrOwner\|_checkAuthorized" contracts/ -B5 -A5
# Silent modifiers using if without revert
grep -rn "modifier\b" contracts/ -A8 | grep -B3 "if (" | grep -v "require\|revert\|else.*revert"
# Uninitialized initializer
grep -rn "function initialize\b" contracts/ -A3
grep -rn "_disableInitializers()" contracts/
# Missing access control on critical functions
grep -rn "function mint\b\|function burn\b\|function emergencyWithdraw\b\|function upgradeTo\b" contracts/ -A3For every privileged role:
□ Who can GRANT this role?
□ Who can REVOKE this role?
□ Is the initial role granted in constructor to the correct address?
□ Can the same address grant itself additional roles?
□ Is there a timelock on role transfers?
□ What happens if this role address is address(0)?
□ Are all roles actually granted that are referenced in the code?require (not silent if)onlyOwner or role check in _authorizeUpgrade_disableInitializers() is present in implementation constructoronlyRole() are actually granted in constructor or initializer| Protocol | Payout | Bug |
|---|---|---|
| Wormhole | $10M | Uninitialized UUPS proxy → anyone calls initialize() |
| ZeroLend | n/a | split() uses existence check not ownership check |
| Alchemix | n/a | poke() missing onlyNewEpoch → infinite FLUX inflation |
| Flare | n/a | Tautology in require → proof always passes |
| Parity | $150M frozen | No access control on initWallet() in library |
#3 Critical bug class — 17% of Criticals. Real protocols: Plume, Puffer, ThunderNFT, Alchemix V3, MetaPool, LI.FI
The happy path (deposit, create, place) handles tokens correctly. An alternate path (update, partial fill, fast path, zero amount) either moves tokens WITHOUT updating accounting, or updates accounting WITHOUT moving tokens, or deletes state regardless of whether the operation succeeded.
Variant 1: Update Function Missing Refund
// ThunderNFT — place_order takes tokens, update_order doesn't refund:
function place_order(OrderInput calldata order) external {
token.safeTransferFrom(msg.sender, address(this), order.price); // takes tokens
orders[orderId] = order;
}
function update_order(OrderInput calldata updatedOrder) external {
if (updatedOrder.price < existingOrder.price) {
uint256 refund = existingOrder.price - updatedOrder.price;
// BUG: NO REFUND for sell orders → tokens permanently stuck
}
orders[orderId] = updatedOrder;
}Variant 2: Partial Fill — Token Stuck
// Plume — refund handles ETH only, not ERC20:
function swapForETH(uint256 amountIn) external {
token.safeTransferFrom(msg.sender, address(this), amountIn);
uint256 filled = dex.swap(amountIn); // partial fill possible
_refundExcessEth(amountIn - filled); // BUG: refunds ETH only
// If token is ERC20: remaining tokens NEVER refunded
}Variant 3: Queue Entry Deleted on Failure
// Puffer — delete happens before execution, in batch where one failure corrupts all:
function executeTransaction(bytes32 txHash) external {
Transaction memory tx = queue[txHash];
delete queue[txHash]; // deleted BEFORE execution
(bool success,) = tx.target.call{value: tx.value}(tx.data);
// In batch: failure of one element corrupted state for whole batch
}Variant 4: safeApprove Without Cleanup
// Plume — residual approval blocks second swap:
function executeSwap(uint256 amount) external {
token.safeApprove(router, amount); // approve full amount
uint256 used = router.swap(amount); // partial fill: used < amount
// remaining approval (amount - used) never cleared
// Next call: safeApprove(router, newAmount) → REVERTS (current allowance != 0)
}
// Fix: token.safeApprove(router, 0); before approvingVariant 5: mint() Skips Receipt Check That deposit() Has
// MetaPool — mint() bypasses the check enforced by _deposit():
function deposit(uint256 assets, address receiver) public override returns (uint256 shares) {
shares = _deposit(assets, receiver); // includes receipt validation
}
function mint(uint256 shares, address receiver) public override returns (uint256 assets) {
assets = convertToAssets(shares);
_mint(receiver, shares); // BUG: directly mints without _deposit() validation
// _deposit() has: require(actualReceived >= expectedAmount, "Insufficient")
// mint() skips this → mints without receiving actual assets
}For every pair of functions that do similar things:
1. List all state changes in function A (deposit/place/create)
2. List all state changes in function B (withdraw/update/cancel)
3. For each state change in A: does B have the corresponding reverse?
4. For each token transfer in A: does B have the corresponding refund?
5. For each event in A: does B emit a corresponding event?
If A does X but B doesn't do the reverse of X → BUG.# Find create/place/add vs update/modify function pairs
grep -rn "function place_\|function create_\|function add_\|function open_" contracts/ -A5
grep -rn "function update_\|function modify_\|function edit_\|function change_" contracts/ -A5
# Find refund logic — does it handle both ETH and ERC20?
grep -rn "_refundExcess\|refundTokens\|refundAmount\|remainder" contracts/ -A10
# safeApprove without zero-reset before
grep -rn "safeApprove\b" contracts/
# delete before operation completes
grep -rn "delete\b" contracts/ -B5 -A5
# ERC4626: compare deposit() vs mint(), withdraw() vs redeem()
grep -rn "function deposit\|function mint\|function withdraw\|function redeem" contracts/ -A10safeApprove(router, 0) present before every safeApprove(router, amount)deposit() and mint() both call the same internal _deposit() function| Protocol | Root Cause |
|---|---|
| Plume | _refundExcessEth handles ETH only → ERC20 partial fill stuck |
| Plume | safeApprove without cleanup → second swap reverts |
| ThunderNFT | update_order missing refund for sell orders |
| Puffer | executeTransaction deletes queue entry on failure |
| LI.FI | $1.7M — library skips whitelist → arbitrary external call |
| MetaPool | mint() bypasses receipt check that deposit() has |
#4 High bug class — 22% of Highs. Single character change. Massive impact. Real protocols: VeChain Stargate, Alchemix, Flare, Shardeum
At a boundary condition (period end, epoch transition, time == deadline), the wrong comparison operator routes to the wrong code branch. The "equal case" is the bug — > misses it, >= catches it.
// VeChain Stargate — post-exit drain:
function _claimableDelegationPeriods(address delegator) internal view returns (uint256) {
uint256 endPeriod = userInfo[delegator].exitPeriod;
// BUG: when block.period == endPeriod (exactly at exit), condition is FALSE
if (endPeriod > nextClaimablePeriod) {
return 0; // exited users get nothing — correct for this case
}
// WRONG: endPeriod == nextClaimablePeriod lands here
return nextClaimablePeriod - lastClaimedPeriod;
// → returns rewards for the period after exit → infinite post-exit drain
// FIX:
// if (endPeriod >= nextClaimablePeriod) { return 0; }
}1. Period / Epoch Boundaries
grep -rn "period\|epoch\|round" contracts/ -i | grep "[<>][^=]"
# Every > should be questioned: should it be >=?2. Time-Based Locks
// Question: is the exact moment of expiry locked or unlocked?
return block.timestamp < users[user].depositTimestamp + lockPeriod;
// At timestamp == depositTimestamp + lockPeriod: false → NOT locked (unlocked at exact expiry)3. Loop Break Conditions
// Alchemix — processes yield per week:
for (uint256 t = weekStart; t <= weekEnd; t += WEEK) {
if (t > roundedTimestamp) break; // BUG: should be t >= roundedTimestamp
// When t == roundedTimestamp: doesn't break → processes incomplete week
// → caches supply at wrong timestamp → division by zero in claims
}grep -rn "\bbreak\b" contracts/ -B5
# For each break: should it also break when equal?4. Array Index Boundaries
for (uint256 i = 0; i <= array.length; i++) { // should be i < array.length
process(array[i]); // array[array.length] = out of bounds → revert
}grep -rn "\.length\s*-\s*1\|i\s*<=\s*.*\.length\b" contracts/5. Amount / Balance Boundaries
require(balanceOf(msg.sender) >= amount); // allows exact full withdrawal
// vs:
require(balanceOf(msg.sender) > amount); // can't withdraw last wei6. Rounding and Precision Boundaries
// Can any input amount produce exactly 0 output that should be non-zero?
uint256 shares = (amount * totalSupply) / totalAssets;
// If amount is just below threshold → gets 0 shares → free deposit entryFor every if (A > B) found: "What happens when A == B?" Which branch? Is that correct?
For every if (A < B) found: "What happens when A == B?"
# Variables that represent boundaries
grep -rn "Period\|Epoch\|Round\|Index\|Timestamp\|Deadline" contracts/ -A3 | grep "[<>][^=]"
grep -rn "period\|epoch\|round\|deadline\|cutoff\|threshold" contracts/ -A3 | grep "[<>][^=]"
# Loop breaks — boundary included?
grep -rn "\bbreak\b\|\bcontinue\b" contracts/ -B10>= and > are present with clear, distinct intent in comments| Protocol | Impact | Bug |
|---|---|---|
| VeChain Stargate | High | > should be >= → infinite post-exit reward drain |
| Alchemix | High | > should be >= in loop → processes incomplete week → div/0 |
| VeChain (same) | High | Same bug reported by 3 different hunters simultaneously |
12% of all reports, largest individual payouts. $117M Mango, $70M Curve. Real protocols: Swaylend, ZeroLend, Chainlink integrations, Pyth, Uniswap V2/V3
If a protocol reads a wrong price, it can be tricked into accepting undercollateralized loans, minting assets with fake backing, liquidating healthy positions, or issuing more debt than collateral supports. With a flash loan: attacker has $1B+ of free capital for 1 block.
Bug A — Missing Staleness Check (most common)
// VULNERABLE:
(, int256 price,,,) = priceFeed.latestRoundData();
return uint256(price);
// If Chainlink node goes down, last reported price returned indefinitely
// CORRECT:
(, int256 price,, uint256 updatedAt,) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt <= MAX_PRICE_AGE, "Stale price");
require(price > 0, "Invalid price");
return uint256(price);Bug B — Missing Sequencer Uptime Check (L2 only)
// On Arbitrum, Optimism: if sequencer goes down, prices can be stale
(, int256 answer, uint256 startedAt,,) = sequencerUptimeFeed.latestRoundData();
require(answer == 0, "Sequencer down");
require(block.timestamp - startedAt >= GRACE_PERIOD, "Grace period active");Bug C — Using latestAnswer() (deprecated)
int256 price = priceFeed.latestAnswer(); // doesn't return timestamp → no staleness check possibleBug A — Confidence Not Subtracted
// VULNERABLE: uses price directly without confidence interval
PythStructs.Price memory p = pyth.getPriceNoOlderThan(priceId, MAX_AGE);
return amount * uint256(int256(p.price)) / 1e8; // overstates collateral
// CORRECT (conservative):
return amount * uint256(int256(p.price - int64(p.conf))) / 1e8;Bug B — Hardcoded Global Confidence Threshold
uint256 public constant ORACLE_MAX_CONF_WIDTH = 20; // BPS — may fail for volatile assetsUniswap V2 — getReserves() Attack
// VULNERABLE: reading price from getReserves() in same block as action
(uint112 reserve0, uint112 reserve1,) = pair.getReserves();
return reserve1 * 1e18 / reserve0; // spot price — manipulable via flash loan
// Attack: Flash loan 10M USDC → swap → inflated price → deposit → borrow → drain → swap backUniswap V3 — slot0() Attack
// VULNERABLE: slot0 is manipulable within one block
(uint160 sqrtPriceX96,,,,,,) = pool.slot0();
// SAFE (TWAP):
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 1800; // 30 minutes ago
secondsAgos[1] = 0;
(int56[] memory tickCumulatives,) = pool.observe(secondsAgos);
// Cost to manipulate TWAP for 30 min > profit from exploitProtocol-Internal (balanceOf) Donation Attack
// VULNERABLE:
function totalAssets() public view returns (uint256) {
return token.balanceOf(address(this)); // manipulable via direct token transfer
}
// Attack: donate tokens directly → inflate price → borrow moregrep -rn "latestRoundData()" contracts/ -A5
# Is updatedAt captured? Is block.timestamp - updatedAt <= MAX checked?
grep -rn "sequencer\|SEQUENCER\|ArbitrumSequencer" contracts/
# Present on L2? If not → bug
grep -rn "slot0\b\|getReserves()" contracts/
# Any pricing logic using these = flash loan manipulable
grep -rn "latestAnswer()" contracts/
# Deprecated → should use latestRoundData()□ Chainlink: updatedAt staleness check present?
□ Chainlink: price > 0 check present?
□ Chainlink: on L2? Sequencer uptime check present?
□ Chainlink: using deprecated latestAnswer()?
□ Pyth: confidence subtracted from collateral valuation?
□ Pyth: per-asset confidence threshold or global?
□ Uniswap V2: using getReserves() for pricing? → needs TWAP
□ Uniswap V3: using slot0() for pricing? → needs TWAP
□ Protocol: using balanceOf(this) for pricing? → needs internal tracking
□ TWAP: window >= 30 minutes?
□ Circuit breaker if price moves >X% in single block?Found repeatedly in 2024-2025: Belong, ResupplyFi, Napier, Astaria, Smilee Finance, FlatMoney
ERC4626 = tokenized vault standard. Users deposit assets, get shares. 1 share = totalAssets / totalShares. The edge cases kill protocols.
1. Attacker deposits 1 wei → gets 1 share
2. Attacker DONATES 999,999 USDC directly to vault (not via deposit)
→ totalAssets = 1M, totalSupply = 1 share → 1 share = 1M USDC
3. Victim deposits 999,999 USDC
→ shares = (999,999 * 1) / 1,000,000 = 0 shares (rounds down)
→ Victim gets 0 shares, can't withdraw
4. Attacker redeems 1 share → receives ~2M USDC// VULNERABLE (no virtual offset):
function convertToShares(uint256 assets) public view returns (uint256) {
uint256 supply = totalSupply();
return supply == 0 ? assets : (assets * supply) / totalAssets();
}
// FIX (OpenZeppelin virtual offset):
return assets.mulDiv(
totalSupply() + 10 ** _decimalsOffset(), // +1 virtual share
totalAssets() + 1, // +1 virtual asset
rounding
);// Custom ERC20 with lock period tracking:
mapping(address => Stake[]) public stakes;
function _update(address from, address to, uint256 value) internal override {
super._update(from, to, value); // just moves balances
// MISSING: migrate stakes from 'from' to 'to'
// Bob has shares but no stake records → can't withdraw → permanent freeze
}// If protocol rounds consistently in user's favor:
// Swap 1 wei → get back 1 wei (should be 0.5 wei rounded down)
// Repeat 1M times → drain pool
// RULE:
// For deposits/mints: round DOWN (fewer shares issued = conservative = safe)
// For withdrawals/redeems: round DOWN (fewer assets given = conservative = safe)
// If rounding consistently favors user → drainable via tiny swaps// VULNERABLE: price derived from raw balance
function totalAssets() public view override returns (uint256) {
return underlying.balanceOf(address(this)); // manipulable via direct transfer
}
// SAFE: track internally
uint256 private _trackedBalance;
function totalAssets() public view override returns (uint256) {
return _trackedBalance;
}# First depositor check
grep -rn "convertToShares\|_convertToShares\|previewDeposit" contracts/ -A5
# Is denominator: totalAssets() + 1?
# Is numerator: totalSupply() + 10**decimalsOffset()?
# Transfer without stake migration
grep -rn "_update\b\|function _transfer\b" contracts/ -A10
# Does _update migrate stakes/locks/rewards when from != 0 AND to != 0?
# Donation attack
grep -rn "totalAssets\(\)" contracts/ -A3
# Does it use balanceOf(address(this)) directly?
# Rounding direction
grep -rn "/ totalAssets\|/ totalSupply\|/ reserves" contracts/□ First depositor: does convertToShares use +1 virtual offset?
□ Is there a "dead shares" mechanism or minimum deposit?
□ Transfer: does _update migrate stake/lock/reward records?
(OR: are transfers disabled entirely?)
□ totalAssets(): tracked internally or raw balanceOf?
□ Rounding: consistent direction (always favor protocol)?
□ Does mint() call the same internal logic as deposit()?
□ Does redeem() call the same internal logic as withdraw()?_decimalsOffset() override presenttotalAssets() uses internal tracked balance, not balanceOf(this)$300M+ losses since Jan 2024. Penpie $27M, Curve $70M. Classic + Cross-function + Read-only + Cross-contract. All 4 must be checked.
// VULNERABLE: state updated AFTER external call
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
(bool success,) = msg.sender.call{value: amount}(""); // attacker's receive() fires
require(success);
balances[msg.sender] -= amount; // runs AFTER attacker's callback → reenter with old balance
}
// CORRECT (Checks-Effects-Interactions):
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount; // Effect FIRST
(bool success,) = msg.sender.call{value: amount}(""); // Interaction last
require(success);
}// withdraw() and transfer() share balance state:
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
token.safeTransfer(msg.sender, amount); // triggers ERC777 tokensReceived
// In tokensReceived: calls transfer() (different function — different nonReentrant mutex?)
}
// KEY: Does nonReentrant block ALL functions or just the current one?
// OpenZeppelin ReentrancyGuard: blocks reentry into ANY nonReentrant function on the contract
// Custom mutex: check if it's per-function or per-contract// Contract A (e.g., Curve pool) is mid-state-change when external call fires:
function removeLiquidity() external nonReentrant {
totalSupply -= lpAmount; // totalSupply updated
(bool success,) = msg.sender.call{value: ...}(""); // fires attacker code
poolBalance -= withdrawn; // poolBalance updated AFTER the call
}
// In attacker's receive():
// Reads CurvePool.totalSupply (updated) and CurvePool.poolBalance (NOT yet updated)
// Price = poolBalance / totalSupply → artificially inflated
// Borrows against inflated collateral value in a third protocolProtocol A calls Protocol B
Protocol B makes external call (to attacker)
Attacker calls Protocol A while B is mid-execution
Protocol A sees consistent own state
But Protocol A's computation depends on Protocol B's state (which is inconsistent)# Step 1: Find all external calls
grep -rn "\.call(\|\.call{value\|safeTransfer\|safeTransferFrom\|\.transfer(\|\.send(" contracts/
# Step 2: For each — is state updated BEFORE this call?
# Does the function have nonReentrant?
# Step 3: ERC777 tokens with hooks
grep -rn "ERC777\|IERC777\|tokensReceived\|tokensToSend" contracts/
# Step 4: External state read during execution (read-only reentrancy)
grep -rn "ICurve\|IBalancer\|IUniswap\|IPool\b" contracts/ -A3
# External calls in MIDDLE of state updates (state before AND after the call)
grep -rn "\.call{value\|\.call(" contracts/ -B20 -A5_processYield / _claim functions follow CEI: state updated before transfernonReentrant present on all external-call-containing functionsharvest() / sensitive functions require whitelisted caller (attacker can't trigger)| Protocol | Loss | Variant |
|---|---|---|
| Penpie | $27M | Classic: batchHarvestMarketRewards() missing nonReentrant |
| Curve Finance | $70M | Read-only: Vyper compiler bug broke reentrancy guard |
| Siren Protocol | Contest | Cross-function: withdraw() + claimFees() share state |
| Rari Capital | $80M | Cross-contract: Compound fork + ETH callback |
Used in 83% of eligible exploits. $0 capital required. Real protocols: Beanstalk $182M, Mango $117M, Euler $197M
Flash loans give unlimited capital for 1 block with no collateral. Any check that relies on "attacker doesn't have enough tokens" is broken.
Pattern 1: Oracle Manipulation
1. Flash borrow 100,000 ETH
2. Dump 100,000 ETH → TARGET_TOKEN on Uniswap (price crashes)
3. Liquidate TARGET_TOKEN positions at crashed price → steal collateral
4. Repay flash loanOR (pump version):
1. Flash borrow USDC
2. Buy TARGET_TOKEN → price pumps
3. Deposit as collateral at inflated price
4. Borrow against it
5. Sell TARGET_TOKEN back (price normalizes)
6. Repay flash loan → protocol has bad debtPattern 2: Governance Attack
1. Flash borrow governance tokens (no collateral needed)
2. Vote on malicious proposal (if no snapshot delay)
3. Execute: "send all funds to attacker"
4. Repay flash loan
Required weakness: voting power checked at vote time, not at proposal creation
Defense: snapshot voting power at proposal creation blockPattern 3: Liquidity Manipulation
1. Flash borrow LP tokens
2. Remove liquidity → temporarily drain pool
3. Pool's balances are tiny → exploit "minimum liquidity" edge case
4. Re-add liquidity → repay flash loan// Balancer V2 (no fee, mainnet + most networks)
address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
// Implement: receiveFlashLoan(tokens, amounts, feeAmounts, userData)
// Morpho Blue (~0% fee)
address constant MORPHO = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb;
// Implement: onMorphoFlashLoan(assets, data)
// Aave V3 (0.05% fee)
address constant AAVE_POOL = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2;
// Implement: executeOperation(assets, amounts, premiums, initiator, params)
// Uniswap V2 (0.3% fee)
// IUniswapV2Pair.swap(amount0Out, amount1Out, to, data)
// Implement: uniswapV2Call(sender, amount0, amount1, data)□ Does any function read an AMM spot price (getReserves/slot0)?
→ Can be manipulated in same block with flash loan
□ Does any function allow voting without snapshot delay?
→ Flash loan governance attack
□ Does any pricing function use balanceOf(address(this))?
→ Donation attack (flash loan + transfer)
□ Is there ANY check of "how many tokens does attacker have?"
→ If yes: can they flash borrow enough to pass the check?
□ Does any calculation compare current balance to a stored value?
→ Can be manipulated by depositing/withdrawing via flash loanHigh payout potential $5K-$500K. Cross-chain opportunity. Real protocols: Polygon $2.2M, zkSync $200K, Alchemix, EIP-2612 permit
Variant 1: Cross-Chain Signature Replay
// VULNERABLE: signature doesn't include chainId
function claimRewards(address user, uint256 amount, bytes memory signature) external {
bytes32 message = keccak256(abi.encodePacked(user, amount)); // no chainId!
address signer = ECDSA.recover(message, signature);
require(signer == authorizedSigner, "Invalid signature");
// Attack: claim on Ethereum, replay same signature on Arbitrum
}
// FIX: include chainId in DOMAIN_SEPARATOR
bytes32 DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"),
keccak256("Protocol"),
block.chainid, // CHAIN ID
address(this) // CONTRACT ADDRESS
));Variant 2: Missing Nonce (Same-Chain Replay)
// VULNERABLE: no nonce → same signature reusable indefinitely
function executePermit(address user, uint256 amount, bytes memory sig) external {
bytes32 hash = keccak256(abi.encodePacked(user, amount, address(this)));
// No nonce tracking → replay this call indefinitely
}
// FIX:
mapping(address => uint256) public nonces;
bytes32 hash = keccak256(abi.encodePacked(user, amount, nonces[user]++, address(this)));Variant 3: EIP-2612 Permit Frontrun DoS
// Victim submits: permitAndDeposit(owner, spender, value, deadline, v, r, s)
// Attacker sees in mempool, frontruns: token.permit(owner, spender, value, deadline, v, r, s)
// → nonce consumed → victim's tx reverts (permit fails, deposit never happens)
// SAFE pattern: wrap permit in try/catch
function permitAndDeposit(uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external {
try token.permit(msg.sender, address(this), amount, deadline, v, r, s) {}
catch {} // allowance already set → deposit proceeds regardless
token.safeTransferFrom(msg.sender, address(this), amount);
}ECDSA Malleability
// VULNERABLE: signatures used as mapping keys (bytes not address)
mapping(bytes => bool) public usedSignatures;
function claim(bytes memory sig) external {
require(!usedSignatures[sig]);
// Attacker modifies s value → different bytes, same signer → bypasses check
}
// FIX: use OpenZeppelin ECDSA.recover (normalizes s to lower half)grep -rn "ecrecover\|ECDSA\.recover" contracts/
grep -rn "chainId\|block\.chainid\|DOMAIN_SEPARATOR" contracts/
# ecrecover present without corresponding chainId = replay vulnerability
grep -rn "nonces\[\|nonce\b" contracts/
# ecrecover without nonce tracking = potentially replayable
grep -rn "permit(" contracts/ | grep -v "//\|IERC20Permit\|interface"
grep -rn "try.*permit\|catch.*permit" contracts/
# Safe pattern: wrapped in try/catch□ Does signature include: chainId? (cross-chain replay protection)
□ Does signature include: contract address? (replay between contracts)
□ Does signature include: nonce? (same-chain replay protection)
□ Does signature include: deadline/expiry?
□ Is OpenZeppelin ECDSA used (not raw ecrecover)?
□ Are signatures used as mapping keys? (malleability attack)
□ Is permit() wrapped in try/catch in compound functions?
□ If multi-chain: is DOMAIN_SEPARATOR computed at runtime (not hardcoded)?block.chainid and address(this)nonces[user]++ pattern$10M Wormhole, $150M Parity. Uninitialized impl = anyone becomes admin. Patterns: UUPS, Transparent Proxy, Beacon Proxy, Storage Collision
// VULNERABLE: implementation deployed but initialize() never called
// AND: no _disableInitializers() in constructor
contract MyVault {
function initialize(address _owner) external {
require(!initialized);
initialized = true;
owner = _owner;
}
}
// Attack:
// 1. Find impl address: proxy.implementation()
// 2. Call impl.initialize(attacker) directly → attacker owns impl
// 3. Call impl.upgradeTo(malicious_contract) → proxy delegates to malicious
// 4. Drain all funds through proxy
// FIX:
constructor() {
_disableInitializers(); // prevents any initialize() call on impl directly
}// VULNERABLE: New implementation adds variable BEFORE existing ones
// V1: slot 0 = totalAssets, slot 1 = owner
// V2: slot 0 = newFee (overwrites totalAssets!), slot 1 = totalAssets (overwrites owner!)
// FIX: Always append new variables at the END
// Use __gap arrays for reserved slots:
uint256[50] private __gap; // reserve 50 slots for future vars// VULNERABLE: Anyone can upgrade
contract MyUUPS is UUPSUpgradeable {
function _authorizeUpgrade(address newImplementation) internal override {
// EMPTY! No access control → anyone calls upgradeTo(malicious)
}
}
// CORRECT:
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}function initializeV2() public reinitializer(2) {
// No access control → anyone calls this → resets state
}grep -rn "function initialize\b" contracts/ -A3
# Does it have: initializer modifier? _disableInitializers() in constructor?
grep -rn "_disableInitializers()" contracts/
# Absent? Check if proxy implementation is separately deployable
grep -rn "_authorizeUpgrade" contracts/ -A3
# Is there: onlyOwner / onlyRole / require(msg.sender == admin)?
# Empty body = anyone can upgrade
# Storage layout comparison between versions
grep -rn "slot\|__gap\|ERC1967Storage" contracts/□ Is the implementation contract upgradeable? (UUPS/Transparent/Beacon?)
□ Is implementation's initialize() protected by initializer modifier?
□ Is _disableInitializers() called in implementation constructor?
□ Does _authorizeUpgrade() have access control?
□ If new version: are variables only added at the END of storage?
□ Is there a __gap reserved for future variables?
□ Is DOMAIN_SEPARATOR recalculated or hardcoded? (hardcoded breaks on upgrade)
□ Are there any selfdestruct calls? (can brick proxy permanently)upgradeTo, no initialize)_disableInitializers()_authorizeUpgrade has onlyOwner or equivalent__gap arrays between version-specific variables| Protocol | Payout | Bug |
|---|---|---|
| Wormhole | $10M | Uninitialized UUPS proxy → anyone calls initialize() |
| Parity | $150M frozen | No access control on initWallet() in library |
| Rank | Class | % Criticals | Flash Loan? | First Grep |
|---|---|---|---|---|
| 1 | Accounting Desync | 28% | No | totalSupply|totalShares|totalAssets |
| 2 | Access Control | 19% | No | function.*external without modifier |
| 3 | Incomplete Path | 17% | Sometimes | function update_|function cancel |
| 4 | Off-By-One | 22% Highs | No | period|epoch|round.*[<>][^=] |
| 5 | Oracle Manipulation | 12% | Yes | latestRoundData|getReserves|slot0 |
| 6 | ERC4626 Vaults | Varies | Yes | convertToShares|totalAssets() |
| 7 | Reentrancy | 8% | Sometimes | \.call{value|safeTransfer before state |
| 8 | Flash Loan | 83% use it | Yes | Any spot price or governance vote |
| 9 | Signature Replay | 3% | No | ecrecover|ECDSA.recover |
| 10 | Proxy/Upgrade | 2% | No | function initialize|_authorizeUpgrade |
→ NEXT: 03-grep-arsenal.md
© tradecatlabs, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes of tradecatlabs/vibe-coding-cn.
Open the folder on GitHubat commit 81fc7ae
We found 2 copies of this SKILL.md (exact, near-identical or edited) in other folders, from 2 other GitHub owners. This page covers the copy in tradecatlabs/vibe-coding-cn, which our catalogue first saw on October 7, 2026.
DeFi Smart Contract Bug Classes next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| DeFi Smart Contract Bug Classes this skilltradecatlabs/vibe-coding-cn | 17k | 2 repos | ~10k | Automated safety check: Pass | MIT | |
| Web3 Smart Contract Auditawarexone/Agentic-Bug-Hunter | 5.3k | 3 repos | ~4.5k | Automated safety check: Pass | MIT | |
| Fizzpashov/skills | 1.2k | 2 repos | ~11k | Automated safety check: Pass | MIT | |
| Flounderadshao/flounder | 517 | — | ~9.2k | Automated safety check: Pass | AGPL-3.0 | |
| Reentrancy Auditoralt-research2/SolidityGuard | 104 | — | ~1.8k | Automated safety check: Pass | Custom licence | |
| Smart Contract Auditforefy/.context | 152 | 1 repos | ~5.1k | Automated safety check: Pass | MIT |
awarexone/Agentic-Bug-Hunter
Guides smart contract audits and bounty target selection with ten DeFi bug classes, kill signals, a Foundry PoC template and grep patterns.
pashov/skills
Generate Echidna/Medusa-compatible Solidity fuzz suites from Foundry or Hardhat projects.
adshao/flounder
Operates Flounder, an autonomous white-hat security auditor.
alt-research2/SolidityGuard
Deep reentrancy vulnerability analysis for Solidity contracts.
forefy/.context
Comprehensive smart contract security audit framework with multi-expert analysis.
elophanto/EloPhanto
A skill your agent uses when reviewing a Solidity, Vyper, or Rust (Solana/Anchor) smart contract for paid audit work or pre-launch sanity check.
tradecatlabs/vibe-coding-cn
Meta-skill that turns docs, APIs, code or specs into a reusable skill with references and a quality gate, and refactors skills that are unclear or misfire.
tradecatlabs/vibe-coding-cn
A master set of ten grep command blocks that surface likely vulnerability classes in Solidity source within the first 30 minutes of auditing a new protocol.
tradecatlabs/vibe-coding-cn
Operates tmux sessions like an administrator: reads pane output, sends keys, inspects many panes at once, and coordinates multiple AI terminals through a swarm state script, built on oh-my-tmux.
tradecatlabs/vibe-coding-cn
A selection guide to AI-driven tools for Web3 bug bounty work, from autonomous web pentesters to smart contract bug finders, with notes on authorization.
tradecatlabs/vibe-coding-cn
Runs Slither and Mythril against Solidity contracts to find reentrancy, overflow and access-control bugs before mainnet deployment, then triages and reports findings.
tradecatlabs/vibe-coding-cn
Runs reproducible math computations and counterexample searches with SymPy, NumPy and mpmath, logging evidence without presenting results as proofs.
Works with
Categories
Reference for ten classes of DeFi smart contract bugs, each with root cause, vulnerable code, fix, grep patterns and paid examples, for audits and bug bounty reviews. A long reference to ten classes of DeFi smart contract vulnerabilities, written for people auditing contracts or hunting bounties. Each class comes with its root cause, vulnerable Solidity code, the fix, grep patterns for finding it and real paid examples.
DeFi Smart Contract Bug Classes fits situations like: auditing a DeFi protocol for one specific bug class; checking vault or accounting code for state that can drift out of sync; searching a contracts folder with grep patterns for risky code; reviewing oracle, flash loan or signature replay exposure.
Run `npx skills add tradecatlabs/vibe-coding-cn --skill web3-bug-classes -a claude-code`. Or copy the skill folder (research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes in tradecatlabs/vibe-coding-cn) into .claude/skills/web3-bug-classes in your project. Claude Code loads it when a task matches its description.
Run `npx skills add tradecatlabs/vibe-coding-cn --skill web3-bug-classes -a codex`. Or copy the skill folder (research/vibe-cybersecurity-cn/skills/web3-bug-bounty-hunting/web3-bug-classes in tradecatlabs/vibe-coding-cn) into .agents/skills/web3-bug-classes in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add tradecatlabs/vibe-coding-cn --skill web3-bug-classes -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/web3-bug-classes, .gemini/skills/web3-bug-classes, .github/skills/web3-bug-classes and .opencode/skills/web3-bug-classes in your project.
Going by SKILL.md and its folder, DeFi Smart Contract Bug Classes needs credentials named TARGET_TOKEN.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
DeFi Smart Contract Bug Classes is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 10k tokens (SKILL.md is roughly 41k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with DeFi Smart Contract Bug Classes: Web3 Smart Contract Audit (awarexone/Agentic-Bug-Hunter, 5.3k stars), Fizz (pashov/skills, 1.2k stars), Flounder (adshao/flounder, 517 stars) and Reentrancy Auditor (alt-research2/SolidityGuard, 104 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
tradecatlabs (a GitHub user) maintains it in tradecatlabs/vibe-coding-cn, which has 17,166 GitHub stars. The repository holds 17 skills in this directory. The repository was last updated on October 1, 2026.
Source: tradecatlabs/vibe-coding-cn on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.