はじめに
2026年9月29日と2026年10月1日の間に、攻撃者はNEAR Intentsの入金時の返金処理を悪用し、対応する資産の裏付けがない内部残高を作り出しました。BNB Chain上のHOT Bridgeの保管庫から確認された7件の出金により、合計3,865,021 USDTが移転されました。その後にプロジェクトが公表した事後分析では、この欠陥はNEAR Intentsのコントラクトに起因するとされ、影響を受けた資金はすべて10月2日に返還されたと述べられています [1]。
返金処理の欠陥によって出金可能な残高がどのように作られたかを理解するには、NEARの非同期実行フローに沿って入金を追う必要があります。NEAR上で開発する際には、状態がいつ確定するか、後続の処理を誰が呼び出せるか、失敗がフロー全体にどう影響するかを考慮しなければなりません。監査では、フロー全体にわたるこれらの相互作用を検証する必要があります。
私たちは、NEARのコントラクト監査とセキュリティインシデント分析の経験を踏まえ、この事例を通じてNEAR上の非同期決済のセキュリティを検討します。まず、攻撃を理解するために必要な背景として、NEARの非同期呼び出しとレシート、トークンの送金と返金、NEAR IntentsとHOT Bridgeの関係を紹介します。次にインシデント分析で脆弱性と攻撃の順序を説明し、修正と資金回収を取り上げます。最後に、状態管理とコールバック処理を論じ、アカウントの認可とストレージコストにも議論を広げます。
背景
以下の分析は、NEARに固有の三つの背景知識に基づいています。非同期のコントラクト間呼び出しがどのように実行されるか、NEP-141とNEP-245がトークンの送金と返金をどのように処理するか、そしてHOT Omni Balanceの台帳上の資産がNEAR Intentsに記録された内部残高をどのように裏付けるかです。
NEARにおける非同期のコントラクト間呼び出し
コントラクトAがコントラクトBを同期的に呼び出し、その戻りを待つEthereumとは異なり [2]、NEARではコントラクト間の処理は後で実行されるようスケジュールされます。そのため、最初の呼び出し、外部呼び出し、コールバックは一つの実行コンテキストを共有しません [3]。このモデルでは、次の二つの用語が重要です。
-
Promise: 別のコントラクトの呼び出しや、その後のコールバックの実行など、コントラクトがスケジュールする処理です。
-
レシート: ネットワークが操作を実行し、アカウント間で結果を渡すために使用するメッセージです。アクションレシートは対象アカウントでスケジュールされた操作を実行し、データレシートは依存先のアクションレシートに実行結果を運びます [4]。
下の図は、この呼び出しとコールバックの流れを三つのアクションレシートとして示しています。レシートAでは、コントラクトAが自身の状態を更新し、Bを呼び出すPromiseを作成します。レシートBはBのロジックを実行します。図では省略したデータレシートがBの結果をレシートCに運び、そこでAのコールバックが結果を処理します。レシートAは、Bやコールバックが実行される前に完了し、その状態を確定できます。

これらのアクションレシートは独立して実行されるため、コントラクトは待機中にほかのリクエストを処理できます。コールバックのレシートが失敗すると、そのレシート内で行われた変更はロールバックされますが、元のレシートで確定した状態は残ります。失敗が過去の処理にさかのぼってその状態を取り消すことはできないため、アプリケーションは整合性を明示的に回復しなければなりません。
トークン規格と非同期の返金決済
NEP-141とNEP-245は、NEAR上のトークン規格です。NEP-141は代替可能トークンを対象とし、Ethereum上のERC-20とおおむね対応します。NEP-245は複数のトークン種別とバッチ操作を対象とし、ERC-1155とおおむね対応します [5] [6] [7] [8]。
ただし、両者は完全に同じではありません。NEARで通常のトークン送金を行うと、資産は送金先のreceiver_idに送られます。これは、コントラクトコードのあるアカウントも、ないアカウントも指し得ます。ここで扱う送金と呼び出しを組み合わせたフローでは、受信者はNEP-141用のft_on_transfer()、またはNEP-245用のmt_on_transfer()を実装したコントラクトアカウントです。受信者のフックは一つのレシートで実行され、その後、トークンコントラクトが別のレシートでft_resolve_transfer()またはmt_resolve_transfer()を実行して、使用額と返金額を確定します。したがって、一つのユーザートランザクションから始まるフローが複数のレシートにまたがることがあります。
受信者は自身のフックから、使用しなかった額を報告します。この戻り値は決済処理への入力であり、必ずしも最終的な返金額ではありません。NEP-141のnear-contract-standards参照実装では、リゾルバーは報告された未使用額に対し、元の送金額と受信者の利用可能残高の両方による上限を適用します。応答が失敗するか無効な場合は、まず送金額全額の返金要求として扱い、その後、同じ上限を適用します [9]。NEP-245では、Promiseチェーンが失敗した場合、マルチトークンのリゾルバーはトークン送金を取り消さなければなりません [8]。いずれの場合も、トークンレベルの返金によって、受信者が先行するレシートで確定したアプリケーションの状態が自動的に取り消されることはありません。
NEAR IntentsとHOT Bridge
NEAR Intentsは、NEAR上のインテントベースの決済プロトコルです。ユーザーは、資産を内部残高として記録するコントラクトであるintents.nearに資産を預け入れます。その後、インテントに署名して、その残高をスワップまたは出金できます [10]。
HOT Omni Bridgeとも呼ばれるHOT Bridgeは、クロスチェーンの資産レイヤーを提供します。資産は外部チェーン上のコントラクトにロックされ、それに対応するオムニアセットがNEAR上のHOT Omni Balanceコントラクトで発行されます。HOT Omni BalanceはNEP-245のトークン台帳を実装しています [11]。
したがって、この連携には三つの会計レイヤーがあります。外部チェーン上でロックされた資産、HOT Omni Balanceにおけるそのオムニアセット表現、そしてintents.nearに記録されたユーザーの内部残高です。これらのレイヤー間では整合性が保たれなければなりません。
下の図は、10単位の入金を例に、Intentsが内部残高を加算した後の二つの分岐を示しています。入金アクションがない場合、mt_on_transfer()は未使用額としてゼロを返し、受信者のフックを呼び出しません。DepositAction::Notifyがある場合、Intentsは受信者を呼び出し、後でmt_resolve_deposit()を使用して、受信者の応答に応じて内部残高への加算額を調整します。Notify分岐では、受信者は6単位を使用し、4単位を未使用として報告します。HOT Omni Balanceのmt_resolve_transfer()はその4単位を返金します。その結果、ユーザーの内部残高には6単位が残り、NEP-245台帳には、それを裏付ける追加の6単位が残ります [12]。

インシデント分析
このインシデントを三つの部分に分けて検討します。脆弱性分析では、返金処理によって資産の裏付けがない内部残高がどのように残り得たかを説明します。攻撃分析では、この挙動を悪用したトランザクションとレシートを追跡します。修正と回収では、実施された変更と報告された資金の返還を取り上げます。
脆弱性分析
以下のソースコード分析は、プロジェクトの再現用リビジョン35044fdに基づいています [13]。返金額の検証が不十分だったため、過大な返金イベントが生成され、コールバックが失敗して、内部残高と保有資産の整合性が失われる可能性がありました。
-
返金額の検証不足。
intents.nearの影響を受けた入金処理経路では、受信者の残高と要求額によって返金額に上限を設けていましたが、決済処理の対象となる送金で預け入れられた額は上限に含めていませんでした。そのため、既存の残高が大きければ、今回の入金額を大幅に超える返金が可能になり得ました。
-
過大な返金イベント。 長いトークン識別子と大きな返金額を含むバッチは、シリアライズ後の
MtBurnEventがログの上限を超える原因になり得ました。そのイベントをemit()で出力すると、返金コールバックが中断される可能性がありました [14]。この上限は配列や書式を含むイベントのペイロードに適用されます。返金額の数値が大きいだけでは、イベントが上限を超えるとはいえません。
入金イベントには送金額が使用されるため、各送金額が小さければ、そのイベントを上限内に収められます。一方、後続の返金イベントにはリゾルバーが受け入れた額が使用されます。入金額による上限がなければ、最初のイベントを確認しても、より大きな返金イベントが上限内に収まることは保証できません。
-
返金決済の不整合。 Intentsの
mt_resolve_deposit()コールバックは、自身のレシート内で内部残高と供給量を減算していました。返金イベントの出力が原因でそのレシートが失敗すると、これらの減算はロールバックされますが、先行する入金レシートで確定した加算は残ります。その後、上流のトークンコントラクトが送金された資産を返金すると、残存する内部残高への加算分には対応する資産の裏付けがなくなります。この処理を繰り返すことで、裏付けのない内部残高をさらに膨らませることが可能でした。
Web3に最適なセキュリティ監査会社
ローンチ前に設計、コード、ビジネスロジックを検証
攻撃分析
攻撃者は、返金イベントのサイズを増やすためのトークン(「フィラートークン」)と、過大な返金要求を引き起こす悪意ある受信者を使用し、対応する資産の裏付けがないまま内部残高を繰り返し膨らませました。プロジェクトによると、失敗した入金トランザクションは合計23件でした [1]。確認された7件の出金により、BNB Chain上のHOT Bridgeの保管庫から3,865,021 USDTが移転されました。
攻撃には複数のトランザクションが関与しました。この分析では、付録Aに列挙した13件の主要なトランザクションに焦点を当て、機能上の三つの段階に分けて整理します。
-
フェーズ1:準備(Tx 1–4)。 攻撃者はテストに必要な資産を入手し、悪意のある受信コントラクトをデプロイして、その内部の水増し用トークン残高にあらかじめ資金を入れました。
-
フェーズ2:残高の増幅(Tx 5–6)。 Tx 5とTx 6は、それぞれ特定された最初と最後の残高増幅トランザクションです。
-
フェーズ3:BNB Chainでの出金(Tx 7–13)。 攻撃者は水増しした内部残高を利用し、BNB Chain上のHOT Bridgeの保管庫から
USDTを出金しました。
これらのフェーズは攻撃者の行動を分類したものであり、互いに重ならない3つの期間を示すものではありません。残高の増幅と出金の活動は時間的に重なっていました。以下の詳細な再構成では、Tx 5と、そのすべてのアクションレシートの実行結果をたどったうえで、BNB Chainでの7件の出金を確認します。
最初の残高増幅の事例では、ここに示す悪意のある受信コントラクトが使われました。その後のトランザクションでは、別の悪意のある受信アカウントが使われました。完全なアドレスとトークンIDは付録Bに記載しています。以下の時刻はすべてUTCです。
| 以下で使用する名称 | 役割 | チェーン |
|---|---|---|
| 攻撃者 | 攻撃者が管理するトランザクション送信者 | NEAR |
| 悪意のある受信者 | 通知コールバック用コントラクト/内部残高の保有者 | NEAR |
| 水増し用トークン | 攻撃者が用意した、バッチの件数を増やすためのトークン | NEAR |
USDTの表現トークン |
NEAR上で表現されたBNB Chainの資産 | NEAR |
USDT |
元となるトークン | BNB Chain |
| Intents | 内部決済コントラクト | NEAR |
| HOT Omni Balance | NEP-245トークン台帳 | NEAR |
| BNB側の受取人 | 攻撃者が管理する出金先 | BNB Chain |
| HOT Bridgeの保管庫 | クロスチェーン資産の保管庫 | BNB Chain |
フェーズ1:準備(Tx 1–4)
-
ステップ1(Tx 1):9月29日12:30:44に、攻撃者はHOT Bridgeを通じてBNB Chainの
USDTの表現トークンを原単位で10e18取得し、HOT Omni Balance内の攻撃者のアカウントに記帳されました。トークンIDの末尾をデコードすると、付録Bに記載されたBNB Chain上のUSDTのアドレスになります。小数点以下の桁数が18桁のため、原単位の10e18は10USDTに相当します。 -
ステップ2(Tx 2):攻撃者はHOT Bridgeを通じて、水増し用トークンを原単位で
97e36 + 97取得しました。このカスタムトークンは同日それ以前にOsmosisで作成・発行され、KavaとGonkaを経由して転送された後、NEAR上で請求されました。準備段階の記録は付録Cに詳述しています。 -
ステップ3(Tx 3):攻撃者は悪意のある受信コントラクトをデプロイして初期化しました。
-
ステップ4(Tx 4):攻撃者はHOT Omni Balanceの
mt_transfer_call()を通じ、msgで悪意のある受信者を指定して、水増し用トークンを原単位で97e36、Intentsへ転送しました。Intentsは悪意のある受信者の内部アカウントにその額を記帳しました。この事前に資金を入れた残高により、悪意のある受信者はTx 5中に、各1e36原単位の返金要求を97件行うことができました。
フェーズ2:残高の増幅(Tx 5–6)
最初の残高増幅トランザクションであるTx 5は、9月29日16:14:19に実行されました。その中核となる5つのアクションレシートは、バッチ転送、入金、通知、失敗した残高調整、上流での返金に対応します。それぞれの識別子と実行結果は付録Dに記載しています。
-
ステップ5(Tx 5):攻撃者はIntentsを転送先として、HOT Omni Balanceの
mt_batch_transfer_call()を呼び出しました。バッチには、同じ水増し用トークンについて、それぞれ1原単位のエントリーが97件と、10e18原単位のUSDTのエントリーが1件含まれていました。Intentsのmt_on_transfer()は、98件すべてのエントリーを悪意のある受信者に記帳し、MtMint入金イベントを発行しました。各水増し用トークンのエントリーを1としたのは意図的です。入金処理ではcheck_refund()を呼び出し、最初のイベントと想定される返金処理分を合わせてもログの上限内に収まるか検証していました。少額の入金はこのチェックを通過しましたが、その後、悪意のある受信者によるはるかに大きな返金要求によって、上限を超えるサイズの返金イベントが生成されました[13]。
-
ステップ6(Tx 5):内部残高への記帳後、Intentsは
DepositAction::Notify分岐に進み、98件すべてのエントリーを渡して、悪意のある受信者のmt_on_transfer()を1回呼び出しました。悪意のある受信者の処理は成功し、水増し用トークンのエントリーについては各1e36原単位の返金要求を97件、USDTのエントリーについては0の返金要求を返しました。 -
ステップ7(Tx 5):Intentsの
mt_resolve_deposit()は、各1e36原単位の返金要求97件をすべて受け付けました。Tx 4で事前に資金を入れた悪意のある受信者の内部の水増し用トークン残高が、それらを賄うのに十分だったためです。これらの大きな返金額と、長いトークン識別子の繰り返しによって、MtBurnEventログは16,495バイトに膨らみ、16,384バイトの上限を超えました。イベントの発行は失敗し、このレシートで試みられた残高の減算はロールバックされましたが、それ以前の入金による記帳は残りました。
-
ステップ8(Tx 5):HOT Omni Balanceの
mt_resolve_transfer()は、Intentsのmt_resolve_deposit()から失敗を示すPromise結果を受け取りました。そのため、悪意のある受信者がUSDTについてゼロの返金を要求していたにもかかわらず、10USDTを含む98件すべてのエントリーについて、当初の転送額を攻撃者に返金しました。攻撃者は資産を取り戻す一方、悪意のある受信者にはIntents内の対応する内部残高が残りました。
攻撃者はこの処理を繰り返し、内部残高をさらに水増ししました。Tx 6では別の悪意のある受信者が使われ、同じログ長の超過による失敗が発生しました。
フェーズ3:BNB Chainでの出金(Tx 7–13)
- ステップ9(Tx 7–13):攻撃者は水増しした内部残高を利用して、HOT Bridge経由の出金を要求しました。BNB Chain上では、付録Aに記載されたTx 7–13の7件のトランザクションにより、HOT Bridgeの保管庫から攻撃者が管理するBNB側の受取人へ
USDTが転送されました。
最初の少額の出金2件は、合計21 USDTです。続く5件の合計は3,865,000 USDTで、**7件のトランザクション全体では3,865,021 USDT**となります。これらの金額は、トークンの転送ログと小数点以下18桁の精度に基づいています。BNB Chainのタイムスタンプは転送先チェーンでの転送時刻を示すものであり、NEARでの入金時刻や出金要求時刻とは区別する必要があります。
修正と資金回収
チームによると、修正は10月2日にデプロイされました[1]。修正後の返金額の計算では、要求額と利用可能な内部残高による上限を維持しつつ、各返金額を現在の転送で入金された額以下に制限します[15]:
let refund_amount = requested_refund.min(*deposited).min(balance_left);
これにより、以前の入金による多額の残高を使って、その後の少額の入金に対する過大な返金を賄うことを防ぎます。
その後、チームは10月7日に入金通知を削除し[1]、悪意のある受信者が返金要求を送る経路を閉じました。PR #374には、この削除が記録されています[16]。事後分析では、追加の安全策も報告されています。現在は監視によってユーザーの内部残高の合計とコントラクトが実際に保有するトークン量を比較し、両者が一致しない場合はコントラクトを自動的に一時停止します[1]。
資金回収について、プロジェクトは10月2日に全資金が返還されたと発表しました[17]。その後の事後分析では、影響を受けたすべてのユーザーの損失が補填されたことを確認しました[1]。
NEARスマートコントラクトのセキュリティ上の考慮事項
この事案は、NEARの非同期レシートにまたがる状態の整合性を検討する出発点となります。この分析と当社のNEAR監査の経験を踏まえ、このセクションでは、非同期実行、アクセスキーによるアカウントの認可、ストレージコストの3つの領域を取り上げます。後者の2つでは、アプリケーションが権限を委任し、永続的な状態の維持費用を賄う方法にまで議論を広げます。焦点は、これらのNEARの仕組みに起因するリスクと、開発・監査時に必要となる具体的な確認事項にあります。一般的なセキュリティ対策も、そうしたリスクの説明や対処に役立つ場合に取り上げます。

非同期実行
1. レシートにまたがる状態管理
NEARでは、コントラクト間の呼び出しとそのコールバックは別々のレシートで実行され、後の処理が失敗しても、先に確定した状態が自動的に取り消されることはありません。また、呼び出しの完了を待つ間にもコントラクトは別の要求を処理できるため、内部残高と実際の資産の整合性を保つには、状態更新のタイミングと処理間の分離が重要です。よくある次の3つのシナリオが、こうしたリスクを示しています。
-
残高の減算の遅延: コールバックまで使用可能残高を減算または引き当てない場合、転送が決済される前に同じ資金が再び使われる可能性があります。
-
入金の早すぎる記帳: 外部処理が完了する前に使用可能な資金として記帳すると、2つのリスクが生じます。
- 決済前の使用: 外部呼び出しがまだ保留中で、決済が確定する前に、ユーザーが記帳済みの資金を使う可能性があります。
- コールバック失敗後も残る記帳額: 返金コールバックが失敗した場合、上流のトークンコントラクトが資産を返却しても、先に確定した内部残高への記帳が残る可能性があります。NEAR Intentsの事案はこのリスクを示しました。
-
共有状態の上書き: 保留中の要求がユーザーや金額を格納する変数を共有している場合、後の要求によって、先の要求のコールバックに必要な情報が上書きされる可能性があります。その結果、返金が誤ったユーザーや金額に結び付けられるおそれがあります。
推奨事項。 NEARのクロスコントラクト処理はレシートをまたいでアトミックではないため、開発時と監査時の双方で、残高を更新するタイミング、保留中のリクエストの分離、呼び出しやコールバックが失敗した場合の復旧を検討する必要があります。出金を開始する前に資金を差し引くか引き当て、送金が失敗した場合にのみ残高を一度だけ戻してください。入金については、決済が確定する前にユーザーが資金を使用できないようにし、決済コールバック自体が失敗した場合も含め、返金された資産に対応する使用可能な内部残高が残らないようにしてください。保留中のリクエストごとに、ユーザー、金額、決済状態を別々に記録してください。呼び出しの成功と失敗、コールバックの失敗、交錯するリクエスト、決済の再試行について、これらの動作を検証してください。
2. クロスコントラクト呼び出しにおけるコールバック
NEARのコントラクトは、promiseを使用してクロスコントラクト呼び出しと、その結果を処理するコールバックをスケジュールします。コールバックへのアクセス制御と失敗時の処理によって、後続の残高計算や決済が意図どおりに実行されるかが決まります。ここから、次の2つのリスクが生じます。
-
制限のないコールバックへのアクセス: コールバックは呼び出し可能なコントラクトメソッドとして公開する必要がありますが、アクセスを制限しなければ、外部の呼び出し元からも到達可能になります。攻撃者は、保護されていないコールバックを偽造したパラメータで呼び出し、本来先行するはずの操作なしに残高計算や決済のロジックを実行させる可能性があります。
-
コールバックの失敗: NEARのコントラクトは、
.then()を使用して、クロスコントラクト呼び出しの結果を処理し、必要な完了処理や復旧処理を行うコールバックを設定します。コールバック自体が失敗すると、そのコールバックによる状態変更は取り消されますが、それ以前に成功したレシートは確定したままとなり、決済状態の不整合につながります。
推奨事項。 #[private]を使用して、コールバックの呼び出し元をコントラクト自身に制限してください [18]。コールバックに十分なガスを割り当て、そこで実行する処理と生成するログ出力に上限を設けてください。失敗したpromiseの結果や不正な形式の戻りデータを明示的に処理してください。失敗したコールバックでは自身の復旧処理による変更を確定できないため、復旧に必要な保留中の決済状態は、それ以前のレシートで記録してください。コールバックが失敗しても裏付けのない使用可能な残高が残らず、復旧処理によって同じ操作が二度決済されないことを検証してください。
アカウントの認可
NEARでは、Function-CallキーとFull Accessキーという2種類のアクセスキーによって、アカウントに対するきめ細かな認可が可能です [19]。
ユーザーがFunction-Callキーを使用するウェブサイトを認可すると、そのサイトは操作ごとに新たなウォレット署名を取得せずに、許可された受信先の許可されたメソッドを呼び出せます。ランタイムはその範囲を強制し、この種類のキーについては、付与されたデポジットがゼロでない呼び出しを拒否します [20]。
その範囲内では、アカウントの識別情報だけでは、委任されたキーで署名された操作と、ユーザーが個別に承認した操作を区別できません。その識別情報のみに依存する機密性の高いメソッドは、送金、出金、権限変更が認可の対象に含まれている場合、悪意のあるサイトによるこれらの操作の開始を許してしまう可能性があります。
推奨事項。 制限付きキーから到達可能な機密性の高いメソッドと、各操作に必要な認可を確認してください。#[payable]とassert_one_yocto()を使用して、正確に1 yoctoNEARを要求すれば、制限付きキーによる直接のトランザクション経路を排除できます。アプリケーション独自の識別情報と権限のチェックは維持してください。ソフトウェアがFull Accessキーを保持でき、別のコントラクトがNEARを付与できるため、デポジット要件を満たしていても人間が操作を開始した証明にはなりません。
ストレージコスト
NEARでは、コントラクトは保存するデータのためにNEARを引き当てる必要があるため、状態が増えると、ほかの操作に利用できる資金が減ります [21]。リスクは、その増加分を誰が負担するかによって異なります。
-
プロトコルが負担するストレージ: ユーザーが費用を支払わずにレコードを作成でき、増加量に上限がない場合、低コストの呼び出しを繰り返すことで、コントラクトのストレージに必要な引当額が増え、利用可能な資金が消費される可能性があります。
-
ユーザーが負担するストレージ: ユーザーへの課金には、料金で費用を完全に賄うことと、使用量を正確に測定することの両方が必要です。
- 料金の回避: 別のエントリーポイントから、通常の登録経路や書き込み経路で必要な料金を徴収せずにレコードを作成できる可能性があります。
- 使用量の誤った帰属:
env::storage_usage()はコントラクト全体のストレージを測定します。外部呼び出しの前に基準値を保存し、後のレシートで比較すると、無関係な書き込みや削除が含まれ、ユーザー自身によるストレージ変更に対する請求額が過大または過小になる可能性があります。
推奨事項。 ストレージ費用の負担、書き込み経路の網羅、使用量の帰属を併せて検討する必要があります。新たなストレージに課金するか、プロトコルによる補助額と容量に明示的な上限を設けてください。すべての書き込み経路で料金を回避できないか確認してください。可能な場合は、操作によるストレージ変更を同じレシート内で測定してください。残高計算が複数のレシートにまたがる場合は、個々のユーザーに使用量を帰属させる前に、無関係な変更を分離してください。
結論
NEARでは、後続のコールバックが失敗しても、成功したレシートによる状態が残り続けることがあります。Ethereum上の同期的な呼び出しチェーンとは異なり、レシートをまたぐ処理には共通のロールバック境界がありません。そのため、アプリケーションは、起こり得るそれぞれの失敗の後に残る状態と資産を考慮しなければなりません。
NEAR Intentsでは、トークンの返金が完了した一方、対応する内部残高は、残高調整が失敗した後も残りました。返金の検証が不十分だったため、その失敗を引き起こすほど大きな返金額が設定されました。この事例は、トークン層で資産を返還しても、アプリケーションには裏付けのない使用可能な残高が残り得ることを示しています。
同様の非同期処理については、開発時と監査時に、起こり得るそれぞれの失敗の後にどの状態変更が残るか、保留中の残高を使用できるか、残高の二重計上や二重返金を起こさずに未完了の決済をどう復旧できるかを追跡する必要があります。各レシートは、それが依存する以前の状態と、それがスケジュールする後続の操作を併せて検討しなければなりません。
決済に加えて、アクセスキーの権限とストレージ費用の負担も明示的な確認が必要です。委任された呼び出しは意図された権限の範囲内にとどめ、ユーザーの操作によって増加する状態には、負担者または上限を定める必要があります。
参考文献
[1] NEAR Intentsの事後分析、2026年10月16日
[2] Ethereum.org:スマートコントラクトのセキュリティ
[5] ERC-20:トークン規格
[10] NEAR Docs:NEAR Intentsとの統合
[11] HOT BridgeのアーキテクチャとHOT Omni Balance
[12] NEP-245の入金フロー、再現用リビジョン35044fd
[14] 過大な返金に関する回帰テスト、マージ済みリビジョン0c1a530
[15] 現行のIntentsソースにおける返金額の上限、閲覧日:2026年10月26日
[16] PR #374:入金時の通知を削除
[17] NEAR Intentsの復旧に関する声明、2026年10月2日
[18] NEAR Docs:非公開コールバック
[19] NEAR Docs:アクセスキー
[20] NEAR RuntimeSpec:関数呼び出しアクセスキーの検証
[21] NEAR Docs:ストレージコストを利用した攻撃
付録
A. トランザクションの時系列 {#appendix-verified-transaction-timeline}
この付録では、当初の調査で使用されたトランザクション番号を維持しています。4件の準備トランザクション、特定された最初と最後の残高増幅トランザクション、およびBNB Chainでの7件の出金を掲載しています。プロジェクトが報告した、失敗した入金トランザクション全23件を網羅した一覧ではありません。時刻はすべてUTCです。フェーズは操作を分類したもので、時間的に重なる場合があります。
| Tx | フェーズ | チェーン | 時刻(UTC) | 操作 | トランザクション |
|---|---|---|---|---|---|
| 1 | 準備 | NEAR | 2026-09-29 12:30:44 | 10 USDTの表象を取得 |
9SiSBvDe... |
| 2 | 準備 | NEAR | 2026-09-29 16:09:34 | 埋め合わせ用トークンを取得 | FmwaoRND... |
| 3 | 準備 | NEAR | 2026-09-29 16:11:48 | 悪意のある受信先をデプロイして初期化 | GgbwMV8N... |
| 4 | 準備 | NEAR | 2026-09-29 16:12:40 | 悪意のある受信先の内部の埋め合わせ用トークン残高に事前に資金を入れる | 4wf2FWcE... |
| 5 | 残高の増幅 | NEAR | 2026-09-29 16:14:19 | 特定された最初の増幅トランザクション | 3k91uDpU... |
| 6 | 残高の増幅 | NEAR | 2026-10-01 05:47:27 | 特定された最後の増幅トランザクション | GAQKwHdT... |
| 7 | クロスチェーン出金 | BNB Chain | 2026-09-30 18:57:07 | HOT Bridgeの保管庫からBNBの受取人へ10 USDTを送金 |
0x4f426e23... |
| 8 | クロスチェーン出金 | BNB Chain | 2026-09-30 20:05:24 | HOT Bridgeの保管庫からBNBの受取人へ11 USDTを送金 |
0xc002bd17... |
| 9 | クロスチェーン出金 | BNB Chain | 2026-09-30 23:54:30 | HOT Bridgeの保管庫からBNBの受取人へ800,000 USDTを送金 |
0x9fe58e03... |
| 10 | クロスチェーン出金 | BNB Chain | 2026-10-01 00:24:02 | HOT Bridgeの保管庫からBNBの受取人へ1,200,000 USDTを送金 |
0x0381265d... |
| 11 | クロスチェーン出金 | BNB Chain | 2026-10-01 00:50:42 | HOT Bridgeの保管庫からBNBの受取人へ1,500,000 USDTを送金 |
0x69d1c68c... |
| 12 | クロスチェーン出金 | BNB Chain | 2026-10-01 01:46:20 | HOT Bridgeの保管庫からBNBの受取人へ330,000 USDTを送金 |
0x9c10b967... |
| 13 | クロスチェーン出金 | BNB Chain | 2026-10-01 06:08:26 | HOT Bridgeの保管庫からBNBの受取人へ35,000 USDTを送金 |
0x16ddfa69... |
B. アカウントとトークン識別子 {#appendix-accounts-and-token-identifiers}
当初の調査では、以下の両チェーンのアドレスを攻撃者のものとしています。チェーン上の記録からは、それぞれがNEARでの送信者とBNB Chainでの出金先であることが独立して確認できます。ここに記載した悪意ある受信者はTx 5で使用されたアカウントです。その後の攻撃規模を拡大するトランザクションでは、別の悪意ある受信者アカウントが使用されました。
| 以下で使用する名称 | 役割 | チェーン | アカウント/識別子 |
|---|---|---|---|
| 攻撃者 | 攻撃者が管理するトランザクションの送信者 | NEAR | 20e6bfe4685d99004fc2095d3a0fc6158cd810ce8f801196141e183ed6e19d65 |
| 悪意ある受信者 | 通知コールバックコントラクト/内部残高の所有者 | NEAR | dd1fe58ec316ada5b673b88c14fe2fa2b7ede82080440901dfbd64b994241757 |
| フィラートークン | バッチの容量を埋めるために攻撃者が供給したトークン | NEAR | 4444119_ |
USDTの表現トークン |
NEAR上で表現されたBNB Chainの資産 | NEAR | 56_2CMMyVTGZkeyNZTSvS5sarzfir6g |
USDT |
元となるトークン | BNB Chain | 0x55d398326f99059ff775485246999027b3197955 |
| Intents | 内部決済コントラクト | NEAR | intents.near |
| HOT Omni Balance | NEP-245トークン台帳 | NEAR | v2_1.omni.hot.tg |
| BNBでの受取人 | 攻撃者が管理する出金先 | BNB Chain | 0x09Fd1f5d9F185067A92493E43AA259ea4AB3ad37 |
| HOT Bridgeの保管庫 | クロスチェーン資産の保管庫 | BNB Chain | 0x233c5370CCfb3cD7409d9A3fb98ab94dE94Cb4Cd |
C. フィラートークンの作成とブリッジ転送 {#appendix-filler-token-origin}
フィラートークンは、OsmosisのTokenFactoryで作成されたカスタム資産です。その作成、発行、その後の転送は、このトークンが攻撃の準備に関係していたことを示しています。以下の時刻はすべて2026年9月29日のUTCです。NEARでの請求は付録AのTx 2に当たります。
| 時刻(UTC) | チェーン | 操作 | トランザクション |
|---|---|---|---|
| 14:00:29 | Osmosis | トークンのデノミネーションを作成 | C489A91E... |
| 14:01:03 | Osmosis | 97e36 + 97基本単位を発行 |
5FD2CF0E... |
| 14:02:00 | Osmosis | 発行した数量をIBC経由でKavaに転送 | 8EFD2659... |
| 14:02:32 | Kava | 同じ数量をIBC経由でGonkaに転送 | D26EABBE... |
| 14:16:07 | Gonka | 資産をHOT Bridgeに預け入れ | 41982DF3... |
| 16:09:34 | NEAR | HOT Omni Balanceでその表現トークンを請求(Tx 2) | FmwaoRND... |
同じOsmosisアカウントがトークンを作成、発行、送信しました。2件のIBC転送には、いずれもメモhot intents ctf fillerが含まれています。発行された数量である97e36 + 97基本単位は、両転送、Gonkaでのブリッジへの預け入れ、NEARでの請求の数量と一致します。Osmosisでの転送の受取人はKavaでの転送の送信者であり、Kavaでの転送の受取人はGonkaでのブリッジへの預け入れの送信者です。これらの記録を総合すると、このトークンが攻撃の準備の一環として作成されたことが示されます。
Gonkaのデノミネーショントレースには、IBCパスtransfer/channel-5/transfer/channel-1と、元のOsmosis資産の識別子が記録されています。
| 資産のレイヤー | 識別子 |
|---|---|
| 元のOsmosisデノミネーション | factory/osmo1dnjr753vam6 |
| GonkaのIBCデノミネーション | ibc/C679139C206D0E7379C1 |
NEARのTx 2に含まれるcontract_idをBase58でデコードすると、上記のGonkaのIBCデノミネーションが得られます。Gonkaでのブリッジへの預け入れとNEARでの請求は、nonce 1790691367000000007133を共有しています。また、元の預け入れの受取人ハッシュは、攻撃者のNEARアカウントIDのSHA-256ハッシュとも一致します。これらのフィールドは、元の預け入れとNEARでの残高付与を結び付けます。「フィラートークン」という呼称は、返金イベントを肥大化させるうえでのこの資産の役割を表しています。
D. Tx 5のレシート詳細 {#appendix-tx5-receipts}
以下の主要な5件のアクションレシートは、ステップ5~8に対応します。ステップ5には、最初のHOT Omni Balanceでの転送とIntentsへの預け入れのレシートの両方が含まれます。
| 対応するステップ | レシート | 実行者 | メソッド | 結果 |
|---|---|---|---|---|
| 5 | 8fWiEwQUbVATPC3J |
HOT Omni Balancev2_1.omni.hot.tg |
mt_batch_transfer_call() |
成功。バッチを転送し、Intentsのmt_on_transfer()をスケジュールした |
| 5 | DSfNwH18mEASbDg2 |
Intentsintents.near |
mt_on_transfer() |
ローカル実行は成功し、Promiseを返した |
| 6 | 7wwKtfGzK1YSGqT2 |
悪意ある受信者dd1fe58ec316ada5 |
mt_on_transfer() |
成功。返金配列を返した |
| 7 | 4CPmVneKa1UWEuQa |
Intentsintents.near |
mt_resolve_deposit() |
失敗:ログの長さが上限を超えた |
| 8 | 7y7VKm4tM3rEspHG |
HOT Omni Balancev2_1.omni.hot.tg |
mt_resolve_transfer() |
成功。転送された資産を返金した |



