Back to Blog

HKDAP ステーブルコインセキュリティレビュー:稼働中、ライセンス取得済み、しかし準備不足

Phalcon Security
August 14, 2026
28 min read
Key Insights
  • HKDAPのKYC失効はデッドコードであり、そのKYC証明はオンチェーンで検証されることはない

  • 単一の鍵でミント、バーン、一時停止、またはフリーズが可能であり、2つの鍵がすべてのアップグレードと役割を制御する

  • いくつかのオンチェーンの特性がHKMAステーブルコイン発行者ガイドラインと乖離している

A BlockSec による HKDAP(Anchorpoint)のセキュリティおよびコンプライアンスレビュー(2026年8月13日時点)。

TL;DR. 我々は、香港で発行された初の規制対象ステーブルコインである HKDAP のデプロイ済みコントラクトを、Ethereum メインネット上でレビューした。その結果、本番稼働可能な品質には達していない。KYC および失効(revocation)の制御は記載された通りに動作せず、ガバナンスは高度に集中しており、単一の鍵でミント、バーン、フリーズが可能である。さらに、オンチェーン上の複数の特性が HKMA 自身のガイドラインと矛盾している。根底には共通の糸がある。このコントラクトは、エコシステムがすでに提供し、大規模に監査しているプリミティブ(マルチシグ、アクセス制御レイヤー、タイムロック、ERC-20 自体)をゼロから再発明しており、欠陥のほとんどは、標準コンポーネントを再利用している部分ではなく、その独自の仕組みに存在する。「Beta Access」というラベルは、そのギャップを埋めるものではない。

2026年8月12日、Anchorpoint は香港ドル建てステーブルコインである HKDAP の第一フェーズを開始した。これは重要なローンチである。Standard Chartered Bank (Hong Kong) が主導し、HKT および Animoca Brands が出資する合弁会社である Anchorpoint は、36の申請者の中から HKMA が付与したわずか2つのステーブルコイン発行者ライセンスのうちの1つ を保有しており、HKDAP は香港のステーブルコイン条例の下で発行された最初のステーブルコインの1つである。

規制対象銀行のほとんどの製品とは異なり、HKDAP は直接検査が可能である。Ethereum メインネット上で稼働し、そのコントラクトのソースコードは Etherscan で検証されている。これは有用な特性である。このように発行されたステーブルコインでは、発行、送金、フリーズを管理するルールは文書に記述されているのではなく、誰でも読むことができ、記述された通りに正確に実行されるコードとして実装されている。ライセンスは主張であり、デプロイされたコントラクトはその主張の実装であり、それは公開されている。

これにより、具体的なレビューが可能になり、我々はそれを実施した。デプロイ済みコントラクトを2つの軸に沿って調査した。第一に、ソフトウェアとして:それは正しく、本番グレードか? 第二に、規制対象ステーブルコインとして:そのオンチェーン上の挙動は HKMA のライセンス付与ステーブルコイン発行者に関する監督ガイドラインと一致するか?

調査結果は両方の軸で一貫している。コントラクトには複数の機能的欠陥が含まれており、記載された通りに動作しないコンプライアンス制御も含まれる。ガバナンスは高度に集中しており、いくつかの高リスク操作は単一の鍵で実行できる。そして、オンチェーン上の多くの特性が HKMA ガイドラインの特定の条項と矛盾している。我々の評価は、ベータ版であっても、このコントラクトは商用ステーブルコインに求められる品質基準を満たしていないというものである。

この記事の残りの部分では、2026年8月13日時点のこのレビューを提示する。我々のレビューは、公開されたデプロイ済みコードと観測可能なオンチェーン上の事実に基づいている。準備金の裏付けやオフチェーンでの鍵管理など、ブロックチェーンでは確認できない事項について、我々は何ら主張しない。

コントラクトの見つけ方

我々はトークンリストからではなく、発行者から始めた。Anchorpoint の企業プレゼンスは、anchorpoint.hk のサイトにリンクしている。Beta Access ページにはデプロイ先が明記されている:Ethereum メインネット、プロキシ 0x87622385F960fcCB3121d6D0A9513bd1D9Bed6cA。そのソースは Etherscan で検証されている

そこから、システム全体をオンチェーンでマッピングした:トークンプロキシとその実装(ControllableAHKD0xe42d38b05d7ff702193ac5ccedb411b7298ffbfc)、それを管理するガバナンスコントラクト(0x47dd47a776902bee03abdd5aeff43f41b0ffbc2b)、トップレベルのロールレジストリ(0xa7287701f7ab8f38ef57cdb2a2a69a40128ec89c)、そしてそれぞれが独自のガバナンスコントラクトを持つ5つのコンプライアンスモジュール。以下のすべての関係は、ソースのみから推測したのではなく、メインネット上でストレージスロットを読み、ビュー関数を呼び出すことで検証された。

コントラクトの構造

HKDAP の3層構造:ガバナンスコントロールプレーンがトークンを管理し、トークンはすべての送金時に5つのコンプライアンスモジュールに問い合わせる。 image.png

HKDAP のトークンは、OpenZeppelin の標準的な ERC-20 リファレンス実装を使用していない。その ERC-20 ロジックはゼロから書かれている(後述のいくつかのバグはここに起因する)。システムには3つの層がある:

  • トークン。 アップグレード可能なプロキシの背後にある ControllableAHKD。ミント、バーン、ポーズ、強制破棄を備えた制御付き ERC-20 であり、すべての送金にコンプライアンスチェックが組み込まれている。
  • 自家製の M-of-N ガバナンスエンジン。 すべての特権アクション(アップグレード、ミント、バーン、ポーズ、ブラックリスト、フリーズ、コンプライアンスモジュールの変更)は、単純なマルチシグではなく、リクエスト、承認、実行という儀式を経る。
  • 5つのコンプライアンスモジュール。 ブラックリスト、フリーズ、KYC アクティベーションサービス、入金・償還ホワイトリスト。各モジュールはそれ自体がプロキシであり、独自のコントロールオーソリティコントラクトによって管理される。

構造的に、この同じ「コントロールオーソリティ+プロキシ+実装」ユニットが6回(トークン+5モジュール)繰り返され、6つのコントロールオーソリティはすべて単一のレジストリでロールを解決する。システム内のすべての制御は最終的にこの1つのレジストリに収束し、その中のロールを書き換えられるのは、後で詳述するように、非常に少数の署名者鍵に依存する。

パート1:セキュリティとバグ

このパートの調査結果を以下にまとめる。各行の詳細は記載されたセクションで説明する。

領域 調査結果 場所 影響
1.1 KYC および失効制御の失敗 KYC 失効はデッドコード TokenHolderActivationServerLibrary.sol:221-235 isActive はプロバイダーの登録解除を無視する(ループは実行されず、= ではなく == を使用);フェイルオープン。
検証者(Verifier)の登録解除が INACTIVE を設定しない TokenHolderActivationServer.sol:504-511 「登録解除された」検証者が依然としてウォレットをオンボードおよび非アクティブ化できる;2回目の呼び出しはリバートする。
KYC 証明がオンチェーンで検証されない TokenHolderActivationServer.sol:575-578 証明は渡されるが破棄される;空文字列を含む任意の証明が通過する。
無料送金の例外を分割して回避可能 ControllableAHKD.sol:337-535 freeTransferLimit は累積ではなく呼び出しごとにチェックされる;非 KYC 活動の上限として使用する場合、制限未満の送金に分割することでバイパスできる。
1.2 ガバナンスの過度な集中 高リスク操作が単一署名 authorizationMatrix;ミント tx 0xa7e53c…b33d7 ミント/バーン/フリーズ/KYC 非アクティブ化(ロール C)、ポーズ/破棄(ロール D)、ブラックリスト/アンフリーズ(ロール F)はそれぞれ1つの鍵で実行される。
1組の鍵(A + B)がすべてをアップグレード authorizationMatrix トークンと全5モジュールの upgradeTo はロール A + B;2人で任意の実装を置き換えられる。
同じ組がすべてのロールを書き換える レジストリ 0xa728… authorizationMatrix レジストリの grantRole / revokeRole もロール A + B(ADMIN_ROLE はコントラクトのみが保持;DEFAULT_ADMIN_ROLE = address(0));外部の鍵が直接ロールを変更することはできない。
1つのアドレスが6つのロールを保持 ロールレジストリ 0xa728… 0x2f7f00… はロール C に加えて ADMIN_TOKEN_HOLDER と全4つの監査ロールを保持;実行と監査が重複。
失効は即時的でなく、タイムロックなし HybridControlEngine.sol:158-227 カウントされた署名は、ロールが失効した後も再検証されない;実行は最終署名と原子的であり、遅延がない。
エンジンの監査証跡は信頼できない HybridControlEngine.sol:36-129, 177-196 evtApprove は署名者として address(0) を発行する;アクティブリクエストリストは nonce 0 を実際の ID と空マーカーの両方として使用するため、逆方向の走査は最初のリクエストを見逃す。
1.3 一貫性のない送金チェック transfertransferFrom で異なるルールを強制 ControllableAHKD.sol:313-502 ホワイトリストの早期リターンは transferFrom 経由でのみ KYC をバイパスする;checkingMode は異なる当事者をチェックする;同じ送金が異なるルールで管理される。
1.4 本番前ビルドの兆候 デバッグログ、誤ったコメント、名前/ハッシュの不一致、テストのアップロード UpgradeableProxy.sol:115-119ControllableAHKD.sol:25 プロキシのフォールバックに console.log(恒久的なガス);プロキシのコメントがコードと矛盾;ロール名/ハッシュの不一致;テストを含む117ファイルがアップロード済み;optimizer runs = 0。
1.5 唯一のアップグレードで欠陥が残った 唯一のアップグレードの追跡 tx 0x742372…851360xa7a400…630c 4月28日デプロイ → 7月10日アップグレード;A + B の儀式の2つのトランザクションは1ブロック差(約12秒);パート1のすべての欠陥はインストール済み実装にある。

1.1 KYC および失効制御は記載された通りに動作しない

規制上、HKDAP が提供する B サイド(機関投資家)ユーザーは KYC を通過する必要がある。コントラクトでは、これは少なくとも2つのことを行う必要があることを意味する:KYC を通過していないウォレットをブロックするように送金に KYC を強制すること、そしてアイデンティティプロバイダーまたはホルダーが削除されたときにアクセスを失効させること。HKDAP では、この経路は3つの独立した箇所で壊れている。

KYC 失効はデッドコードである。 トークンは KYC モジュールの isActive(address) を呼び出すことで送金をゲートする。isActive は2つのことを行うことを意図している:第一に、ウォレットごとのカウンターから基本値を設定する(そのウォレットが非アクティブ化された回数よりも KYC アクティブ化された回数が多い場合にアクティブ)。第二に、そのウォレットを保証したアイデンティティプロバイダーがその後登録解除された場合、その値を false に狭めること。この2つは2種類の失効をカバーする:1人のホルダーの KYC の失効(カウンター)と、プロバイダー全体を失効させ、それによってオンボードされたすべてのウォレットを巻き込むこと(ループ)。実際に起こるのは最初のものだけである。

// contracts/hce/erc20/libs/TokenHolderActivationServerLibrary.sol
221    active = ( tokenHolderRegistration[_address].deactivationCount < tokenHolderRegistration[_address].activationCount ) ;
223    uint256 entryCount = 0 ;
224    uint256 index = chainedItemList.firstEntry ;        // 0 if such ChainedList is empty
226    while ( entryCount > chainedItemList.entryCount && active ) {
227        ChainedListLibrary.ChainedItem storage chainedItem = identityProvidersByStatuss[index] ; // deregistered provider
230        // NDLR: .... not too sure about that one to be frank
231        active == ( tokenHolderKYCProofDirectoryByProvider[_address][identityProviders[chainedItem.objectId].identifier] == 0 ) ;
233        entryCount++ ;
234        index = chainedItem.pointNext ;
235    }

221行目は実際の代入(=)であり、これだけが有効になる:activedeactivationCount < activationCount になる。ループ(226-235行目)はその値を狭めるはずのものだが、2つの理由で失敗する。226行目の条件 entryCount > chainedItemList.entryCount0 > N であるため、本体は決して実行されない。また、仮に実行されたとしても、231行目は = で代入すべきところを == を使用しており、その結果は破棄される。したがって、isActive はカウンターの比較のみを返し、プロバイダーの失効を完全に無視する。これはフェイルオープンである:侵害された KYC プロバイダーを登録解除しても、そのプロバイダーがオンボードしたウォレットが取引するのを止められない。さらに、unregisterVerifier はそれらのカウンターに触れないため、失効したプロバイダーのウォレットは正のカウントを維持し、アクティブのままとなる。

プロバイダーの失効はもう一方の側でも壊れている。 unregisterVerifier はリンクリストのノードを移動するだけで、プロバイダーのディレクトリステータスを INACTIVE に設定することはない:

// contracts/hce/erc20/modules/TokenHolderActivationServer.sol
504    function _unregisterVerifier(string calldata _verifierId) internal {
505        _removeToList(_verifierId, ACTIVE_IDENTIY_PROVIDER, "60a");
507        uint256 _ipIdx       = identityProviders.length - 1;
508        uint256 _newIdx      = identityProvidersByStatuss.length + 1;
510        _addToList(_ipIdx, _newIdx, INACTIVE_IDENTIY_PROVIDER, _verifierId, "60b");
511    }

ステータスが ACTIVE のままであるため、「登録解除された」検証者は依然としてホルダーを登録および非アクティブ化でき、プロバイダーを再アクティブ化するための分岐は到達不能であり、2回目の unregisterVerifier 呼び出しはアンダーフローしてリバートする。

KYC 証明はオンチェーンで検証されることはない。 許可された検証者が registerOrRenew を通じてウォレットを登録または更新するとき(現在アクティブな検証者のみが呼び出せるようにゲートされている)、フローは _checkKYCProof に到達する。これは、提出された証明をプロバイダーのスキームに対して検証することを意図している。インターフェースによれば、その証明は「オラクルの URI、または検証可能なソースからの署名付きハッシュ」である。関数はそれを無視する:

// contracts/hce/erc20/modules/TokenHolderActivationServer.sol
575    function _checkKYCProof( string memory _ipIdentifier, string memory, string memory _rcCode ) internal view returns ( bool isVerified ) {
576        require( identityProviderDirectory[_ipIdentifier].status == ACTIVE_IDENTIY_PROVIDER, string.concat(ERROR_404, _rcCode, "41b")) ;
577        return true ;
578    }

証明は実際にこの関数に到達する。registerOrRenew は、提出された kycProof_registerOrRenew を通じて渡し、2番目の引数として渡す。しかし575行目で、その引数は名前すら持たないパラメータに置かれ、関数の上のドキュメントコメントもそれを省略しており、本体はそれを決して読み取らない。関数はプロバイダーがアクティブであることを確認し、577行目で true を返すだけなので、証明はチェックされるのではなく破棄される。これは部外者によるバイパスではない(アクティブな検証者だけが到達できる)が、オンチェーンではコントラクトは KYC 証拠の検証を一切行わないため、その整合性は完全にオフチェーンの検証者に依存する。侵害された、または不注意な検証者は、空文字列を含む任意の証明で任意のウォレットをアクティブ化できる。

これら3つを合わせると、中核的なコンプライアンス特性であるアクセスをゲートし失効させる能力が、記載された通りに機能しないことを意味する。

これら3つに加えて、関連する弱点がある:無料送金の例外は分割して回避できる。 トークンには freeTransferLimit があり、それを下回る送金は isActive(KYC)チェックをスキップする。しかし、この制限は現在の送金の金額とのみ比較され、コントラクトはアドレスまたは期間ごとの累計を保持しない。したがって、この制限を非 KYC 活動の上限として使用する場合、ホルダーは制限をわずかに下回る送金を繰り返し分割することで任意の合計を移動でき、上限は無効になる。

1.2 ガバナンスは過度に集中しており、高リスク操作は単一署名である

我々はオンチェーン上のすべてのロールとすべてのロール保持者を列挙した。詳細に入る前に、2つのことが際立っている。

第一に、M-of-N 儀式を許可するロールには読み取り可能な名前がない。デプロイされた構成では、それらは32バイトのハッシュとしてのみ表示され、検証済みソースの名前付きロール定数(SUPPLY_CONTROLLER_ROLE などの名前付きロールは署名者ではなくコントラクト自体が保持する)のいずれとも一致しない。我々は6つの署名者ロールを A から F とラベル付けする。システム内の最も強力なロールが不透明な識別子であることは、それ自体が弱点である:名前付きロールよりもガバナンスのレビューが難しくなる。

第二に、保持者の数は少ない。以下の表はオンチェーンのロールレジストリから読み取ったものであり、アドレスは省略されている。

ロール(当方のラベル) オンチェーンハッシュ 保持者 許可される内容
A 0x37f0f656… 0x747356d525bf47f495af7c6e42f3ab9b8ba87a4b upgradeTo / changeAdmin / setControlAuthority の最初の署名
B 0x95c27f81… 0x8117a657df7a1c399231bfe26a096eb8f8ad5973, 0x5d9d9b28e83382e694916b0feeaf7a9badbb659c, 0xebb73f78e253539acceb4e6cc287095788eee5d4 ほぼすべての2署名アクションの2番目の署名
C 0xfa2fe896… 0x2f7f00cc5334fe2861e485ff610f74890a0316ed mintToDeposit, burnFrom, freeze, KYC 非アクティブ化(単一)
D 0x3dce3265… 0x5092af62a1625fa57404557d3ad417474f3f494c pause, destroyBlackFunds, 入金・償還アドレスの登録/登録解除, registerVerifier(単一)
E 0xfd21a76d… 0xa9315aadc89ba681f1fe5df3375ab1a87d37eca2 コンプライアンスモジュールの変更(setBlacklistServer など)
F 0x510ac1ff… 0x7daabe5092d3feea5cc19ddbb5380a4eac38f89c addBlackList, unfreeze(単一)

この表からいくつかの点が導かれる。

単一署名者の高リスク操作。 ほとんどの高リスク操作は、クォータ1の単一ロールを必要とする。以下の表は、トークンのガバナンスコントラクトおよび5つのモジュールガバナンスコントラクトの authorizationMatrix からオンチェーンで読み取ったものである(+ B の2番目の署名が示されない限り単一署名):

操作 管理対象 必要な署名
mintToDeposit, burnFrom トークン C x1
freeze, batchFreeze フリージングモジュール C x1
deactivate, adminDeactivate(KYC) KYC モジュール C x1
pause, destroyBlackFunds トークン D x1
register, unregister(入金/償還) ディレクトリモジュール D x1
registerVerifier, unregisterVerifier KYC モジュール D x1
addBlackList, batchBlackList ブラックリストモジュール F x1
unfreeze フリージングモジュール F x1
removeBlackList ブラックリストモジュール F x1 + B x1
setBlacklistServer, setFreezingServer, setCheckingMode トークン E x1 + B x1
ディレクトリサーバーおよび供給上限のセッター トークン D x1 + B x1
upgradeTo / changeAdmin / setControlAuthority(トークンおよび全モジュール), unpause トークン+モジュール A x1 + B x1

発行、フリーズ、KYC 非アクティブ化は単一署名(すべてロール C の下);ポーズ、破棄、ディレクトリ変更、検証者登録はロール D の下で単一署名;ブラックリスト登録とアンフリーズはロール F の下で単一署名である。2番目の署名を必要とするのはアップグレードと構成変更のみである。非対称性に注意:freezeaddBlackList は1つの署名を必要とする一方、removeBlackList は2つを必要とするため、アカウントを制限することは解放することより容易である。

これは単なるマトリックスの読み取りではない。実際のミントで観測可能である。執筆時点での最新の発行である tx 0xa7e53c…b33d7 は、唯一のロール C アカウント(0x2f7f00cc5334fe2861e485ff610f74890a0316ed)からトークンのガバナンスコントラクトに送信された単一のトランザクションである。それは request を呼び出し、その同じトランザクションがクォーラムに達し、address(0) からのミント Transfer を発行する。別個の承認トランザクションも2番目の署名者もない。発行は要求者自身のトランザクション内で完了するため、1つの鍵がミントを要求し、かつ実行した。

また、エンジンのどこにもタイムロックはない。最後の必要な署名が到着した瞬間、アクションはその同じトランザクション内で実行され、レビュー、キャンセル、または異議申し立てができる遅延はない。エンジンは executedAt タイムスタンプを記録するが、それをチェックすることはない。したがって、2署名の操作でさえ、2番目の鍵が署名すると即座に完了する。

1組の鍵がすべてをアップグレードする。 トークンのアップグレードと5つのコンプライアンスモジュールすべてのアップグレードは同じ要件、A プラス B を使用する。A は1つのアカウントであり、B はロールを共有する3つのアカウントであるため、2人でシステム内の任意の実装を置き換えることができる。

同じ組がロールテーブルも制御する。 ロールはレジストリ(HybridControlledAuthority0xa7287701f7ab8f38ef57cdb2a2a69a40128ec89c)上でのみ、それ自体の儀式を通じて付与および失効できる。外部の鍵が直接変更することはできない。そして、その authorizationMatrix はオンチェーンで読み取ると、ロールの変更にアップグレードと同じ署名、つまりロール A プラスロール B を必要とする。したがって、任意の実装を置き換えることができる2人は、ロール C に鍵を追加し、既存の保持者を削除し、ロールテーブル全体を書き換えることもできる。この2署名ゲートは上記の単一署名操作よりはましだが、システムのルートとしては依然として低いハードルである。ロール A は冗長性のない単一アカウントだからである。

1つのアドレス、6つのロール。 ロール C の保持者(0x2f7f00cc5334fe2861e485ff610f74890a0316ed)は、名前付きの ADMIN_TOKEN_HOLDER_ROLE(KYC 検証者管理)も保持し、4つすべての監査ロール(BLACKLIST_AUDITOR_ROLEFREEZING_AUDITOR_ROLEWHITELIST_AUDITOR_ROLE、および AFL_TOKEN_AUDITOR_HOLDER_ROLE)のメンバーでもある。この1つの鍵の侵害は、発行、フリーズ、KYC 管理の同時喪失を意味する。

監査ロールにはそれ自体2つの問題がある。 第一に、それらはコンプライアンスリストの読み取り専用ゲッターをゲートしており、おそらく誰が読めるかを制御するためだが、パブリックチェーン上ではそれは無意味である:基礎となるストレージは誰でも読み取れる(ストレージスロットを読むことが、我々がこのシステムをマッピングした方法である)。したがって、リストはどちらにせよ公開されており、この制限は設計がパブリックチェーン上にあることを考慮していなかったことを示している。第二に、集中である:4つすべての監査ロールは同じ23のアカウントにあり、実行ロール A、C、D、F の保持者もその中にいる。したがって、ミント、バーン、フリーズ、ブラックリスト登録を行う同じ鍵が、それらのアクションをレビューすることを意図されたグループにもいる。

失効は即時的ではない。 儀式エンジンでは、署名者のロールは一度チェックされ、クォータがデクリメントされる。以前の署名者は再検証されることはないため、その後ロールを失効させても、すでにカウントされた投票は撤回されない:

// contracts/hce/HybridControlEngine.sol
158    for ( uint256 i=0; i < approvalRequest.ceremony.length; i++ ) {
159        if ( !_hasBeenMandated && !approvalRequest.ceremony[i].completed &&
160                IControlAuthority(authorityContract).hasRole( approvalRequest.ceremony[i].expectation.authority, _signer ) ) {
161            _hasBeenMandated = true ;
163            approvalRequest.ceremony[i].expectation.quota-- ;
167            approvalRequest.ceremony[i].completed = _approvalIntent && approvalRequest.ceremony[i].expectation.quota == 0 ;
168        }

最後に、エンジン自身の監査証跡は信頼できない。 承認のたびに、evtApprove イベントは実際の承認者ではなく address(0) を署名者として発行する。実際の署名者はトランザクション送信者と内部レコードにのみ残るため、イベントログは誰がリクエストを承認したかを特定できない。また、アクティブリクエストリストは nonce 0 を実際のリクエスト ID と空マーカーの両方として使用するため、リストを逆方向に辿るモニタリングまたは承認ツールは nonce 0 のリクエストを見逃す。どちらも致命的ではないが、クリーンな監査証跡を必要とする規制対象システムにとって、両者はそこから減算される。

1.3 transfertransferFrom の間で送金制御が一貫していない

2つの間の違いの一部は予想されるものである。transferFrom では、イニシエーターである msg.sender は承認されたスペンダーであり、資金の送金元ではない。そのため、コードはすべてのモードで実際の送金元 from を明示的にチェックする。transfer はその必要がない。そこでは msg.sender が送金元だからである。この適応は合理的である。他の2つの違いはイニシエーターによって説明されず、同じ経済的アクションが異なるルールで管理されることになる。

第一に、入金および償還ホワイトリストは transferFrom パスのみで参照され、メンバーシップがあると isActive(KYC)チェックをバイパスする早期リターンがトリガーされる。transfer はそれらを決して参照しない。受信者がホワイトリストに登録されているかどうかは、誰が送金を開始したかとは無関係である。したがって、同じ受信者が transfer では KYC の対象となるが、transferFrom ではそれをスキップできる:

// contracts/hce/ControllableAHKD.sol : transfer(), "Source" mode gates only the sender
323            else if ( activateMode == ActivateMode.Source )
325                _transferCheckSourceMode(_value, "80h");

// contracts/hce/ControllableAHKD.sol : transferFrom() -> _transferFromCheckDestinationMode(_to)
428        try depositDirectoryServer.isRegistered(_to)
429                    returns ( bool isIt, IWhitelistServer.WalletAddress memory ) {
430            if ( isIt ) { return ; }
436        try redemptionDirectoryServer.isRegistered(_to)
437                    returns ( bool isIt, IWhitelistServer.WalletAddress memory ) {
438            if ( isIt ) { return ; }
444        try tokenHolderActivationServer.isActive(_to) returns ( bool isIt ) {
445            require(isIt || _value < freeTransferLimit,     string.concat(ERROR_404, _rcCode, "/86a" ) ) ;

第二に、checkingMode は2つの関数で異なる意味を持つ。transfer では「Source」モードは送信者をチェックする。transferFrom では「Source」モードはスペンダー(msg.sender)をチェックし、from はすべてのモードでチェックされる。したがって、単一の構成設定がエントリポイントに応じて2つの異なるポリシーを強制する。

その結果、規制対象トークンの送金制御はどの関数が使用されるかに依存し、それによって推論が困難になり、構成によっては回避可能になる。

1.4 本番前ビルドの兆候

特定のロジックバグに加えて、コードベースのいくつかの特性は、本番前ビルドがメインネットにデプロイされたことを示している。

本番環境でのデバッグログ。 hardhat/console.log 呼び出しが全体に残っており、すべてのユーザートランザクションで実行されるプロキシのフォールバック内にも含まれる。プロキシ自体はアップグレード可能ではないため、このオーバーヘッドは永続的である:

// contracts/proxy/UpgradeableProxy.sol
115    function _beforeFallback() internal virtual override {
116        console.log("iam %s, entering fallback as %s", address(this), msg.sender) ;
117        // require(msg.sender != _getAdmin(), "[UPY]404/02");
118        super._beforeFallback();
119    }

コードの逆を説明するプロキシヘッダーコメント。 同じファイルには OpenZeppelin の TransparentUpgradeableProxy のドキュメントが含まれており、管理者は決して実装にフォールスルーできないと述べている。このコントラクトは意図的にその逆を行う:117行目のガードはコメントアウトされており、管理者はフォールスルーする。コメントを信頼するレビューアは、トラストバウンダリーを誤ってモデル化するだろう。

ハッシュと一致しないロール名。 「高リスク」ロールは、トークンとモジュールで同じ定数名だが異なる keccak 文字列で宣言されており、2つの異なるロールが生成される:

// contracts/hce/ControllableAHKD.sol  (token)    hash = 0xd2b9...
25    bytes32 internal constant ELEVATEDRISK_OWNER_ROLE = keccak256('ELEVATED_RISK_OWNER_ROLE') ;
// contracts/hce/erc20/modules/BlackListServer.sol  (module)    hash = 0x4de4...
18    bytes32 internal constant ELEVATEDRISK_OWNER_ROLE = keccak256('ELEVATEDRISK_OWNER_ROLE') ;

デプロイメントは、各モジュールが独自のコピーを保持するため、問題を回避しているだけである。2番目のロール名(AFL_TOKEN_HOLDER_AUDITOR_ROLE)も同じように転置されている。

その他の兆候。 プロキシの検証バンドルは、プロジェクトのテストスイートを含む117ファイルを公開エクスプローラーにアップロードしており、読者に内部テストとエッジケースを提供する。最新のアップグレードはコンパイラ警告のクリーンアップのみを変更し、第三者監査は関与していない。また、オプティマイザはゼロランに設定されており、頻繁に使用されるトークンのホットパスをより安くではなくより高くしている。

これらは個々には深刻ではない。しかし、まとめると、メインネット上で価値を保持するコントラクトに期待されるリリース規律をコードが経ていないことを示している。

1.5 コントラクトの唯一のアップグレード、追跡

プロキシは一度アップグレードされている。2026年4月28日に実装 0x8ff12fe3bed22d9e40afb4b98ef4dee28d94699e でデプロイされ、2026年7月10日に現在の 0xe42d38b05d7ff702193ac5ccedb411b7298ffbfc にアップグレードされた。アップグレードはオンチェーン上のガバナンスアクションであるため、誰が承認したかを正確に見ることができる。

upgradeTo はロール A プラスロール B を必要とする。アップグレードは隣接するブロック内の2つのトランザクションで、約12秒の間隔だった:

  • リクエスト、ロール A、0xa9315aadc89ba681f1fe5df3375ab1a87d37eca2 から、ブロック 25500519(tx 0x742372…85136);
  • 承認、ロール B、0x3795300b31429f9d37b0dc805528d9390ce87c50 から、ブロック 25500520(tx 0xa7a400…630c)。これはクォーラムに達し、同じトランザクション内でアップグレードを実行した。

2つのことが際立っている。第一に、2署名の儀式全体が単一のブロック間隔内で完了した。リクエストから実行まで1ブロックであり、2番目の署名者が変更が稼働する前に独立してレビューできるウィンドウはなかった。

第二に、どちらの署名者も現在のロールレジストリからは特定できない。ロールはその後ローテーションされている:リクエスターの 0xa9315a… はもはやロール A を保持しておらず(現在はロール E を保持)、2番目の署名者 0x3795300b… は現在まったくロールを保持していない。現状のレジストリを読んでも、誰がアップグレードを承認したかはわからない。トランザクション履歴だけが教えてくれる。これは「失効は即時的ではない」という特性を反対側から見たものである:ロールは動くので、誰が何を保持しているかのスナップショットは、誰が何をしたかの記録ではない。

そして、アップグレードはこのレビューのいかなる欠陥も修正しなかった。それがインストールした実装 0xe42d38b0… は、パート1のすべてが説明するものである。アップグレード前に第三者監査が実施されたかどうかをチェーンから知ることはできない。もし実施されたとしても、パート1の欠陥がそれを生き延びたと言えるだけである。

パート2:香港のステーブルコイン枠組みと一致するか?

香港のステーブルコイン条例は2025年8月1日に発効し、ライセンス付与発行者は HKMA のライセンス付与ステーブルコイン発行者に関する監督ガイドラインの下で監督される。我々は、スマートコントラクトが単独で満たすことができる条項のみを比較した。準備金の裏付け、カストディ、オフチェーンでの鍵の儀式は、オンチェーンレビューの範囲外である。以下の各条項について、ガイドラインが要求するもの、コントラクトが行うこと、両者がどこで乖離するかを述べる。比較はここに要約され、後続のセクションで詳述される。

HKMA 条項 要求内容 コントラクトの乖離点 判定
6.5.3 高リスク操作は一方的であってはならない(マルチシグ、および速度制限やタイムロックなどの緩和策) ミント、バーン、ポーズ、フリーズはそれぞれ1つの鍵で実行される;タイムロックなし(実行は最終署名と原子的) 乖離
6.5.4 許可された者間で職務を分離する;権限を直ちに失効させる 1つのアカウントが6つのロールを保持するため、実行と監査が重複する;カウントされた承認は後の失効後も存続する 乖離
6.5.5 すべてのコード変更について第三者監査;正確で、一貫性があり、脆弱性がない パート1の欠陥がデプロイ済み実装に存在する;isActive とプロバイダー失効は、その名前が示すことを行わない 乖離
コンプライアンス制御の有効性 ブラックリスト、フリーズ、ホワイトリスト、KYC 制御が有効でなければならない KYC ゲーティングとプロバイダー失効がデプロイ済みコードで機能しない 乖離
2.2.3 フリーズまたは破棄されたコインは完全に裏付けられ、照合可能でなければならない destroy は Transferaddress(0) ではなく address(this) に発行し、純発行カウンターに触れない;イベントから再構築された供給量は乖離する 懸念

パラグラフ 6.5.3:高リスク操作は一方的であってはならない

要求内容。 高リスク操作は、例えばマルチシグネチャプロトコルを通じて、単一の当事者が一方的に実行できないように設計されるべきであり、ガイドラインは速度制限や時間遅延(タイムロック)制御などのさらなる緩和策を列挙している。

コントラクトの実際。 ガバナンスコントラクトの authorizationMatrix(完全なマトリックスは 1.2 の表にある)から読み取ると、供給および緊急操作はそれぞれ、クォータ1の単一ロールを必要とする:

操作 必要な署名
mintToDeposit ROLE_C x1
burnFrom ROLE_C x1
freeze ROLE_C x1
pause ROLE_D x1
destroyBlackFunds ROLE_D x1

乖離点。 ミント、バーン、ポーズ、フリーズはそれぞれ1つの鍵で実行できる。これは「単一の当事者が一方的に行わない」という要件を満たさない。また、タイムロックもない:1.2 で示したように、クォーラムに達するとアクションは同じトランザクション内で実行されるため、ガイドラインが挙げる緩和策の1つである時間遅延も欠如している。コントラクトは供給速度制限(whenWithinRiskThresholds)を実装しているため、その緩和策は存在するが、操作自体のマルチシグネチャ制御の代わりにはならない。

パラグラフ 6.5.4:職務の分離と即時失効

要求内容。 異なる操作は異なる許可された者に分離されるべきであり、許可された者の権限は直ちに失効可能であるべきである。

コントラクトの実際。 1つの外部所有アカウントが6つのロール(発行、フリーズ、KYC 管理、および4つすべての監査ロール)を保持しているため、実行ロールと監査ロールが重複する。ロール変更自体もロール A プラスロール B のみを必要とするため(1.2 参照)、誰が権限を与えられ、実行およびアップグレードできるかを決定するのは同じ小グループである。また、すでにカウントされた署名は、署名者のロールが後で失効した場合に再検証されない。

乖離点。 職務は分離されているのではなく集中しており、失効は即時的ではない:失効した署名者の以前の承認は、後の実行に向けて依然としてカウントされる。

パラグラフ 6.5.5:すべてのコード変更を監査;正確で、一貫性があり、脆弱性がない

要求内容。 適格な第三者がすべてのコード変更についてスマートコントラクトを監査し、(i) 正しく実装されている、(ii) 意図された機能と一貫している、(iii) 高い信頼度で脆弱性がないことを確認する必要がある。

コントラクトの実際。 現在の実装は7月10日のアップグレードでインストールされたものであり(1.5 で追跡)、パート1の欠陥がその中に存在する。

乖離点。 監査が実施されたかどうかをオフチェーンで見ることはできないが、結果はいずれにせよ基準を満たしていない。isActive とプロバイダー失効がその名前が示すことを行わないため、条件 (i) と (ii) は満たされない。そして、パート1の欠陥がデプロイ済みコードに存在するため、(iii) も満たされない。

コンプライアンス制御の有効性

要求内容。 ガイドラインのライフサイクルモデル(ブラックリスト、フリーズ、ホワイトリスト、KYC)は、これらの制御が有効であることを前提としている。

コントラクトの実際。 1.1 で示したように、KYC ゲーティングとプロバイダー失効はデプロイ済みコードで機能しない。

乖離点。 機能することが要求されている制御が機能しない。これは形式的な問題ではなく、実質的なギャップである。

パラグラフ 2.2.3:フリーズまたは破棄されたコインは完全に裏付けられ、照合可能なままである

要求内容。 執行措置によってフリーズまたは破棄されたステーブルコインは、供給量と準備金を照合できるように、完全に裏付けられたままであるべきである。

コントラクトの実際。 規制対象ステーブルコインの通常の是正フローは、不良アドレスの資金をバーンし、後で被害者に同額を別個のミントとして再発行することである(これが USDT の destroyBlackFunds プラス issue の仕組みである)。HKDAP の destroy は _totalSupply を減少させるため、これはバーンであるが、balances[address(this)] に貸方記録することはなく、Transferaddress(0) ではなく address(this) に発行し、ミント制限が使用する純発行カウンターに触れない:

// contracts/hce/ControllableAHKD.sol
628    function destroyBlackFunds(address _blackListedUser) external override whenNotPaused onlySupplyDestroyer() {
636        uint dirtyFunds = balanceOf(_blackListedUser);
637        balances[_blackListedUser] = 0;
638        _totalSupply = _totalSupply - dirtyFunds ;
639        emit DestroyedBlackFunds(_blackListedUser, dirtyFunds);
640        emit Transfer(_blackListedUser, address(this), dirtyFunds);
641    }

乖離点。 状態変更はバーンであるが、イベントはトークンがコントラクトに移動したと述べており、そこには何も保持されない(balances[address(this)] はゼロのまま)。インデクサーは、保持していないトークンを address(this) に貸方記録し、イベントから再構築された総供給量はチェーンと一致しない。また、是正自体も混乱させる:トークンは待機させられるのではなくバーンされるため、被害者への再発行は新しいミントでなければならない。しかし、誤解を招く Transfer(..., address(this), ...) は、コントラクトがそれらをカストディし、転送できることを示唆するが、それは不可能である。address(0) へのクリーンなバーンと別個の再発行は、正確かつ照合可能である。記載されたままでは、準備金の照合が依存するオンチェーン上の会計が、チェーンの真の状態から乖離する。これはクリーンパスではなく懸念事項である。

我々はこれらの調査結果を、チェーンが示すものに限定する。準備金が完全に裏付けられているか、鍵が HSM またはエアギャップ環境にあるか、トランザクションが署名前にオフチェーンでシミュレーションされるかは、コントラクトからは見えず、我々はそれらについて何ら主張しない。

結論

レビューの両方の軸にわたって、状況は一貫している。ソフトウェアとして、HKDAP には機能的欠陥が含まれており、記載された通りに実行されないコンプライアンス制御も含まれ、本番前ビルドのいくつかの兆候を示している。規制対象ステーブルコインとして、そのオンチェーン上の複数の特性が HKMA ガイドラインの特定の条項と矛盾している。オンチェーンの証拠に基づき、我々が見ることのできないオフチェーンの事項を脇に置けば、デプロイ済みコントラクトは、商用ステーブルコインが満たすべき基準をまだ満たしていない。

以下に続く2つの観察結果は、明確に述べる価値がある。

第一に、パブリックチェーン上で発行することは、コンプライアンスがどこで決定されるかを変える。「単一の当事者が一方的に行動できるべきではない」などの要件は、デプロイ済みコードのロールチェックによって満たされるかどうかが決まり、そのコードは公開されている。ライセンスやドキュメントではなく、実装こそが、そのような要件が実際に満たされるか、満たされないかの場所であり、誰でもどちらかを検証できる。

第二に、「Beta Access」というラベルは、デプロイ済みコードのリスクプロファイルを変えない。コントラクトは Ethereum メインネット上で稼働し、実際の鍵によって管理され、香港ドルに対する請求権を表している。したがって、どのようにラベル付けされているかに関係なく、本番基準に準拠して評価されるべきである。

特定の調査結果の下には共通の糸が走っている。このアーキテクチャは、エコシステムがすでに提供し、大規模に監査しているプリミティブを、特注の形式で再発明している:OpenZeppelin の AccessManager と TimelockController を備えた Safe マルチシグで済むところに、ゼロからの承認エンジンとロールレイヤー;EnumerableSet の代わりに手書きのリンクリストコレクション;標準の TransparentUpgradeableProxy の代わりに修正されたプロキシ;そして OpenZeppelin の代わりに手書きの ERC-20。このレビューの欠陥のほとんどは、標準コンポーネントを再利用している部分ではなく、その独自の仕組みに存在する。 これは、オンチェーン開発が好む、監査済みの小さな構成要素から組み立てられるのではなく、汎用ソフトウェアが抽象化される方法で抽象化されたシステムのように読める。そこでは、カスタム抽象化のすべての層が、ガス、攻撃対象領域、アップグレードリスクでもある。それらの標準コンポーネントから構築された設計は、より小さく、より安全で、レビューが容易であり、このシステムが現在欠いているもの、つまりタイムロックによる熟慮のウィンドウと、名前付きで読みやすいロールを備えているだろう。

ここで説明された問題は対処可能である。高リスク操作へのマルチシグネチャの復元、実行と監査の分離、KYC 失効ロジックの修正、送金チェックの統一、デバッグコードの削除、各アップグレード前の第三者監査の要求は、そのほとんどを解決するだろう。検証済みソースでメインネットにデプロイすることは、このレビューを可能にしたものでもあり、それは正しいデフォルトである。この種の継続的なオンチェーンセキュリティおよびコンプライアンスレビューは、BlockSec が行っていることであり、我々は喜んで支援する。

Get Real-Time Protection with Phalcon Security

Audits alone are not enough. Phalcon Security detects attacks in real time and blocks threats mid-flight.

phalcon security