web3-audit について
web3-auditは、セキュリティ研究者向けのスマートコントラクト監査スキルです。対象の選別方法、脆弱性パターン、コード検索例を使ってWeb3プロトコルを確認できます。
主な機能
- 01監査前の対象選別
- 02スマートコントラクトの10種のバグ
- 03Solidityの検索パターン
- 04関連関数の比較
- 05Foundry PoCテンプレート
- 06実際の報奨金付き脆弱性例
活用シーン
- 01監査前の報奨金対象の選定
- 02会計処理とアクセス制御の確認
- 03関連するコントラクト経路の比較
- 04概念実証テストの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill web3-auditインストール後、Claude Code で/web3-auditを入力して読み込まれたか確認します。
詳細
Web3 スマートコントラクト監査
10 のバグクラス。着手前の中止シグナル。Foundry PoC テンプレート。実際の報奨金獲得事例。
着手前の中止シグナル(コードレビューの前に必ず確認)
ZKsync の教訓:TVL 3 億 2,200 万ドル + OZ 監査 + 75 万 LOC + 5 セッション = 発見ゼロ。大規模で十分に監査されたブリッジは、非常に攻略が困難です。
- TVL < 50 万ドル → 最大報奨金が、労力に見合わないほど低い
- トップクラスの監査が 2 件以上(Halborn、ToB、Cyfrin、OpenZeppelin)実施されているシンプルなプロトコル → バグはすでに発見済み
- プロトコルが 500 行未満で、単一の A→B→C フロー → 攻撃対象領域が最小限
- 計算式:
max_realistic_payout = min(10% × TVL, program_cap)— 1 万ドル未満なら見送る
ソフト中止条件: 現行バージョンに対する OZ/ToB/Cyfrin の監査があり、かつコードベースが 50 万 LOC を超える場合 → 1 件見つかるかどうかで 40 時間以上かかる見込みです。報奨金の下限が 5 万ドルを超え、かつそのプロトコル固有の専門知識がある場合に限り、進めてください。
ターゲットのスコアリング(6/10 以上なら着手):
- TVL > 1,000 万ドル:+2
- Critical の報奨金が 5 万ドル以上の Immunefi プログラム:+2
- 現行バージョンに対するトップクラスの監査がない:+2
- デプロイから 30 日未満:+1
- 以前に調査したことがあるプロトコル:+1
- ソースコードと natspec コメントがある:+1
- アップグレード可能なプロキシ:+1
唯一のルール
「すべての関連関数を読むこと。
vote()にモディファイアがあるなら、poke()、reset()、harvest()も確認する。関連関数にモディファイアがないこと、それ自体がバグです。」
このルールだけで、Critical の発見事例全体の 19% を説明できます。
1. 会計状態の不整合
Critical バグクラスの第 1 位 — Immunefi の Critical 全体の 28%。
内容
同期を保つべき 2 つの状態変数があります。あるコードパスで 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. アクセス制御
Critical の第 2 位 — Critical 全体の 19%。2024 年だけで 9 億 5,300 万ドルが失われています。
パターン 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 | 1,000 万ドル | UUPS プロキシが未初期化 → 誰でも initialize() を呼び出せる |
| ZeroLend | なし | split() が所有権確認ではなく、存在確認を使用 |
| Alchemix | なし | poke() に onlyNewEpoch がない → FLUX が無限に増加 |
| Parity | 1 億 5,000 万ドル相当が凍結 | ライブラリ内の initWallet() にアクセス制御がない |
3. 不完全なコードパス
Critical の第 3 位 — 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. オフバイワンと境界条件
High の第 4 位 — High 全体の 22%。たった 1 文字の変更で、大きな影響が生じます。
根本原因
// 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 つの境界箇所
- 期間/エポックの境界:期間終了時に
>と>=のどちらを使っているか - 時間ベースのロック:
block.timestamp == deadlineのとき、ロックされるか解除されるか - ループの終了条件:
breakで>と>=のどちらを使っているか - 配列インデックスの境界:
i <= array.length(i < array.lengthにすべき) - 数量/残高の境界:
>= amountで全額の引き出しが可能になるか - 丸め処理/精度:ゼロ以外になるべき出力が 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 億 1,700 万ドル、Curve で 7,000 万ドル。
バグ 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 チートコード
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
検証
- Solana ツール — Anchor/SPL ツールが利用可能であることを確認します:
which anchor 2>/dev/null && echo "PASS: anchor CLI found" || echo "NOTE: anchor not installed" which solana 2>/dev/null && echo "PASS: solana CLI found" || echo "NOTE: solana CLI not installed" - 一般的な Solana の脆弱性クラス — 主要なクラスが文書化されていることを確認します:
grep -q "account confusion\|signer check\|CPI\|PDA\|bump seed" SKILL.md && echo "PASS: Solana-specific vuln classes present" || echo "FAIL"
すべてのテストで Web3 監査への準備状況を検証します。
注意点
- Solana と EVM の混同 — Solana のプログラミングモデル(Rust、BPF)は、EVM(Solidity、EVM バイトコード)とはまったく異なります。手法を相互に流用しないでください。
- 非 EVM チェーンにおける再入攻撃 — Solana のランタイムは、ブロック内のトランザクションを順番に処理します。クロスプログラム呼び出し(CPI)による再入は、EVM の再入攻撃とは異なります。
- Solana におけるアカウントの混同 — Solana のアカウントモデルでは、アカウントを明示的に渡す必要があります。署名者チェックの欠落やアカウント検証の不備が、Solana の主な脆弱性クラスです。
- 任意の CPI による脆弱性 — ユーザーが制御するプログラム ID を
invoke()またはinvoke_signed()に渡すと、任意のプログラムを呼び出せます。 - PDA(プログラム派生アドレス)の誤用 — 適切なシード検証を行わない PDA は偽造される可能性があります。型チェックなしでユーザー入力をシードに使うのは危険です。
関連スキルとチェーン
meme-coin-audit— 対象が DeFi プロトコルではなく、ミームコイン/SPL トークンの場合に使用します。ワークフローの要点:事前調査での中止判断が異なります。このスキルの「TVL < $500K ならスキップ」という条件は、ミームコインには当てはまりません。ミームコインではラグチェック(ミント権限、凍結権限、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 バグバウンティ/非公開監査/スマートコントラクトレビュー)を確認します。このスキルの事前調査における中止判断は、その案件種別に対する標準の評価基準に代わるものです。
オペレーター向けメモ
実際のエンゲージメントから得た知見と、2026年固有の追加事項を、ベンダー提供の基盤に加えています。 実際に許可を得たエンゲージメントからの知見と、このリポジトリにある31以上のスキル領域のライブテストを通じたフェーズ2の検証に基づいています。上流のコンテンツがWHATを扱うのに対し、 このレイヤーでは「うまくいくとき」と「うまくいかないとき」を扱います。
2026年も報奨金につながっているバグの種類
フラッシュローン攻撃は、Immunefiで依然として報奨金上位です(2024~2026年の報奨金額上位5位以内)。その経済的な手法――5,000万ドルを借り、価格オラクルを操作し、プールから資金を引き出して返済する――が何度も繰り返されるのは、新しいプロトコルが次々とリリースされ、フラッシュローンで生じる不均衡のもとでは成り立たないコンポーザビリティの前提を置いているためです。
リエントランシーは今も報奨金につながっています。新しいプロトコルがERC-777、フック、コールバックを使ってリリースされ続けているためです。この脆弱性の種類はもうなくなったと考えないでください。2023~2025年の報奨金対象コーパスには、Checks-Effects-Interactionsを実装したコードベースにおけるリエントランシーのバグが40件以上含まれています(関数をまたぐリエントランシー、状態更新の途中で呼び出されたview関数を介する読み取り専用リエントランシーなど)。
オラクル操作は依然として多くの報奨金につながりますが、難易度は上がっています。現在、ほとんどのプロジェクトがChainlinkの価格フィードを使っています。攻撃対象となるのは、ほとんどのプロジェクトが併用している二次オラクルです(流動性の低いUniswap V2ペアのTWAP、プロトコル独自の内部オラクル、古い値に依存するフォールバック経路など)。プライマリフィードだけでなく、フェイルオーバーの経路も監査してください。
ベンダー提供コンテンツの作成後に加わったこと
- EIP-1153(一時ストレージ) — 2024年に導入されました。新たなリエントランシーの種類として、同じトランザクション内でキャッシュされた一時ストレージの読み取り値が永続ストレージと同期しなくなる可能性があります。
tload/tstoreの使用箇所では、外部呼び出し後の読み取りを監査してください。 - EIP-7702(2025年のPectraハードフォーク) — EOAをスマートアカウントにアップグレードできるようになりました。再委任によるATO類似の手法が新たに生まれています。一度署名したEOAを攻撃者が制御するコントラクトに委任し、その後、委任をまたいで署名を再利用できます。
- アカウント抽象化(ERC-4337のバンドラー) — ペイマスターへのスポンサーシップ悪用や、バンドラーへのグリーフィングが問題になります。送信元の許可リストを厳格に適用しないペイマスターコントラクトは、最初の呼び出しで資金を流出させます。
- ZKロールアップのブリッジバグ — ロールアップ間での証明のリプレイ、オフチェーンの証明者の侵害、強制包含のエッジケースにつながるシーケンサーによる検閲などがあります。
- LST/LRTのデペグ動向 — 損失が発生しても1対1のペッグを前提とするリキッドステーキングトークンやリキッドリステーキングトークンがあります。オラクルはペッグを前提とする一方、市場ではデペッグが反映され、清算ロジックが破綻します。
2026年のツールスタック
テストフレームワークには引き続きFoundryを使用します。不変条件テストにはforge test --gas-report --debug、プロパティベーステストにはforge fuzz、ストレージレイアウト監査にはforge inspectを使います。静的解析とファジングにはSlitherとEchidna、小規模なコントラクトのシンボリック実行にはMythrilを使います。メインネットの状態を使ったフォークとシミュレーション(攻撃の再現に最適なUX)にはtenderly.coを使います。
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時間を超える場合、ラグプルのリスクは低くなります。発見内容がチームの悪意を前提とするなら、影響度は低く、Immunefiの「中央集権化リスク」の除外規定により対象外となる可能性が高いです。プログラムの概要を読んでください。Immunefiのプログラムの多くは、管理者の悪意を前提とする発見内容を明示的に低く評価するか、却下します。
報告に関する規律
ImmunefiではFoundry PoCが必須です。PoCのない提出は自動的に却下されます。手動セットアップ(「まずこれをデプロイしてから、あれを呼び出す」)が必要なPoCを添付した提出は、通常、重大度が引き下げられます。PoCは、影響を証明する単一の forge test 呼び出しである必要があり、流出した残高、ミントされたトークン、破損した状態に対する明示的な assertEq を含めてください。
重大度の主張には、Immunefiの重大度マトリックスをそのまま使用してください。独自の重大度を作らないでください。このマトリックスは、TVLに対する直接的な経済的損失の割合を基準としています。500Kドルのプロトコルに対するクリティカルと、500Mドルのプロトコルに対するクリティカルでは、報奨額が異なります。クリティカルと主張する前に、プログラム固有の重大度割り当てを確認してください。