ether.fi LiquidのAtomicQueue.solve()が呼び出し元指定のsolverを検証せず、既存approve経由で資金が引き抜かれる

何が起きたか
SlowMistの記録によれば、2026年9月11日、ether.fi Liquid(liquidETH)の利用者が被害を受けた。AtomicQueue.solve()において、呼び出し元が指定するsolverパラメータにアクセス制御が掛かっていなかったことが原因とされる。攻撃者は細工したAtomicRequestを構成し、すでに当該コントラクトへ承認(approve)を与えていた被害者アドレスをsolverの位置に置くことで、残存していたERC-20のallowanceをtransferFromで引き出した。影響を受けたのは約11名、被害額は約15.45 ETH(約38,130ドル、1ドル=152.27円換算で約581万円。為替は2026年9月13日時点のGoogle Finance値。この金額から逆算されるETH価格は約2,468ドル)である。
AtomicQueueは、複数の償還・交換要求をまとめて処理し、solverと呼ばれる主体が実際の資産移動を実行する設計である。solverが誰であるかは通常オフチェーンの都合で変わりうるため、パラメータとして受け取ること自体は珍しくない。問題は、受け取ったアドレスが「この操作について資金の出し手になることを承認しているか」を検証していなかったことにある。
同じ週に、構造的に同一の事例がBNB Chainで起きている。SlowMistによれば、2026年9月7日、名称非公表のDEXルータでuniswapV3SwapCallbackが本物のV3プールからの呼び出しであることを認証しておらず、攻撃者は偽のプールを用意し、被害者をpayerに指定して既存のallowanceをtransferFromで消化した。1トランザクションで29のウォレットから約62.28 WBNB(約46,070ドル=約702万円)が抜かれている。
本稿執筆時点で、ether.fiおよび該当ルータ側からの公式ポストモーテムは確認できていない。以下の整理は第三者分析に基づく。
共通する欠陥の形
二つの事例は、対象プロトコルもチェーンも異なるが、欠陥の形は同一である。すなわち、呼び出し元がcalldataで指定したアドレスを資金の出し手として扱いながら、そのアドレスが当該操作を承認したかどうかを検証していないという形である。ERC-20のallowanceは「このコントラクトが私の残高から引き出してよい」という許可であって、「このコントラクトが誰の指示でも私の残高から引き出してよい」という許可ではない。しかしコントラクト側がその区別を実装していなければ、両者は同じものになる。
この形が危険なのは、被害者側に落ち度らしい落ち度がない点である。approveを与えた相手は正規のプロトコルであり、フィッシングサイトに署名したわけでも、怪しいコントラクトを承認したわけでもない。攻撃の起点は自分ではなく、承認先のコードの中にある。ユーザー教育で防げる種類の事故ではない。
さらに、無制限approve(uint256の最大値)の慣行がこの欠陥の破壊力を増幅する。ガス代を節約するために一度だけ無制限に承認するというパターンは長年推奨されてきたが、それは承認先コントラクトが将来にわたって正しく引数を検証し続けることに賭ける行為でもある。今回のように引数検証が一箇所抜けただけで、その賭けは負けになる。被害額が小さかったのは、たまたま残っていたallowanceの総量が小さかったからにすぎない。
承認モデルそのものの限界
ERC-20のapproveは、金額の上限だけを表現する。誰の指示で、どのような操作のために、いつまで引き出してよいかは表現できない。この表現力の乏しさが、承認先コントラクトのコードにすべての責任を集中させている。近年はPermit2やEIP-2612のように、有効期限や署名ベースの限定的な承認を導入する動きがあるが、これらも「承認先が引数を正しく検証する」という前提そのものは置き換えない。期限が付けば被害の時間的な窓は狭まるが、期限内に同じ欠陥が突かれれば結果は変わらない。
実務上、この構造への対処は二層になる。一層目はコントラクト側で、資金の出し手をcalldataから受け取る設計を避けるか、受け取るなら必ず出し手側の明示的な同意(署名、事前登録、msg.senderとの一致)を検証すること。二層目はユーザー側で、未使用の承認を残さないこと。前者が守られていれば後者は不要だが、後者が守られていれば前者が破れても被害は限定される。二層とも実装コストが低いにもかかわらず、どちらも省略されがちである。
検出の観点も付け加えておく。今回のような攻撃は、被害者のウォレットから見れば「自分が出していないトランザクションによる残高減少」として現れる。approve先ごとの残allowanceと、そこからの引き出し実績を監視していれば、初回の1件で検知できた可能性が高い。プロトコル側でも、solverやpayerの位置に立つアドレスの分布を監視していれば、通常は数個に収まるはずのアドレスが突然多様化した時点で異常が見える。
想定される落とし穴
第一に、この種の欠陥は「資金がコントラクトに預けられているか」では測れない。TVLがゼロのコントラクトでも、allowanceが残っていれば引き出せる資産がある。監査やリスク評価がロックされた残高だけを見ている場合、承認残高という第二の攻撃面を見落とす。
第二に、コールバックやソルバーのような「外部から呼ばれることを前提とした関数」は、検証を忘れやすい。設計上は「プールから呼ばれる」「ソルバーが呼ぶ」と想定されているため、呼び出し元が想定どおりであることの検証が自然言語のコメントで済まされ、コードに落ちていないことがある。Uniswap V3系のコールバックについては、呼び出し元が正規のプールであることを検証する定型があり、これを省略するとそのまま同じ穴になる。
第三に、影響範囲の把握が難しい。誰がどのコントラクトにどれだけのallowanceを残しているかは、プロトコル側からは把握できても、ユーザー側は自覚していないことが多い。事故後に「revokeしてください」と告知しても、対象ユーザーに届く保証がない。ether.fiの事例で影響が約11名にとどまったのは、承認を残していたユーザーが少なかったからであり、設計上の防御が効いたからではない。
第四に、これらは修正の公開状況が不明なまま報じられている。同じパターンを自社のコードから探す作業は、他人の修正を待たずに始められる。逆に言えば、自社に同型の関数がある限り、他社の被害額の小ささは何の保証にもならない。
【技術インサイトとアクション】
- ERC-20のallowanceは「誰の指示でも引き出してよい」という意味ではないが、引数検証を欠いたコントラクトの中では実質的にそうなる。過去に与えた承認は、承認先のコードが将来も正しく引数を検証し続けるという継続的な賭けであり、その賭けはコード変更のたびに更新される。 [コントラクト開発者] 今週中に、transferFromを呼ぶ全経路を洗い出し、資金の出し手アドレスがcalldata由来である箇所を特定して、その出し手が当該操作を承認したことを検証しているかを確認する。
- コールバックとソルバーは「外部から呼ばれる前提」ゆえに検証が抜けやすい。想定される呼び出し元をコメントで書いているだけの関数は、事実上誰からでも呼べる関数である。 [コントラクト開発者・監査担当] 次のリリースまでに、外部から呼ばれる想定の関数を列挙し、呼び出し元またはパラメータ由来のアドレスに対する検証をテストケースとして固定する(不正な呼び出し元で失敗することを明示的に検証する)。
- リスク評価の対象はロック残高ではなく、承認残高である。TVLが小さいコントラクトでも、ユーザーが残した無制限approveの総和が実質的な被害上限になる。 [プロダクト側・運用] 2週間以内に、自社プロトコルに対する未使用allowanceの総量を集計し、無制限approveを前提とするUIを必要額approveへ切り替える検討を始める。あわせて、緊急時にユーザーへrevokeを促す連絡経路を確認する。
【引用:出典】
- SlowMist Hacked(インシデントデータベース、2026年9月11日 Ether.fi Liquid項): https://hacked.slowmist.io/
- SlowMist Team(@SlowMist_Team)による技術分析(2026年9月11日): https://x.com/SlowMist_Team/status/2098344499923784048
- SlowMist Team(@SlowMist_Team)によるBNB Chain DEXルータ事例の分析(2026年9月7日): https://x.com/SlowMist_Team/status/2097153762746159228
本記事は公開情報に基づく技術解説であり、投資助言を目的としたものではありません。被害プロトコルによる公式のポストモーテムは本稿執筆時点で確認できておらず、攻撃経路の記述は第三者分析に基づきます。
