mcpField
W
web3-audit

web3-audit

github.com/elementalsouls安全5.3k285 次安装今天更新
代码仓库

关于 web3-audit

面向审查 Web3 协议的安全研究人员的智能合约审计技能。它围绕十类漏洞组织代码审查,并提供审计前目标筛选和 Foundry 概念验证指导。

主要功能

  1. 01审计前目标筛选信号
  2. 02十类智能合约漏洞
  3. 03同组函数访问控制检查
  4. 04Solidity 检索模式
  5. 05Foundry PoC 模板
  6. 06真实付费漏洞案例

适用场景

  1. 01代码审查前筛选协议
  2. 02检查记账与访问控制路径
  3. 03审查预言机及 ERC4626 金库逻辑
  4. 04构建 Foundry 概念验证

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/elementalsouls/claude-bughunter --skill web3-audit

安装后在 Claude Code 中输入/web3-audit确认已加载。

详细介绍

Web3 智能合约审计

10 类漏洞。审计前的止损信号。Foundry PoC 模板。真实付费案例。


审计前止损信号(代码审查前务必检查)

ZKsync 的教训:3.22 亿美元 TVL + OZ 审计 + 75 万行代码 + 5 次审计 = 0 个发现。大型且经过充分审计的跨链桥极难发现问题。

  1. TVL < $500K → 最高奖金太低,不值得投入精力
  2. 有 2 次或更多顶级审计(Halborn、ToB、Cyfrin、OpenZeppelin),且协议简单 → 漏洞已经被发现
  3. 协议代码 < 500 行,只有单一的 A→B→C 流程 → 攻击面极小
  4. 公式:max_realistic_payout = min(10% × TVL, program_cap) — 如果低于 $10K,就跳过

软性止损: 当前版本经过 OZ/ToB/Cyfrin 审计,且代码库 > 50 万行 → 预计要花 40 小时以上,可能只找到 1 个问题。只有在奖金下限 > $50K 且你具备该协议的专业知识时才继续。

目标评分(>= 6/10 才继续):

  • TVL > $10M:+2
  • Immunefi 计划的 Critical 奖金 >= $50K:+2
  • 当前版本未经顶级审计:+2
  • 距离部署时间 < 30 天:+1
  • 你以前审计过该协议:+1
  • 有源代码 + natspec 注释:+1
  • 可升级代理:+1

唯一规则

“阅读所有同类函数。如果 vote() 有修饰符,就检查 poke()、reset()、harvest()。同类函数缺少修饰符本身就是漏洞。”

这一条规则可以解释 Immunefi 上 19% 的 Critical 发现。


1. 记账状态不同步

排名第 1 的 Critical 漏洞类别——占 Immunefi 上所有 Critical 的 28%。

漏洞是什么

两个状态变量本应保持同步。某条代码路径更新了 A,却忘记更新 B。后续代码读取这两个变量,并依据过期的 B 作出判断。

Real Value = A - B
If A updated but B isn't → Real Value appears larger → phantom value
根因模式

变体 1:虚假收益(Yeet 协议——35 份重复报告)

function startUnstake(uint256 amount) external {
    totalSupply -= amount;  // decremented BEFORE transfer
    // aToken.balanceOf(this) still reflects old value
    // yieldAmount = aToken.balanceOf - totalSupply = phantom yield
}

变体 2:快速路径跳过状态更新(Alchemix V3)

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(...);
}

变体 3:更新顺序错误(Alchemix)

function deposit(uint256 amount) external {
    _shares = (amount * totalShares) / totalAssets;  // calculated BEFORE deposit
    totalAssets += amount;   // assets added AFTER shares calculated → wrong rate
}
Grep 模式
# Find all accounting variables
grep -rn "totalSupply\|totalShares\|totalAssets\|totalDebt\|cumulativeReward\|rewardPerShare" 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 in normal path are skipped?

2. 访问控制

排名第 2 的 Critical——占 Critical 的 19%。仅 2024 年就损失了 9.53 亿美元。

变体 1:同类函数缺少修饰符
function vote(uint256 tokenId) external onlyNewEpoch(tokenId) {  // guarded
function reset(uint256 tokenId) external onlyNewEpoch(tokenId) { // guarded
function poke(uint256 tokenId) external {                         // NO GUARD → infinite FLUX inflation
}
变体 2:检查错误(是否存在,而非是否拥有)
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 steals tokens they don't own
}
变体 3:无提示修饰符(if 而非 require)
// VULNERABLE — non-admin silently gets through:
modifier onlyAdmin() {
    if (msg.sender == admin) {
        _;  // body only executes for admin, but non-admin doesn't revert
    }
}
// CORRECT: require(msg.sender == admin, "Not admin"); _;
变体 4:代理未初始化
function initialize(address _owner) public {  // MISSING: initializer modifier
    owner = _owner;  // anyone can call → become owner
}
// Fix: constructor() { _disableInitializers(); }
Grep 模式
# 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: existence vs ownership?
grep -rn "_requireOwned\|ownerOf\|_isApprovedOrOwner\|_checkAuthorized" contracts/ -B5

# Silent modifiers
grep -rn "modifier\b" contracts/ -A8 | grep -B3 "if (" | grep -v "require\|revert"

# Uninitialized initializer
grep -rn "function initialize\b" contracts/ -A3
grep -rn "_disableInitializers()" contracts/
真实付费案例
协议奖金漏洞
Wormhole$10MUUPS 代理未初始化 → 任何人都能调用 initialize()
ZeroLend不适用split() 检查是否存在,而非是否拥有
Alchemix不适用poke() 缺少 onlyNewEpoch → FLUX 无限增发
Parity冻结 $150M库中的 initWallet() 没有访问控制

3. 代码路径不完整

排名第 3 的 Critical——占 Critical 的 17%。

函数族对比测试
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?
If A does X but B doesn't do the reverse of X → BUG.
变体 1:更新函数遗漏退款(ThunderNFT)
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 {
    // BUG: NO REFUND for sell orders when price decreases → tokens permanently stuck
    orders[orderId] = updatedOrder;
}
变体 2:部分成交的代币被卡住(Plume)
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, not ERC20
}
变体 3:mint() 绕过了 deposit() 中的检查(MetaPool)
function deposit(uint256 assets, address receiver) public override {
    shares = _deposit(assets, receiver);  // includes receipt validation
}
function mint(uint256 shares, address receiver) public override {
    assets = convertToAssets(shares);
    _mint(receiver, shares);  // MISSING: _deposit() validation → mints without receiving assets
}
Grep 模式
grep -rn "function place_\|function create_\|function add_\|function open_" contracts/ -A5
grep -rn "function update_\|function modify_\|function cancel_" contracts/ -A5
grep -rn "safeApprove\b" contracts/    # safeApprove without zero-reset before
grep -rn "delete\b" contracts/ -B5 -A5  # delete before operation completes
grep -rn "function deposit\|function mint\|function withdraw\|function redeem" contracts/ -A10

4. 差一错误与边界条件

排名第 4 的 High——占 High 的 22%。只改一个字符,影响巨大。

根因
// VeChain Stargate — post-exit reward drain:
function _claimableDelegationPeriods(address delegator) internal view returns (uint256) {
    if (endPeriod > nextClaimablePeriod) {  // BUG: should be >=
        return 0;  // exited users get nothing
    }
    return nextClaimablePeriod - lastClaimedPeriod;  // rewards for period AFTER exit
}
每个比较条件都要进行的思考测试

对每个 if (A > B),都问自己:“当 A == B 时会发生什么?”结果是否正确?

需要检查的 6 个边界位置
  1. 周期/纪元边界:周期结束时,> 与 >= 的区别
  2. 基于时间的锁:当 block.timestamp == deadline 时,是锁定还是解锁?
  3. 循环终止条件:使用 > 还是 >= 的 break
  4. 数组索引边界:i <= array.length(应为 i < array.length)
  5. 数量/余额边界:>= amount 是否允许精确全额提款?
  6. 舍入/精度:是否存在某种输入会导致应为非零的输出变为 0?
Grep 模式
# Boundaries in comparisons
grep -rn "Period\|Epoch\|Round\|Deadline\|period\|epoch\|deadline" contracts/ -A3 | grep "[<>][^=]"

# Loop breaks
grep -rn "\bbreak\b" contracts/ -B10

# Off-by-one in array access
grep -rn "\.length\s*-\s*1\|i\s*<=\s*.*\.length\b" contracts/

5. 预言机/价格操纵

占所有报告的 12%。单笔最高奖金。Mango 损失 1.17 亿美元,Curve 损失 7000 万美元。

漏洞 A:缺少数据过期检查(最常见)
// VULNERABLE:
(, int256 price,,,) = priceFeed.latestRoundData();
return uint256(price);  // If Chainlink node goes down, stale price returned indefinitely

// CORRECT:
(, int256 price,, uint256 updatedAt,) = priceFeed.latestRoundData();
require(block.timestamp - updatedAt <= MAX_PRICE_AGE, "Stale price");
require(price > 0, "Invalid price");
漏洞 B:缺少置信区间检查(Pyth)
// VULNERABLE:
PythStructs.Price memory p = pyth.getPriceUnsafe(priceFeed);
return p.price;  // ignores p.conf (confidence interval)

// CORRECT:
require(p.conf * 10 <= uint64(p.price), "Price too uncertain");
// conf > 10% of price = untrustworthy
漏洞 C:TWAP 时间窗口过短(可被闪电贷操纵)
// VULNERABLE: 60-second TWAP
uint32[] memory secondsAgos = new uint32[](2);
secondsAgos[0] = 60; secondsAgos[1] = 0;
// Flash loan can shift price for entire 60s window

// CORRECT: 1800s minimum TWAP (30 min)
漏洞 D:单一来源预言机
// VULNERABLE: only Uniswap spot price
uint price = getUniswapSpotPrice(token);  // flash loan manipulatable

// CORRECT: Chainlink primary, Uniswap TWAP as fallback, require close agreement
Grep 模式
# Missing staleness check
grep -rn "latestRoundData" contracts/ -A5 | grep -v "updatedAt\|timestamp"

# Pyth price usage — confidence interval checked?
grep -rn "getPriceUnsafe\|getPrice\b" contracts/ -A8 | grep -v "conf\|confidence"

# TWAP windows — short TWAP flag
grep -rn "secondsAgo\|TWAP\|cardinality" contracts/ -A5

6. ERC4626 金库攻击

汇率操纵(近乎空仓的金库)
// VULNERABLE — first depositor attack:
// 1. Attacker deposits 1 wei → gets 1 share
// 2. Attacker donates large amount directly (transfer, not deposit)
// 3. Exchange rate: 1 share = (1 + donation) assets
// 4. Victim deposits → rounds down to 0 shares → free donation to attacker

// CORRECT: virtual shares (OpenZeppelin v4.9+)
function _decimalsOffset() internal view virtual override returns (uint8) {
    return 9;  // add 1e9 virtual shares + assets to prevent manipulation
}
ERC4626 转账(转移份额,但不转移质押/锁定记录)
// VULNERABLE: shares transferred, but lock records stay with original owner
// → shares stuck, can't redeem → permanent freeze (Belong pattern)
function transfer(address to, uint256 amount) external override {
    _transfer(msg.sender, to, amount);  // moves shares
    // MISSING: transfer lock record from msg.sender to `to`
}
Grep 模式
grep -rn "function transfer\|function transferFrom" contracts/ -A15
grep -rn "function deposit\|function mint\|function withdraw\|function redeem" contracts/ -A10

7. 重入攻击

2016 年至今。CEI 模式可以防止此类攻击,但它仍然出现在 DeFi 中。

变体
  • 单函数:攻击者在状态更新前重新进入同一函数
  • 跨函数:使用过期状态重新进入同级函数
  • 跨合约:通过回调重新进入另一个协议
  • 只读:重新进入返回过期数据的 view 函数,供攻击者利用
根本原因模式
// VULNERABLE (effects after interaction):
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);
    (bool success,) = msg.sender.call{value: amount}("");  // INTERACTION first
    require(success);
    balances[msg.sender] -= amount;  // EFFECT after → reentrancy window
}

// CORRECT (CEI — Checks, Effects, Interactions):
function withdraw(uint256 amount) external {
    require(balances[msg.sender] >= amount);  // CHECK
    balances[msg.sender] -= amount;            // EFFECT
    (bool success,) = msg.sender.call{value: amount}("");  // INTERACTION last
    require(success);
}
Grep 模式
# External calls before state updates
grep -rn "\.call{value\|safeTransfer\|transfer(" contracts/ -B10 | grep -v "require\|revert"

# Missing nonReentrant modifier on critical functions
grep -rn "function withdraw\|function redeem\|function claim" contracts/ -A2 | grep -v "nonReentrant"

# Storage slot for reentrancy guard
grep -rn "nonReentrant\|ReentrancyGuard\|_notEntered" contracts/

8. 闪电贷攻击

通过闪电贷操纵预言机
// Attack flow:
// 1. Borrow $100M from Aave flash loan
// 2. Dump token in Uniswap pool → crash spot price
// 3. Protocol reads Uniswap spot → undercollateralized loans accepted
// 4. Borrow max against cheap collateral
// 5. Repay flash loan, keep profits
价格预言机合理性检查(检查要点)
grep -rn "getReserves\|getAmountsOut\|slot0\b" contracts/ -A5
# spot price from reserves = manipulatable with flash loan
# slot0 = Uniswap V3 spot price = manipulatable

9. 签名重放

缺少 Nonce
// VULNERABLE:
function permit(address owner, address spender, uint256 value,
                uint256 deadline, uint8 v, bytes32 r, bytes32 s) external {
    bytes32 hash = keccak256(abi.encodePacked(owner, spender, value, deadline));
    // MISSING: nonce not included → same signature usable multiple times
    require(ecrecover(hash, v, r, s) == owner);
}
缺少 Chain ID
// VULNERABLE: signature valid on mainnet AND testnet AND all forks
bytes32 hash = keccak256(abi.encodePacked(params));
// MISSING: block.chainid not in hash → works on any chain
Grep 模式
grep -rn "ecrecover\|ECDSA\.recover" contracts/ -B20
# Check: does the signed hash include nonce + chainId + contract address?

grep -rn "nonce\|_nonces\|nonces\[" contracts/

10. 代理/升级问题

存储冲突
// Implementation and proxy share storage layout
// Proxy slot 0: _owner
// Implementation slot 0: _initialized
// → writing to _initialized overwrites _owner
未初始化的实现
// If implementation can be initialized directly → anyone becomes owner of implementation
// Attack: call initialize() on implementation contract → call upgradeTo() → replace logic
对用户可控地址执行 delegatecall
function execute(address target, bytes calldata data) external onlyOwner {
    target.delegatecall(data);  // target is validated, but what if owner is compromised?
}
Grep 模式
# UUPS initialization protection
grep -rn "function initialize\b\|_disableInitializers\|initializer" contracts/

# Delegate call
grep -rn "delegatecall\b" contracts/ -B3 -A5

# Storage layout — proxy uses EIP-1967 slots?
grep -rn "0x360894\|EIP1967\|_IMPLEMENTATION_SLOT" contracts/

Foundry PoC 模板

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/VulnerableContract.sol";

contract ExploitTest is Test {
    VulnerableContract target;
    address attacker = makeAddr("attacker");
    address victim = makeAddr("victim");

    function setUp() public {
        // Fork mainnet at specific block
        vm.createSelectFork("mainnet", BLOCK_NUMBER);

        // Deploy or load target
        target = VulnerableContract(TARGET_ADDRESS);

        // Fund accounts
        deal(address(token), attacker, INITIAL_BALANCE);
        deal(address(token), victim, VICTIM_BALANCE);
    }

    function test_exploit() public {
        console.log("Attacker balance before:", token.balanceOf(attacker));

        vm.startPrank(attacker);

        // Step 1: Setup conditions
        // Step 2: Execute exploit
        // Step 3: Verify impact

        vm.stopPrank();

        console.log("Attacker balance after:", token.balanceOf(attacker));
        assertGt(token.balanceOf(attacker), INITIAL_BALANCE, "Exploit failed");
    }
}
主要 Foundry Cheatcode
vm.prank(address)           // next call from address
vm.startPrank(address)      // all calls from address until stopPrank()
vm.deal(address, amount)    // set ETH balance
deal(token, address, amount) // set ERC20 balance
vm.warp(timestamp)          // set block.timestamp
vm.roll(blockNumber)        // set block.number
vm.createSelectFork("mainnet", blockNumber)  // fork mainnet
vm.expectRevert(bytes)      // next call should revert
vm.label(address, "name")   // label for trace output
vm.assume(condition)        // fuzz: discard inputs where false
运行测试
# Run specific test
forge test --match-test test_exploit -vvvv

# Run with fork
forge test --match-test test_exploit -vvvv --fork-url $MAINNET_RPC

# Gas report
forge test --gas-report

# Coverage
forge coverage --report summary

相关技能与链

  • meme-coin-audit — 当目标是 Meme 币/SPL 代币,而非 DeFi 协议时使用。工作流要点:下潜前的终止信号有所不同——本技能的“TVL < $500K 时跳过”不适用于 Meme 币,因为拉地毯检查(铸造权限、冻结权限、LP 锁定)就是完整审计;应改用 meme-coin-audit。
  • triage-validation — 当合约发现已准备好提交至 Immunefi 时使用。工作流要点:Immunefi 有自己的报告格式,但 triage-validation 中经过影响验证、端到端覆盖链路的严谨方法仍然适用;提交前,先对 Foundry PoC 运行 7Q 检查。
  • report-writing — 撰写 Immunefi 报告正文时使用。工作流要点:report-writing 的 Immunefi 模板(包含 Foundry PoC、根因代码片段、量化的经济影响)是本技能发现的问题所对应的正文框架。
  • offensive-osint — 审计协议的链下攻击面(前端、管理员 API、RPC 网关)时使用。工作流要点:链上审计由本技能负责;协议的任何 web2 组件(web 前端、管理面板、索引器 API)都应转交给 offensive-osint 进行侦察。
  • bb-methodology — 决定是否要开始深入审计时使用。工作流要点:bb-methodology 的 PART 0 用于确认委托类型(web3 漏洞赏金/私人审计/智能合约审查);对于此类委托,本技能的下潜前终止信号取代标准评分标准。

操作人员备注(Claude-BugHunter)

根据实际委托总结,并针对 2026 年补充了特定内容,作为 vendored 基础内容的扩展。 内容源自真实的授权委托,以及对本仓库 31+ 个技能领域实时测试的第 2 阶段验证。 上游内容涵盖“是什么”; 本层内容涵盖“何时有效”与“何时无效”。

2026 年仍有收益的漏洞类型

闪电贷攻击仍是 Immunefi 上赏金最高的漏洞类型之一(按赏金金额计算,2024–2026 年排名前 5)。这种经济原语——借入 5,000 万美元、操纵价格预言机、抽干资金池、偿还贷款——一再出现,因为新协议不断上线,却仍假定可组合性在闪电贷造成的失衡情况下依然成立。

重入攻击仍有赏金,因为新协议不断上线并使用 ERC-777、钩子和回调。不要以为这类漏洞已经绝迹——2023–2025 年的已付赏金漏洞中,有 40 多个重入漏洞出现在采用检查-效果-交互模式之后的代码库中(跨函数重入,以及通过状态变更过程中的视图函数调用引发的只读重入)。

预言机操纵:仍有大量赏金,但难度更高。现在大多数项目都使用 Chainlink 价格馈送;攻击目标是大多数项目也会使用的次级预言机(来自低流动性 Uniswap V2 交易对的 TWAP、协议自身的内部预言机、过时的备用路径)。审计故障转移链路,而不只是主价格馈送。

自托管内容编写后新增的变化
  • EIP-1153(瞬态存储) — 于 2024 年引入。新增重入漏洞类型:在同一笔交易中缓存的瞬态存储读取结果可能与持久存储不同步。审计所有 tload/tstore 用法,检查外部调用后是否存在读取操作。
  • EIP-7702(2025 年 Pectra 硬分叉) — 新增 EOA 到智能账户的升级能力。重新委托可引入类似 ATO 的原语:EOA 一次签名后即可委托给攻击者控制的合约,随后可在不同委托之间重放签名。
  • 账户抽象(ERC-4337 打包器) — 赞助者滥用和打包器骚扰攻击。未严格限制发送者白名单的赞助者合约会在首次调用时被掏空。
  • ZK Rollup 桥接漏洞 — 跨 Rollup 证明重放、链下证明者遭入侵,以及排序器审查导致的强制纳入边缘情况。
  • LST/LRT 脱锚动态 — 假定损失情况下仍维持 1:1 锚定的流动性质押和流动性再质押代币;预言机假定锚定状态,市场反映脱锚情况,清算逻辑随之失效。
2026 年工具栈

Foundry 仍是测试框架。使用 forge test --gas-report --debug 进行不变量测试;使用 forge fuzz 进行基于属性的测试;使用 forge inspect 审计存储布局。使用 Slither + Echidna 进行静态分析和模糊测试。对较小的合约使用 Mythril 进行符号执行。使用 tenderly.co 进行分叉和模拟(在主网状态下复现攻击的最佳 UX)。

对于 Solana:使用 anchor 框架、sealevel-attacks 漏洞集合(由 anchor 维护者整理的 PoC)以及 soteria-sec / sec3 扫描器。对于 Move(Aptos、Sui):使用 move-prover、aptos-cli aptos move test。

对于跨链项目:hyperlane 和 LayerZero 都有审计工具仓库;桥接漏洞需要模拟两个端点,而不只是其中一个。

预审查中的排除信号何时重要

TVL 低于 50 万美元的项目不值得花时间审计,除非赏金下限很高。项目已由审计公司审计 = 投资回报率低,除非你能发现他们遗漏的问题——查看审计报告中的范围排除部分,了解他们明确未审计的内容(预言机、治理、链下组件、前端、管理路径)。

多签签署者超过 5 人 + 时间锁超过 48 小时 = Rug Pull 风险较低;如果你的发现依赖团队存在恶意行为的假设,那么影响较低,而且根据 Immunefi 的“中心化风险”排除条款,很可能不在范围内。阅读项目说明——大多数 Immunefi 项目都会明确降低或拒绝那些假定管理员有恶意行为的发现。

报告规范

Immunefi 要求提供 Foundry PoC。未提交 PoC 的报告会被自动拒绝。需要手动设置(“先部署这个,再调用那个”)的 PoC 通常会被降级——PoC 应当通过一次 forge test 调用证明影响,并使用明确的 assertEq 断言被清空的余额、铸造的代币或遭破坏的状态。

严重性声明必须严格遵循 Immunefi 的严重性矩阵;不要自行编造严重性等级。矩阵根据 TVL 的直接经济损失百分比划分等级——针对 50 万美元协议的 Critical,与针对 5 亿美元协议的 Critical,奖金不同。声明 Critical 前,请先阅读该计划的具体严重性分配标准。

评论

支持 Markdown