原文タイトル:《見積もり0%、執行12.8%:Uniswap v4「毒池」動的手数料率トラップの振り返り》
原文著者:王子豪、BitsLab
2026年8月22日から23日にかけて、BNB Chain上のUniswap v4 USDC/WBNB動的手数料率プールは、アグリゲーターの見積もりでは0%の手数料率を表示していたが、ユーザーの実際の約定では12.8%のLP feeが徴収された。約29時間の間に、このプールは21,086件の取引を成立させ、累計手数料は131,888ドルに達し、その後注销された。被害を受けた取引はすべて成功していた。コントラクトは盗まれておらず、出力はユーザーが設定した下限を上回っており、異常なシグナルは一切なかった——その差額は静かに手数料へと変わっていた。
本記事では、そのうちの1件のサンプル取引を入口とする:
(0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0:ユーザーは5,926.90 USDTを投入し、受取額は予想より757.43 USDT少なかった)。このプールがどのようにして「見積もり0%、執行12.8%」を実現したのかを分解する:手数料率関数の三層判定、形骸化されたスリッページ保護、他のLPを締め出すホワイトリスト。すべての結論はオンチェーンデータとローカルでの制御された検証に基づく。
ユーザーがアグリゲーターのフロントエンドでスワップを開始した後、経路全体は次のように進む:

この経路には2つの背景的事実があり、以降の各層の判定はこの2点の上に成り立っている。
まずv4側を見る:Hookごとに手数料率を上書きできる。PoolKey.fee = 0x800000は動的手数料率プールを宣言し、HookはbeforeSwapの3番目の戻り値で0x400000 | feeを伴う。v4-coreにおいてこの戻り値を受け取るコード(Pool.sol:303-305):

isOverride()は上書きフラグを確認し、removeOverrideFlagAndValidate()は0x400000を剥がしてMAX_LP_FEE = 1,000,000(100%)を超えないことを検証し、その手数料率で本件のswapを執行してSwapイベントに書き込む。つまり、Hookが何を返そうと、ユーザーはそれを支払う——出力が依然としてamountOutMinを上回っている限り。これは正式な機能であり、ボラティリティや在庫に応じて手数料率を調整することは正当な用途である。
次にこの Hook 自体を見てみましょう。宣言されている権限はわずか2つだけです。v4 では権限がコントラクトアドレスの下位14ビットにエンコードされ(Hooks.sol:29-47)、デプロイ時に検証され、後から変更することはできません。v4-core 側のフラグ定義とアドレスデコードは次のように対応しています:

オンチェーンで実際に getHookPermissions() を呼び出すと、ビットマップと一致する結果が返ります。この2ビットのみが true です。
Hook にはソースコードがありません。本記事ではバイトコードから手数料関数の全文を逆コンパイルしました——以下は、すでにオンチェーンで稼働したことのあるこのロジックの動作分析です:

次に層ごとに見ていきます。最初の2層が答えるのは「これはシミュレーションかどうか」であり、第3層が実際の取引でいくら支払うかを決定します。
手数料関数が呼び出されると、まず tx.origin を照合します:3つの定数アドレスのいずれかと等しければ、そのままフォールバック档を返します。フォールバック档は現在 0% に設定されています。
3つのアドレスは適当に選ばれたものではありません。eth_call で from を指定しない場合、取引の origin はゼロアドレスになります——アグリゲーターの一括見積もりでは通常指定されません。ローカルフォークで直接検証:

第1層はこの3つのアドレスのみを認識します。次の層では、通常のアドレスを指定した後、Hook がどのように区別できるかを見ていきます。
tx.origin を通常のアドレスに変えて再試行しても、返り値は依然として 0% です——第1層は回避されたのに、手数料率は変わりません。これは第1層が唯一の判定ではないことを示しており、第2層を特定します:gasleft() * 10100 / 10000 >= gate。
アグリゲーターの見積もりは高い gas を与える習慣があり、候補ルートがシミュレーション中に gas 不足で失敗するのを防ぎます。実際の取引が Hook に入る前に、gas は Router、承認、前置ジャンプによってすでに消費されています。同じコードで、3種類の gas:

境界線は実測で 16.85M~16.9M gas の間に位置します。30M と 2.59M の差は、まさに「見積もり」と「実行」の差です。
前の二層を通過した実際の取引は第三層へ進む:ブロック環境のフィンガープリントをハッシュ化して10,000で割った余りを求め、その結果が属する区間に応じて対応する手数料率ティアを返す。現在の設定は三档8% / 10% / 10%で、該当なしの場合は0%にフォールバックする。
この層には注目すべき設計が二つある。フィンガープリントにはcalldataの三つのフィールドが混入されている——アグリゲーターの見積もりcalldataと実際のルーティングcalldataは元々異なるため、フィンガープリントも当然異なる。また、取引ごとの疑似ランダムにより手数料率に分布が生じ、数件を見ただけでは規則性を察知しにくく、単純なルール列挙では防げない。
全21,086件のオンチェーンイベント中に19档の履歴手数料率(6.8%–28%)が出現しており、手数料率ティアが管理者関数によって随時再設定され続けていることを示している。サンプル取引の約定時に有効だったのは12.8%である。

ここまでで「毒池」に定義を与えられる——三つの条件を同時に満たすv4プール:PoolKey.feeが動的手数料率フラグを帯びている;手数料率の決定が公開市場状態ではなく実行環境シグナル(gas、origin、ブロック環境)を読み取る;見積もりシミュレーションとオンチェーン実行の手数料率に系統的な分岐が生じ、受益者が毒池のデプロイヤーである。分岐点は手数料率が何を読むかにある:いかなる呼び出し元にも同一の手数料率を返すものはプログラマブル市場設計であり、「誰が値段を聞いているか」によって異なる手数料率を返すものはルーティングシステムに対する欺瞞である。
ここまでで、手数料率分岐のメカニズムは完全に解明された。しかし、手数料率が徴収された後、なぜ取引が依然として成功するのか、それが次の問題である。
まずこの取引の完全な経路と金額を見てみよう:

該当するSwapイベントは有効な手数料率をfeeフィールドに記録している:128000。v4の手数料率単位は1,000,000を100%とするため、128000は12.8%である。この取引の損失は二つの角度から測定できる:プールから見ると、手数料率はWBNB入力に対して徴収される;ルート全体から見ると、ステーブルコインの出入り差額は757.43 USDTで、12.7794%の損失であり、その中には前段のホップ手数料とペッグ乖離も含まれる。二つの数字はわずか0.02パーセントポイントしか違わず、損失の主要な来源がこのプールのLP feeであることを示している。
amountOutMinは最終出力の下限のみを検証し、中間の各ホップでどれだけ手数料が徴収されたかは見ない:

12.8% の手数料率は全体が 20.32% のバッファ内に収まり、出力は依然として下限を上回っている——取引は成功し、ロールバックしない。主流のステーブルコインルートの通常のデフォルトは 0.1%~1% であり、このルートは 20 倍以上の余地を与えている。ユーザーは広いバッファを「安定性」だと思っているが、実際にはパス上のすべてのコントラクトを相手側の自主性に委ねている。この計算を図にすると:

ホワイトリストのロジックは beforeAddLiquidity 内にあり、逆コンパイル結果は以下の通り:

流動性の追加には三道の関門を通過する必要がある:まず、どの v4 Hook にもある PoolManager 呼び出しチェック、次に公式 PositionManager を経由すること、最後にポジション保有者のホワイトリストを照合すること。リストにないアドレスはここでコントラクトによってロールバックされ、他の LP は入れず、手数料収入は希薄化されず、すべて展開者に帰属する。リスト自体は管理者関数(セレクタ 0xc4452e52)によってアドレスごとに維持される。
21,086 件の取引をまとめて見ると、分布は明確なパターンを示している:0% 档 6,946 件、約定 12,947,751 ドル、1 件あたり平均 1,864 ドル;手数料档 14,140 件、約定 1,120,106 ドル、1 件あたり平均 79 ドル。大口約定は 0% 档に集中し、手数料は少額取引に集中している。大口 0% の最も合理的な説明は、展開者自身が高いガスで自己約定していることである:高額ガスとアグリゲーターのクォートが命中するのは同じ判定であり、自己約定の手数料コストはほぼゼロである。稼ぎ出された出来高はプールを相場サイトのランキングに押し上げ(スナップショットでは 24 時間出来高約 1,082 万ドル、16,671 件)、アグリゲーターの目には、これは深度が良く手数料率が低いプールとして映る。
判定三で 19 档の履歴手数料率に言及したが、それらは逆コンパイルされた手数料設定関数(セレクタ 0x4d909a45、公開シグネチャ未収録)に由来する:

四つの手数料档と閾値が全体としてパッケージ化され、keccak(poolId, 2) というストレージスロットに書き込まれる。オンチェーンで実測したこのスロットの読み取り値は 0x0138800186a00186a0 であり、関数のパッケージ形式とセグメントごとに対応している。手数料档はここではいつでも調整可能なパラメータであり、展開時に固定されるものではない。
このプールは08-22 06:57に作成され、07:13に最初のスワップが発生し、21:39が本記事のサンプル取引である。08-23 12:25に最後の約定があり、その後注销された。アクティブ期間は約29時間で、総取引額は14,067,857ドル、手数料収入は131,888ドル(デプロイヤーのコスト未控除)。再確認時には登録フラグが有効に戻っており、プールはまだ存在し、いつでも再度有効化できる。
攻撃チェーン全体をつなげると:

チェーン全体の因果関係は上の図にすべて示されており、三つの条件がすべて揃う必要がある:シミュレーションが最初の二層を命中させること、実際の取引が第三層に落ちること、バッファが手数料率より大きいこと。いずれか一つでも成立しなければ、この手法は機能しない。
第一のポイントはアグリゲーターのクォート方式にある。根本原因はシミュレーションと実行が同じ経路を通っていないことだ:クォートは理想化されたプールレベルのクエリを使用し、ゼロアドレスと高いgasを組み合わせている;実際の取引は実際のアイデンティティとすでに消費されたgasを伴う。Hookはまさにこれらの差異を読み取ることができ、手数料率が分岐する。対処法はシミュレーションを実行に近づけることだ——実際に送信するRouter calldata、実際のfrom/to、オンチェーン取引に近いgasでシミュレーションを行う;約定後にSwapイベントをデコードしてfeeを照合し、クォートと一致しなければ重みを下げるか下线させる。
第二のポイントはユーザーのスリッページ設定にある。根本原因はmin_outが緩すぎることだ:20.32%のバッファが12.8%の手数料率を完全に飲み込み、取引は通常通り成功する。主流のステーブルコインルートでは0.1%–1%で日常使用に十分であり、取引を发起する前にこの数値を確認すべきだ;ウォレットとフロントエンドがデフォルト値を厳しくすることで、ユーザーの大部分のリスクを防ぐことができる。
第三のポイントはルーティング准入にある。根本原因は任意の動的手数料率プールが直接クォート競争に参加できることだ:オープンソースでもなく、監査記録もないHookでも、水増しされた出来高によって推薦ルートに入ることができる。信頼できるプロファイルのない動的手数料率プールについては、デフォルトでルーティングに入れないか、显著に重みを下げることが最も稳妥な処理である。
·サンプル取引:
https://bscscan.com/tx/0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0(ブロック 117497524)
・プール初期化トランザクション:
https://bscscan.com/tx/0x8fa72ef72d77b61f715dc9ce90548249ab045c6d78716078cd5ab425bbe69a05
・PoolManager:
0x28e2ea090877bf75740558f6bfb36a5ffee9e9df
・Pool ID:
0x36e5540e9dedc02229fe8a82aa5b10c0bf07d1fa74e4f2ffe0efd00fa1a36aea
・Hook:
0xd111b3ddd92e627f1864520c770e913ec04e0880(ソースコードなし、疑似コードは本文が独自に逆コンパイルしたもの。管理者 0x08b03e1a5444d469f4dc954e74d3f662c94a6b13)
・約定と手数料:当該プールの全21,086件のSwapイベントを1件ずつ累計(eth_getLogs)、WBNBは同一トランザクションの執行価格で換算。収入はグロス値であり、デプロイヤーのコストは差し引かれていない
BlockBeats の公式コミュニティに参加しよう:
Telegram 公式チャンネル:https://t.me/theblockbeats
Telegram 交流グループ:https://t.me/BlockBeats_App
Twitter 公式アカウント:https://twitter.com/BlockBeatsAsia