Back to Blog

〜80万ドルの損失:HinkalのDouble-Spend攻撃 | BlockSec週次レポート

Code Auditing
July 9, 2026
12 min read
Key Insights

過去1週間(2026/06/29 - 2026/07/05)において、Ethereumで約80万ドルの損失を伴う以下の重要なセキュリティインシデントが発生しました。

日付 インシデント 種別 推定損失
2026/07/02 Hinkal ビジネスロジックの欠陥 ~$800K
  • Hinkal: シールドプールプロトコルに対するダブルスペンド攻撃。同一のデポジットから複数の有効なヌリファイアを生成できるレガシーノートフォーマットの欠陥が悪用された可能性が高い。

Web3のベストセキュリティ監査機関

ローンチ前に設計・コード・ビジネスロジックを検証

今週のハイライト:Hinkal

本インシデントでは、シールドプールにおけるダブルスペンドがレガシーノートフォーマットの欠陥によって引き起こされた可能性が高い。ヌリファイアベースのプライバシープロトコルにおける支払い能力の保証は、コントラクトが有効な証明を受け入れることだけでなく、回路レベルでのヌリファイアバインディングに依存している。

2026年7月2日、Ethereum上のシールドプールプロトコルであるHinkalが、ダブルスペンド攻撃により約80万ドル相当の資産を流出させた [1]。根本原因として考えられるのは、レガシーノートフォーマットの欠陥であり、単一のノートが一意のヌリファイアに強く結び付けられていないため、1つのデポジットを複数回スペンドできる状態になっていた。プロジェクトは回路実装をオープンソース化しておらず、チームはいまだ詳細な技術的説明を公開していない。以下の分析は、公開ドキュメント、難読化解除されたクライアントコード、およびオンチェーンの観察に基づいている。

背景

プロトコル概要

Hinkalはシールドプールプロトコルである。残高はオンチェーンのマークルツリー内のコミットメントとして表現されるノートとして保存され、通常のアカウント残高ではない。

ノートをスペンドするには、ユーザーはzk証明とヌリファイアを送信する。コントラクトは各ヌリファイアを記録し、繰り返しを拒否する。1つのノートが1つのヌリファイアのみを生成できるというルールはコントラクトによって強制されない。これは回路に委ねられている。

以下の図はデポジットと引き出しのフローを示している:

     ノートフォーマット(クライアント)     |       オンチェーンパス
                                   |
  +--------------------------+     |     +-------------------------------+
  | 新フォーマット(デフォルト):|     |     | transact()【証明あり】        |
  | nkを直接Poseidon6に使用  |     |     | 全操作:デポジット/転送/     |
  | -> 1コミット : 1ヌリファイア|     |     |           引き出し            |
  +--------------------------+     |     +-------------------------------+
                               --> | -->
  +--------------------------+     |     
  | レガシーフォーマット:    |     |     +-------------------------------+
  | nkはe*nkの積のみで使用   |     |     | prooflessDeposit()【証明なし】 |
  | -> 1コミットに対して複数  |     |     | デポジットのみ                |
  |    のヌリファイアが許容   |     |     +-------------------------------+
  |    される可能性あり      |     |   
  +--------------------------+     |     デポジットの場合、両パスとも
                                   |     -> ノートはマークルツリーに追加される

ノートフォーマット

実際の回路実装はオープンソース化されていない。以下の分析は、Hinkalの公開回路設計ドキュメント [2] および難読化解除されたクライアント証明者コード [3] に基づいている。

ドキュメントとクライアントコードから、isNewStyleフラグによって選択される2種類のノートのstealthAddress構築方法が示唆されている:

  • レガシー: H0 = e*Base8H1 = (e*nk)*Base8stealthAddress = Poseidon3(signs, H0y, H1y)

  • 新方式: H1 = nk*H0stealthAddress = Poseidon6(signs, H0y, H1y, spk0, spk1, nk)

Poseidon2Poseidon3Poseidon6はいずれもPoseidonハッシュ関数のインスタンスであり、サフィックスは入力数を示す。

ここでeは乱数、nkは秘密のヌリファイアキー、Base8は固定点である。H0H1はBaby Jubjub楕円曲線上の点であるため、それぞれx座標とy座標を持つ。signsの値はそれらのx座標H0xおよびH1xの符号ビット(2*L(H0x) + L(H1x))をパックしたものである。レガシーのstealthAddressにはnkが積e*nkを介してのみ含まれ、スペンディングキー(spk0spk1)は直接含まれない。一方、新フォーマットではnkspk0spk1がすべてPoseidon6に入力される。

ヌリファイアはnkから直接計算される:nullifier = Poseidon2(commitment, Poseidon2(nk, commitment))

zkProofWorkerNode.jsから難読化解除された関連関数(S = Poseidon、Base8 = 固定ジェネレーター、e = ランダム化、t = nk):

// レガシー
static getRandomizedStealthPairOld = (e, t) => {
  const a  = e * (t % B) % B;            // a = e*nk
  const H0 = mulPointEscalar(Base8, e);  // H0 = e*Base8
  const H1 = mulPointEscalar(Base8, a);  // H1 = (e*nk)*Base8(nkは積を介してのみ)
  return { H0, H1 };
};

// 新方式
static getRandomizedStealthPair = (e, t) => {
  const a  = t % B;                      // a = nk
  const H0 = mulPointEscalar(Base8, e);  // H0 = e*Base8
  const H1 = mulPointEscalar(H0, a);     // H1 = nk*H0(nkはノードポイントに結び付けられる)
  return { H0, H1 };
};

// ヌリファイアはnkから直接導出される
getNullifier() {
  const c   = this.getCommitment();
  const sig = S(this.nullifyingKey, c);  // Poseidon2(nk, commitment)
  this.nullifier = S(c, sig);            // Poseidon2(commitment, sig)
}

このisNewStyleフラグはノートのstealthAddressStructure内のextraRandomizationというフィールドの最上位ビットに格納される。クライアントコードはextraRandomization = (isNewStyle << 255) | H0xとしてパックし、コントラクトはパックされた値をデコードする際にgetPointSign(H) = H / 2**255およびgetPointY(H) = H % 2**255で分解する。コードのコメントには「isNewStyleフラグ(ビット255)を取り除き、回路がクリーンなH0x座標を受け取れるようにする」と記されている。したがって、最上位ビットはgetPointSign(extraRandomization)によって取得でき、ノートタイプを判定できる。

CircomDataBuilder.sol:formBasicInput()の以下のスニペットはこのアンパックを示している:

// CircomDataBuilder.sol:formBasicInput()
    ...
    // isNewStyleフラグ(ビット255)を取り除き、回路がクリーンなH0x座標を受け取れるようにする
    input[index++] = getPointY(
        circomData.stealthAddressStructure.extraRandomization
    ); // = H0x
    input[index++] = circomData.stealthAddressStructure.H0; // = H0y
    ...

デポジット構築のクライアントコードでは、このisNewStyleフラグはデフォルトでtrueに設定されているため、通常のユーザーフローではレガシーノートが生成されないようになっている。

// 自分自身へのhinkalDeposit
  // セルフ出力UTXO
  //@hinkal/common/common/src/functions/pre-transaction/outputUtxoProcessing.mjs
	let m = [new r({
		amount: e(d + o, 0n),
		erc20TokenAddress: f,
		mintAddress: p,
		nullifyingKey: i.getShieldedPrivateKey(),
		timeStamp: s,
		spendingPublicKey: i.getSpendingKeyPair().pubSpendingBJJPoint,
		isNewStyle: !0 // isNewStyle: true と同等
	})];

// 他者へのhinkalDeposit
  // @hinkal/common/common/src/data-structures/Hinkal/hinkalDeposit.mjs
    w = h.map((e, t) => [new s({
		amount: o[t],
		erc20TokenAddress: e,
		H0: [BigInt(_), BigInt(y)],
		stealthAddress: g,
		encryptionKey: b,
		isNewStyle: !0 // isNewStyle: true と同等
	})])

したがって、フラグが0に設定されたレガシースタイルのデポジットは、通常のユーザーフローでは生成されない。

デポジットと引き出しのパス

transact()は、デポジット・プライベート転送・引き出しのすべての操作に対して使用される、証明検証を伴うユニバーサルなエントリーポイントである。クライアントはオフチェーンでzk証明を生成し、transact()はその証明を検証し、マークルルートを確認した後、ヌリファイアとコミットメントを書き込む。

prooflessDeposit()transact()を完全にバイパスする別のオンチェーン関数である。トークンを受け取り、証明を必要とせず、呼び出し元が指定したデータからコミットメントを直接構築する。

    function prooflessDeposit(
        address[] calldata erc20Addresses,
        uint256[] calldata amounts,
        uint256[] calldata tokenIds,
        StealthAddressStructure[] calldata stealthAddressStructures
    ) public payable nonReentrant {
        hinkalHelper.performProoflessDepositChecks(
            erc20Addresses,
            amounts,
            tokenIds,
            stealthAddressStructures
        );

        bytes memory returnData = address(hinkalInLogic).functionDelegateCall(
            abi.encodeWithSelector(
                hinkalInLogic.handleProoflessDeposit.selector,
                erc20Addresses,
                amounts,
                tokenIds,
                stealthAddressStructures
            )
        );

        UTXO[] memory utxoSet = abi.decode(returnData, (UTXO[]));
        uint256 length = utxoSet.length;

        OnChainCommitment[] memory commitmentArray = new OnChainCommitment[](
            length
        );

        for (uint256 i = 0; i < length; i++) {
            commitmentArray[i] = createCommitment(utxoSet[i]);
        }

        insertCommitments(
            new uint256[][](0),
            new bytes[][](0),
            commitmentArray,
            new bool[](0)
        );
    }

ERC-20ノートの場合、コミットメントはamounttokenstealthAddresstimestampから導出される。stealthAddressは呼び出し元から直接渡される。

      commitment = hash4(
          utxo.amount,
          uint256(uint160(utxo.erc20Address)),
          utxo.stealthAddressStructure.stealthAddress,
          utxo.timeStamp
      );

脆弱性分析

この分析はオンチェーンの挙動と難読化解除されたクライアントコードからの推論である。実際の回路実装はオープンソース化されておらず、根本原因は確認中である。

影響を受けたコントラクトはHinkal0x25e5...a826)である。

欠陥の可能性。 背景で説明したとおり、レガシーフォーマットではnkが積e*nkのみを介して含まれるが、ヌリファイアはnkから直接導出される。レガシーのスペンド回路がnkをコミットメントに一意に結び付けていない場合、異なる(e, nk)のペアが同じ積e*nk(つまり同じコミットメント)を保ちながらnkを変えることで、同一のリーフに対して毎回新しいヌリファイアを生成できる。新フォーマットではnkが直接Poseidon6に入力されるため、コミットメントごとに1つのnkが固定される。Hinkalコントラクトから見ると、すべての引き出しは有効に見える:証明はパスし、ルートは存在し、各ヌリファイアは新しいため、insertNullifiers()はそれを受け入れる。コントラクトはヌリファイアをリーフに結び付けることができないため、各スペンドを通常の引き出しとして処理する。バグはオンチェーンのチェックにあるのではなく、おそらくレガシー証明回路が何を証明できるかに起因する。

prooflessDeposit()とアタックサーフェス。 prooflessDeposit()関数はperformProoflessDepositChecks()に処理を委譲しているが、オンチェーンの挙動からフォーマット検証は観察されない:この関数はレガシーフォーマットのノートを拒否せず、コントラクトもその戻り値を使用していないようである。これにより、フォーマット制限なしにレガシーノートがマークルツリーに挿入でき、上記の欠陥に対するアタックサーフェスが生じる。

攻撃分析

以下の分析は、影響を受けたHinkalコントラクトの過去のトランザクションに基づいている。攻撃者は複数の資産タイプ(USDC、ETHなど)をターゲットにしており、ここではUSDCのフローを例として使用する。

  • ステップ1:攻撃者はトランザクション 0xfbedf0...8c2f11 においてprooflessDeposit()経由で100 USDCをデポジットした。このデポジットのextraRandomizationは最上位ビットが0(getPointSign = 0)であり、ノートがレガシーフォーマットを使用していることを示している。

  • ステップ2:攻撃者はこの100 USDCのレガシーノートに対してtransact()を繰り返し呼び出し、毎回同じマークルルートに対して新しいヌリファイアを使用し、1回の呼び出しにつき100 USDCを引き出した。これがダブルスペンドであり、同一のノートが何度もスペンドされ、合計で約25,000 USDCが蓄積された。

  • ステップ3:攻撃者はトランザクション 0xbf7252...d50008 において別のprooflessDeposit()呼び出しにより、25,000 USDCすべてを1つのレガシーノートに統合した。より大きなノートに統合することで、以降のダブルスペンド引き出しでは100 USDCではなく25,000 USDCずつ取得できるようになった。

  • ステップ4:攻撃者は25,000 USDCのノートに対して同じプロセスを繰り返し、毎回新しいヌリファイアでtransact()を呼び出した。デポジット後、攻撃者は追加の資産を一切供給せず、それでもデポジットした25,000 USDCをはるかに超える金額を引き出した。

すべてのtransact()呼び出し合計で、約80万ドル相当の資産が流出した。

結論

Hinkalコントラクトは個別には有効な証明をすべて受け入れたが、レガシーノートフォーマットの欠陥により、単一のノートが何度も換金された可能性が高い。コントラクトのオンチェーンチェック(証明検証、ヌリファイアの一意性)はすべてパスしたが、同一のノートが複数の有効なヌリファイアを生成していることを検出できなかった。チームはその後、全チェーンにわたるすべてのHinkalスマートコントラクトを一時停止し、影響を受けたすべてのユーザーへの全額補償を約束した [1]。 Hinkal固有の緩和策としては、レガシーノートフォーマットのパスを完全に無効化すること、prooflessDeposit()でノートフォーマットの検証を強制すること(例:isNewStyle == 0のノートを拒否する)、そして一対一のヌリファイアバインディングを確認するための専用回路監査の実施が挙げられる。

ヌリファイアベースのプライバシープロトコル全般において、不変条件は同じである:各ノートは単一の明確に定義された導出によって正確に1つのヌリファイアにマッピングされなければならない。そのバインディングは回路レベルで強制される必要がある。なぜなら、コントラクトはヌリファイアがまだ見られていないことを確認できるだけで、それが特定のノートに対して唯一の有効なヌリファイアであるかどうかは確認できないからである。

Phalcon Explorerを始めよう

トランザクションを深く分析して賢明な行動を

今すぐ無料で試す

参考文献

[1] Hinkalプロトコルのインシデント後アップデート: https://x.com/hinkal_protocol/status/2073136163880149417

[2] 回路設計ドキュメント: https://hinkal-team.gitbook.io/hinkal/technical-description/circuits/swapper-m#id-3.-correct-nullifiers-per-input

[3] クライアントサイドコード: https://github.com/Hinkal-Protocol/Hinkal-API-Enclave ](https://github.com/Hinkal-Protocol/Hinkal-API-Enclave)

Phalcon Securityを始めよう

あらゆる脅威を検出し、重要なアラートを発信し、攻撃をブロック。

今すぐ無料で試す
Sign up for the latest updates
ニュースレター - 2026年6月
Security Insights

ニュースレター - 2026年6月

本月報は2026年6月の主要セキュリティインシデント3件を対象とし、確認済み損失総額は約2,200万ドルです。高度なハニーポット攻撃がトークン許可の未検証を悪用し、JaredFromSubwayのMEVボットから約1,500万ドルを流出。旧型Aztecロールアップ2件はプルーフ決済の境界ギャップにより約435万ドルを損失。SecondFiのEd25519実装欠陥により374ウォレットの秘密鍵が露出し約240万ドルが流出。3件に共通するのは、表面上は保たれているように見えたセキュリティ保証が実際には機能していなかった点です。

約400万ドルの損失:TaikoとSecondFiのエクスプロイト|BlockSec週次レポート
Security Insights

約400万ドルの損失:TaikoとSecondFiのエクスプロイト|BlockSec週次レポート

2026年6月22〜28日のブロックチェーンセキュリティ報告:EthereumとCardanoで約410万ドルの損失を確認。Taikoブリッジ攻撃はSGX署名鍵の露出と不完全なアテステーションポリシーを悪用し、悪意のあるプルーバー登録とL2状態証明の偽造を可能にした。SecondFiはEd25519ナンス導出の欠陥により、公開Cardanoトランザクションから秘密鍵の復元が可能となった。

〜1800万ドルの損失:jaredFromSubway、Aztecなど|BlockSecウィークリー
Security Insights

〜1800万ドルの損失:jaredFromSubway、Aztecなど|BlockSecウィークリー

ブロックチェーンセキュリティ週次レポート(2026年6月15日〜21日):EthereumとBNB Chainで3件の重大インシデントが発生し、総損失約1830万ドル。注目のjaredFromSubway事件では、MEVボットが裁定取引のために自身の資産を未検証の第三者コントラクトに承認するという逆承認攻撃が判明。攻撃者は実イベントを発行しながら承認を消費しない偽トークンとプールを構築し、損失は約1500万ドル。またAztecでは3日間で2度目の攻撃が発生、エスケープハッチZK回路でold_data_rootの等価制約欠如を悪用し、偽マークルツリーに対するノート所有権の証明が可能となった。

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit