A BlockSec Sicherheits- und Compliance-Überprüfung von HKDAP (Anchorpoint), Stand 13. August 2026.
TL;DR. Wir haben den deployed Vertrag von HKDAP überprüft, der ersten regulierten Stablecoin, die in Hongkong ausgegeben wurde und live im Ethereum-Mainnet ist — und er ist nicht produktionsreif. Seine KYC- und Widerrufskontrollen funktionieren nicht wie vorgesehen, seine Governance ist so stark konzentriert, dass ein einzelner Schlüssel minten, brennen oder einfrieren kann, und mehrere seiner On-Chain-Eigenschaften stehen im Widerspruch zur eigenen Leitlinie der HKMA. Dem Ganzen liegt ein gemeinsamer roter Faden zugrunde: Der Vertrag erfindet die Primitive, die das Ökosystem bereits bereitstellt und in großem Maßstab auditiert, von Grund auf neu (ein Multisig, eine Zugriffskontrollschicht, ein Timelock, der ERC-20 selbst), und die meisten Defekte stecken in dieser eigenen Mechanik, nicht in den Teilen, die Standardkomponenten wiederverwenden. Das Label „Beta Access“ schließt diese Lücke nicht.
Am 12. August 2026 startete Anchorpoint die erste Phase von HKDAP, eine Hongkong-Dollar-Stablecoin. Es ist ein bedeutender Start. Anchorpoint, ein Joint Venture unter der Führung der Standard Chartered Bank (Hongkong) mit HKT und Animoca Brands, hält eine von nur zwei Stablecoin-Emittentenlizenzen, die die HKMA vergeben hat, von 36 Bewerbern, und HKDAP gehört zu den ersten Stablecoins, die unter Hongkongs Stablecoins Ordinance ausgegeben wurden.
Anders als die meisten Produkte einer regulierten Bank ist HKDAP direkt prüfbar. Es läuft im Ethereum-Mainnet, und der Quellcode des Vertrags ist auf Etherscan verifiziert. Das ist eine nützliche Eigenschaft: Bei einer so ausgegebenen Stablecoin sind die Regeln für Ausgabe, Übertragung und Einfrieren nicht in einem Dokument beschrieben, sondern in Code implementiert, den jeder lesen kann und der exakt so ausgeführt wird, wie er geschrieben ist. Eine Lizenz ist eine Behauptung; der deployed Vertrag ist die Umsetzung dieser Behauptung, und er ist öffentlich.
Das macht eine konkrete Überprüfung möglich, und genau das haben wir getan. Wir haben den deployed Vertrag entlang zweier Achsen untersucht. Erstens als Software: Ist er korrekt und produktionsreif? Zweitens als regulierte Stablecoin: Entspricht sein On-Chain-Verhalten der Leitlinie zur Beaufsichtigung lizenzierter Stablecoin-Emittenten der HKMA?
Die Ergebnisse sind entlang beider Achsen konsistent. Der Vertrag enthält mehrere funktionale Defekte, darunter Compliance-Kontrollen, die nicht wie vorgesehen funktionieren. Seine Governance ist stark konzentriert; mehrere Hochrisiko-Operationen können mit einem einzigen Schlüssel ausgeführt werden. Und eine Reihe seiner On-Chain-Eigenschaften steht im Widerspruch zu konkreten Klauseln der HKMA-Leitlinie. Unsere Einschätzung ist, dass der Vertrag selbst als Beta nicht die Qualitätslatte erfüllt, die eine kommerzielle Stablecoin verlangt.
Der Rest dieses Artikels präsentiert diese Überprüfung, Stand 13. August 2026. Unsere Überprüfung basiert auf öffentlich bereitgestelltem Code und beobachtbaren On-Chain-Fakten. Wir treffen keine Aussagen über Dinge, die die Blockchain nicht belegen kann, etwa Reservehinterlegung oder Off-Chain-Schlüsselverwahrung.
Wie wir den Vertrag gefunden haben
Wir sind vom Emittenten ausgegangen, nicht von einer Token-Liste. Anchorpoints Unternehmenspräsenz verlinkt auf seine Website unter anchorpoint.hk. Die Beta-Access-Seite nennt das Deployment: Ethereum-Mainnet, Proxy 0x87622385F960fcCB3121d6D0A9513bd1D9Bed6cA, dessen Quellcode auf Etherscan verifiziert ist.
Von dort aus haben wir das gesamte System on-chain kartiert: den Token-Proxy und seine Implementierung (ControllableAHKD, 0xe42d38b05d7ff702193ac5ccedb411b7298ffbfc), den Governance-Vertrag, der ihn verwaltet (0x47dd47a776902bee03abdd5aeff43f41b0ffbc2b), das Top-Level-Rollenregister (0xa7287701f7ab8f38ef57cdb2a2a69a40128ec89c) und fünf Compliance-Module, jedes mit einem eigenen Governance-Vertrag. Jede der folgenden Beziehungen wurde durch Lesen von Storage-Slots und Aufrufen von View-Funktionen im Mainnet verifiziert, nicht nur aus dem Quellcode abgeleitet.
Wie der Vertrag aufgebaut ist

Der Token von HKDAP verwendet nicht OpenZeppelins Standard-ERC-20-Referenzimplementierung; seine ERC-20-Logik ist von Grund auf neu geschrieben (hier entstehen einige der unten genannten Bugs). Das System hat drei Schichten:
- Der Token.
ControllableAHKD, hinter einem upgradebaren Proxy. Ein kontrollierter ERC-20 mit Mint, Burn, Pause und erzwungenem Destroy sowie Compliance-Prüfungen, die in jede Übertragung eingebunden sind. - Eine eigen entwickelte M-of-N-Governance-Engine. Jede privilegierte Aktion (Upgrade, Mint, Burn, Pause, Blacklist, Freeze, Änderung eines Compliance-Moduls) durchläuft eine Request-Approve-Execute-Zeremonie statt eines einfachen Multisigs.
- Fünf Compliance-Module. Blacklist, Freezing, ein KYC-Aktivierungsdienst sowie Einzahlungs- und Einlösungs-Whitelists. Jedes Modul ist selbst ein Proxy, der von einem eigenen Control-Authority-Vertrag regiert wird.
Strukturell wiederholt sich dieselbe Einheit aus „Control Authority plus Proxy plus Implementierung“ sechsmal (der Token plus fünf Module), und alle sechs Control Authorities lösen ihre Rollen über ein einziges Register auf. Die gesamte Kontrolle im System läuft letztlich auf dieses eine Register zusammen, und wer die Rollen darin neu schreiben kann, läuft auf eine sehr kleine Gruppe von Signaturschlüsseln hinaus, wie wir unten im Detail zeigen.
Teil 1: Sicherheit und Bugs
Die Ergebnisse dieses Teils sind unten zusammengefasst; jede Zeile wird im angegebenen Abschnitt ausführlich behandelt.
| Bereich | Befund | Wo | Auswirkung |
|---|---|---|---|
| 1.1 KYC- und Widerrufskontrollen versagen | KYC-Widerruf ist toter Code | TokenHolderActivationServerLibrary.sol:221-235 |
isActive ignoriert die Abmeldung von Anbietern (die Schleife läuft nie und verwendet == statt =); Fail-Open. |
| Verifier-Abmeldung setzt nie INACTIVE | TokenHolderActivationServer.sol:504-511 |
Ein „abgemeldeter“ Verifier kann weiterhin Wallets anlegen und deaktivieren; ein zweiter Aufruf reverts. | |
| KYC-Nachweis wird nie on-chain validiert | TokenHolderActivationServer.sol:575-578 |
Der Nachweis wird übergeben, aber verworfen; jeder Nachweis, einschließlich eines leeren Strings, wird akzeptiert. | |
| Die Ausnahme für freie Transfers kann durch Aufteilen umgangen werden | ControllableAHKD.sol:337-535 |
freeTransferLimit wird pro Aufruf geprüft, nicht kumulativ; wenn es als Obergrenze für Nicht-KYC-Aktivität dient, kann es durch Aufteilen in Transfers unterhalb des Limits umgangen werden. |
|
| 1.2 Governance ist überkonzentriert | Hochrisiko-Operationen sind Einzelsignaturen | authorizationMatrix; Mint-Tx 0xa7e53c…b33d7 |
Mint / Burn / Freeze / KYC-Deaktivierung (Rolle C), Pause / Destroy (Rolle D), Blacklist / Unfreeze (Rolle F) werden jeweils mit einem Schlüssel ausgeführt. |
| Ein Schlüsselpaar (A + B) aktualisiert alles | authorizationMatrix |
upgradeTo für den Token und alle fünf Module erfordert Rolle A + B; zwei Personen können jede Implementierung ersetzen. |
|
| Dasselbe Paar schreibt alle Rollen neu | Register 0xa728… authorizationMatrix |
grantRole / revokeRole im Register sind ebenfalls Rolle A + B (ADMIN_ROLE wird nur vom Vertrag gehalten; DEFAULT_ADMIN_ROLE = address(0)); kein externer Schlüssel kann Rollen direkt ändern. |
|
| Eine Adresse hält sechs Rollen | Rollenregister 0xa728… |
0x2f7f00… hält Rolle C sowie ADMIN_TOKEN_HOLDER und alle vier Auditor-Rollen; Ausführung und Prüfung überschneiden sich. |
|
| Widerruf ist nicht sofort; kein Timelock | HybridControlEngine.sol:158-227 |
Eine gezählte Signatur wird nach einem Rollenentzug nicht erneut validiert; die Ausführung ist atomar mit der letzten Signatur, ohne Verzögerung. | |
| Der Audit-Trail der Engine ist unzuverlässig | HybridControlEngine.sol:36-129, 177-196 |
evtApprove emittiert address(0) als Unterzeichner; die Liste aktiver Requests verwendet Nonce 0 sowohl als echte ID als auch als Leermarker, sodass die Rückwärtsdurchquerung den ersten Request übersieht. |
|
| 1.3 Inkonsistente Transferprüfungen | transfer und transferFrom erzwingen unterschiedliche Regeln |
ControllableAHKD.sol:313-502 |
Der Whitelist-Frühreturn umgeht KYC nur über transferFrom; checkingMode prüft verschiedene Parteien; derselbe Transfer wird unterschiedlich geregelt. |
| 1.4 Anzeichen eines Vorproduktions-Builds | Debug-Logs, falscher Kommentar, Name/Hash-Mismatch, Tests hochgeladen | UpgradeableProxy.sol:115-119; ControllableAHKD.sol:25 |
console.log im Proxy-Fallback (permanentes Gas); Proxy-Kommentar widerspricht dem Code; Rollenname/Hash-Mismatch; 117 Dateien inkl. Tests hochgeladen; Optimizer-Runs = 0. |
| 1.5 Das einzige Upgrade hinterließ die Defekte | Rückverfolgung des einzelnen Upgrades | tx 0x742372…85136, 0xa7a400…630c |
Deploy 28. Apr → Upgrade 10. Jul; die zwei Transaktionen der A + B-Zeremonie lagen einen Block auseinander (~12s); jeder Defekt aus Teil 1 steckt in der installierten Implementierung. |
1.1 Die KYC- und Widerrufskontrollen funktionieren nicht wie vorgesehen
Nach der Regulierung müssen die B-seitigen (institutionellen) Nutzer, die HKDAP bedient, KYC durchlaufen. Im Vertrag bedeutet das, er muss mindestens zwei Dinge tun: KYC bei Übertragungen durchsetzen, sodass ein Wallet, das KYC nicht bestanden hat, blockiert wird, und den Zugriff entziehen, wenn ein Identitätsanbieter oder ein Halter entfernt wird. Bei HKDAP ist dieser Pfad an drei unabhängigen Stellen kaputt.
KYC-Widerruf ist toter Code. Der Token regelt Transfers, indem er isActive(address) auf dem KYC-Modul aufruft. isActive soll zwei Dinge tun: erstens einen Basiswert aus einem Pro-Wallet-Zähler setzen, aktiv, wenn dieses Wallet öfter KYC-aktiviert als deaktiviert wurde; zweitens diesen Wert auf false einschränken, wenn ein Identitätsanbieter, der für das Wallet gebürgt hat, inzwischen abgemeldet wurde. Die beiden decken zwei Arten des Widerrufs ab: den Widerruf des KYC eines einzelnen Halters (der Zähler) und den Widerruf eines ganzen Anbieters, sodass jedes von ihm angebundene Wallet mitfällt (die Schleife). Nur Ersteres passiert.
// 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 }
Zeile 221 ist eine echte Zuweisung (=), und sie ist die einzige, die wirkt: active wird zu deactivationCount < activationCount. Die Schleife (Zeilen 226-235) soll diesen Wert einschränken, scheitert aber zweimal. Ihre Bedingung in Zeile 226, entryCount > chainedItemList.entryCount, ist 0 > N, der Schleifenkörper wird also nie ausgeführt; und selbst wenn er liefe, verwendet Zeile 231 ==, einen Vergleich, dessen Ergebnis verworfen wird, wo eigentlich mit = zugewiesen werden müsste. isActive gibt daher nur den Zählervergleich zurück und ignoriert den Anbieter-Widerruf vollständig. Das ist Fail-Open: Das Abmelden eines kompromittierten KYC-Anbieters stoppt die von ihm angebundenen Wallets nicht daran, Transaktionen durchzuführen. Und weil unregisterVerifier diese Zähler nicht anfasst, behalten die Wallets eines widerrufenen Anbieters einen positiven Zählerstand und bleiben aktiv.
Auch die Anbieter-Abmeldung ist auf der anderen Seite kaputt. unregisterVerifier verschiebt nur einen Knoten der verknüpften Liste; es setzt den Verzeichnisstatus des Anbieters nie auf 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 }
Da der Status ACTIVE bleibt, kann ein „abgemeldeter“ Verifier weiterhin Halter registrieren und deaktivieren, der Zweig, der einen Anbieter reaktivieren soll, ist unerreichbar, und ein zweiter unregisterVerifier-Aufruf verursacht einen Underflow und reverts.
Der KYC-Nachweis wird nie on-chain validiert. Wenn ein autorisierter Verifier ein Wallet über registerOrRenew registriert oder erneuert (so eingeschränkt, dass nur ein aktuell aktiver Verifier ihn aufrufen kann), erreicht der Ablauf _checkKYCProof, das den eingereichten Nachweis gegen das Schema des Anbieters validieren soll. Laut Interface ist dieser Nachweis „eine URI für ein Orakel oder ein signierter Hash aus einer verifizierbaren Quelle“. Die Funktion ignoriert ihn:
// 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 }
Der Nachweis erreicht diese Funktion tatsächlich. registerOrRenew trägt den eingereichten kycProof durch _registerOrRenew nach unten und übergibt ihn als zweites Argument. Aber in Zeile 575 landet dieses Argument in einem Parameter ganz ohne Namen, den auch der Dokumentationskommentar über der Funktion weglässt, und der Funktionskörper liest ihn nie. Die Funktion bestätigt nur, dass der Anbieter aktiv ist, und gibt in Zeile 577 true zurück; der Nachweis wird also verworfen statt geprüft. Das ist kein Umgehen von außen, da nur ein aktiver Verifier ihn erreichen kann, aber on-chain führt der Vertrag keine Validierung des KYC-Nachweises durch, sodass seine Integrität vollständig vom Off-Chain-Verifier abhängt. Ein kompromittierter oder nachlässiger Verifier kann jedes Wallet mit jedem Nachweis aktivieren, auch mit einem leeren String.
Zusammen bedeuten diese drei Punkte, dass eine zentrale Compliance-Eigenschaft, die Möglichkeit, Zugriff zu gewähren und zu entziehen, nicht wie vorgesehen funktioniert.
Über diese drei Punkte hinaus gibt es eine damit zusammenhängende Schwäche: Die Ausnahme für freie Transfers kann durch Aufteilen umgangen werden. Der Token hat ein freeTransferLimit, und eine Übertragung darunter überspringt die isActive-(KYC-)Prüfung. Aber das Limit wird nur gegen den Betrag der aktuellen Übertragung verglichen; der Vertrag führt keine kumulative Summe pro Adresse oder Zeitraum. Wenn das Limit also als Obergrenze für Nicht-KYC-Aktivität dient, kann ein Halter einen beliebigen Gesamtbetrag bewegen, indem er ihn in wiederholte Transfers knapp unter dem Limit aufteilt, was die Obergrenze wirkungslos macht.
1.2 Governance ist überkonzentriert, und Hochrisiko-Operationen sind Einzelsignaturen
Wir haben jede Rolle und jeden Rolleninhaber on-chain erfasst. Zwei Dinge fallen vor den Details auf.
Erstens haben die Rollen, die die M-of-N-Zeremonien autorisieren, keine lesbaren Namen. In der deployed Konfiguration erscheinen sie nur als 32-Byte-Hashes, und keine von ihnen entspricht einer benannten Rollenkonstante im verifizierten Quellcode (die benannten Rollen wie SUPPLY_CONTROLLER_ROLE werden von den Verträgen selbst gehalten, nicht von den Signataren). Wir bezeichnen die sechs Signaturrollen als A bis F. Dass die mächtigsten Rollen im System undurchsichtige Identifikatoren sind, ist selbst eine Schwäche: Es macht die Governance schwerer zu prüfen als benannte Rollen.
Zweitens ist die Zahl der Inhaber klein. Die folgende Tabelle wird aus dem Rollenregister on-chain gelesen; Adressen sind abgekürzt.
| Rolle (unser Label) | On-Chain-Hash | Inhaber | Autorisiert |
|---|---|---|---|
| A | 0x37f0f656… |
0x747356d525bf47f495af7c6e42f3ab9b8ba87a4b |
erste Signatur für upgradeTo / changeAdmin / setControlAuthority |
| B | 0x95c27f81… |
0x8117a657df7a1c399231bfe26a096eb8f8ad5973, 0x5d9d9b28e83382e694916b0feeaf7a9badbb659c, 0xebb73f78e253539acceb4e6cc287095788eee5d4 |
die zweite Signatur bei fast jeder Zwei-Signatur-Aktion |
| C | 0xfa2fe896… |
0x2f7f00cc5334fe2861e485ff610f74890a0316ed |
mintToDeposit, burnFrom, freeze, KYC-Deaktivierung (einzeln) |
| D | 0x3dce3265… |
0x5092af62a1625fa57404557d3ad417474f3f494c |
pause, destroyBlackFunds, Registrieren/Abmelden von Einzahlungs- und Einlösungsadressen, registerVerifier (einzeln) |
| E | 0xfd21a76d… |
0xa9315aadc89ba681f1fe5df3375ab1a87d37eca2 |
Ändern der Compliance-Module (setBlacklistServer usw.) |
| F | 0x510ac1ff… |
0x7daabe5092d3feea5cc19ddbb5380a4eac38f89c |
addBlackList, unfreeze (einzeln) |
Aus der Tabelle ergeben sich mehrere Punkte.
Hochrisiko-Operationen mit nur einem Signatar. Die meisten Hochrisiko-Operationen erfordern eine einzelne Rolle mit einem Quorum von eins. Die folgende Tabelle wird on-chain aus der authorizationMatrix des Governance-Vertrags des Tokens und der fünf Modul-Governance-Verträge gelesen (Einzelsignatur, sofern nicht eine zweite Signatur + B angegeben ist):
| Operation | Geregelt durch | Erforderliche Signaturen |
|---|---|---|
mintToDeposit, burnFrom |
Token | C x1 |
freeze, batchFreeze |
Freezing-Modul | C x1 |
deactivate, adminDeactivate (KYC) |
KYC-Modul | C x1 |
pause, destroyBlackFunds |
Token | D x1 |
register, unregister (Einzahlung / Einlösung) |
Verzeichnismodule | D x1 |
registerVerifier, unregisterVerifier |
KYC-Modul | D x1 |
addBlackList, batchBlackList |
Blacklist-Modul | F x1 |
unfreeze |
Freezing-Modul | F x1 |
removeBlackList |
Blacklist-Modul | F x1 + B x1 |
setBlacklistServer, setFreezingServer, setCheckingMode |
Token | E x1 + B x1 |
| Directory-Server- und Supply-Limit-Setter | Token | D x1 + B x1 |
upgradeTo / changeAdmin / setControlAuthority (Token und jedes Modul), unpause |
Token + Module | A x1 + B x1 |
Ausgabe, Einfrieren und KYC-Deaktivierung sind Einzelsignaturen (alle unter Rolle C); Pause, Destroy, Verzeichnisänderungen und Verifier-Registrierung sind Einzelsignaturen unter Rolle D; Blacklisting und Unfreezing sind Einzelsignaturen unter Rolle F. Nur Upgrades und Konfigurationsänderungen benötigen eine zweite Signatur. Beachten Sie die Asymmetrie: freeze und addBlackList benötigen eine Signatur, während removeBlackList zwei benötigt; das Einschränken eines Kontos ist also einfacher als das Freigeben.
Das ist nicht nur eine Lektüre der Matrix; es ist in einem Live-Mint beobachtbar. Die jüngste Ausgabe zum Zeitpunkt des Schreibens, Tx 0xa7e53c…b33d7, ist eine einzelne Transaktion, die vom alleinigen Rolle-C-Konto (0x2f7f00cc5334fe2861e485ff610f74890a0316ed) an den Governance-Vertrag des Tokens gesendet wurde. Sie ruft request auf, und dieselbe Transaktion erreicht das Quorum und emittiert den Mint-Transfer von address(0), ohne separate Genehmigungstransaktion und ohne zweiten Signatar. Da die Ausgabe innerhalb der eigenen Transaktion des Anfragenden abgewickelt wird, hat ein einziger Schlüssel den Mint sowohl angefragt als auch ausgeführt.
Es gibt außerdem keinen Timelock in der Engine. In dem Moment, in dem die letzte erforderliche Signatur eintrifft, wird die Aktion in derselben Transaktion ausgeführt, ohne Verzögerung, in der sie geprüft, storniert oder angefochten werden könnte; die Engine zeichnet einen executedAt-Zeitstempel auf, prüft aber nie einen. Selbst die Zwei-Signatur-Operationen werden also sofort abgewickelt, sobald der zweite Schlüssel signiert.
Ein Schlüsselpaar aktualisiert alles. Das Upgraden des Tokens und aller fünf Compliance-Module verwendet dieselbe Anforderung, A plus B. Da A ein Konto ist und B drei Konten, die sich eine Rolle teilen, können zwei Personen jede Implementierung im System ersetzen.
Dasselbe Paar kontrolliert auch die Rollentabelle. Rollen können nur über das Register (HybridControlledAuthority, 0xa7287701f7ab8f38ef57cdb2a2a69a40128ec89c) in dessen eigener Zeremonie gewährt und entzogen werden; kein externer Schlüssel kann sie direkt ändern. Und seine authorizationMatrix, on-chain gelesen, verlangt für eine Rollenänderung dieselben Signaturen wie für ein Upgrade: Rolle A plus Rolle B. Die zwei Personen, die jede Implementierung ersetzen können, können also auch einen Schlüssel zu Rolle C hinzufügen, einen bestehenden Inhaber entfernen und die gesamte Rollentabelle neu schreiben. Dieses Zwei-Signatur-Gate ist besser als die Einzelsignatur-Operationen oben, aber es ist immer noch eine niedrige Hürde für die Wurzel des Systems, da Rolle A ein einziges Konto ohne Redundanz ist.
Eine Adresse, sechs Rollen. Der Inhaber von Rolle C (0x2f7f00cc5334fe2861e485ff610f74890a0316ed) hält außerdem die benannte ADMIN_TOKEN_HOLDER_ROLE (KYC-Verifier-Verwaltung) und ist Mitglied aller vier Auditor-Rollen (BLACKLIST_AUDITOR_ROLE, FREEZING_AUDITOR_ROLE, WHITELIST_AUDITOR_ROLE und AFL_TOKEN_AUDITOR_HOLDER_ROLE). Die Kompromittierung dieses einen Schlüssels bedeutet gleichzeitig den Verlust von Ausgabe, Einfrieren und KYC-Verwaltung.
Die Auditor-Rollen haben zwei eigene Probleme. Erstens beschränken sie die read-only Getter für die Compliance-Listen, offenbar um zu kontrollieren, wer sie lesen kann, aber in einer öffentlichen Chain ist das sinnlos: Der zugrunde liegende Storage ist für jeden lesbar (das Lesen von Storage-Slots ist die Methode, mit der wir dieses System kartiert haben), sodass die Listen so oder so öffentlich sind, und die Einschränkung zeigt, dass das Design die Tatsache, auf einer öffentlichen Chain zu sein, nicht berücksichtigt hat. Zweitens Konzentration: Alle vier Auditor-Rollen liegen bei denselben 23 Konten, und die Inhaber der Ausführungsrollen A, C, D und F gehören dazu, sodass dieselben Schlüssel, die minten, brennen, einfrieren und blacklisten, auch in der Gruppe sitzen, die diese Aktionen prüfen soll.
Widerruf ist nicht sofort. In der Zeremonien-Engine wird die Rolle eines Signatars einmal geprüft und das Quorum dekrementiert; frühere Signatare werden nie erneut validiert, sodass der spätere Entzug einer Rolle eine bereits gezählte Stimme nicht zurücknimmt:
// 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 }
Schließlich ist der Audit-Trail der Engine selbst unzuverlässig. Bei jeder Genehmigung emittiert das evtApprove-Event address(0) als Signatar statt des echten Genehmigenden; der tatsächliche Signatar bleibt nur im Transaktionssender und einem internen Datensatz erhalten, sodass Event-Logs nicht zuordnen können, wer einen Request genehmigt hat. Und die Liste aktiver Requests verwendet Nonce 0 sowohl als echte Request-ID als auch als Leermarker, sodass Monitoring- oder Genehmigungstools, die die Liste rückwärts durchlaufen, den Request bei Nonce 0 übersehen. Keines davon ist kritisch, aber für ein reguliertes System, das einen sauberen Audit-Trail braucht, mindern beide Punkte ihn.
1.3 Transferkontrollen sind zwischen transfer und transferFrom inkonsistent
Ein Teil des Unterschieds zwischen den beiden ist zu erwarten. Bei transferFrom ist der Initiator, msg.sender, ein genehmigter Spender und nicht die Quelle der Gelder, daher prüft der Code die echte Quelle from in jedem Modus explizit; bei transfer ist das nicht nötig, weil dort msg.sender die Quelle ist. Diese Anpassung ist vernünftig. Zwei andere Unterschiede lassen sich nicht durch den Initiator erklären, und sie führen dazu, dass dieselbe wirtschaftliche Handlung unterschiedlichen Regeln unterliegt.
Erstens werden die Einzahlungs- und Einlösungs-Whitelists nur im transferFrom-Pfad abgefragt, wo eine Mitgliedschaft einen frühen Return auslöst, der die isActive-(KYC-)Prüfung umgeht. transfer fragt sie nie ab. Ob ein Empfänger auf der Whitelist steht, hat nichts damit zu tun, wer die Übertragung initiiert hat; derselbe Empfänger unterliegt also bei transfer der KYC-Prüfung, kann sie aber bei transferFrom überspringen:
// 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" ) ) ;
Zweitens bedeutet checkingMode in den beiden Funktionen unterschiedliche Dinge. Bei transfer prüft der Modus „Source“ den Sender; bei transferFrom prüft der Modus „Source“ den Spender (msg.sender), während from in jedem Modus geprüft wird. Eine einzige Konfigurationseinstellung erzwingt also je nach Einstiegspunkt zwei verschiedene Richtlinien.
Die Wirkung ist, dass die Transferkontrollen eines regulierten Tokens davon abhängen, welche Funktion verwendet wird, was sie schwer durchschaubar und je nach Konfiguration vermeidbar macht.
1.4 Anzeichen eines Vorproduktions-Builds
Über die spezifischen Logikfehler hinaus deuten mehrere Eigenschaften der Codebasis darauf hin, dass ein Vorproduktions-Build ins Mainnet deployed wurde.
Debug-Logging in Produktion. hardhat/console.log-Aufrufe sind überall geblieben, auch im Fallback des Proxys, der bei jeder Nutzertransaktion läuft. Da der Proxy selbst nicht upgradebar ist, ist dieser Overhead dauerhaft:
// 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 }
Ein Proxy-Kopfkommentar, der das Gegenteil des Codes beschreibt. Dieselbe Datei enthält die Dokumentation von OpenZeppelins TransparentUpgradeableProxy, die besagt, dass der Admin niemals zur Implementierung durchfallen kann. Dieser Vertrag tut bewusst das Gegenteil: Die Schutzprüfung in Zeile 117 ist auskommentiert, und der Admin fällt sehr wohl durch. Ein Prüfer, der dem Kommentar vertraut, würde die Vertrauensgrenze falsch modellieren.
Rollenname und Hash passen nicht zusammen. Die „Elevated-Risk“-Rolle wird im Token und in den Modulen mit demselben Konstantennamen, aber einem anderen Keccak-String deklariert, was zwei verschiedene Rollen erzeugt:
// 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') ;
Das Deployment vermeidet Probleme nur, weil jedes Modul seine eigene Kopie hält; ein zweiter Rollenname (AFL_TOKEN_HOLDER_AUDITOR_ROLE) ist auf dieselbe Weise vertauscht.
Weitere Anzeichen. Das Verifikationsbündel des Proxys hat 117 Dateien, einschließlich der Testsuite des Projekts, in den öffentlichen Explorer hochgeladen, was einem Leser die internen Tests und Randfälle ausliefert. Das jüngste Upgrade änderte nur Compiler-Warning-Bereinigungen, ohne ein externes Audit im Prozess. Und der Optimizer ist auf null Runs gesetzt, was die heißen Pfade eines stark genutzten Tokens teurer statt günstiger macht.
Keiner dieser Punkte ist für sich genommen schwerwiegend. Zusammen deuten sie darauf hin, dass der Code nicht die Release-Disziplin durchlaufen hat, die man von einem Vertrag erwartet, der im Mainnet Werte hält.
1.5 Das einzige Upgrade des Vertrags, zurückverfolgt
Der Proxy wurde einmal aktualisiert. Er wurde am 28. April 2026 mit der Implementierung 0x8ff12fe3bed22d9e40afb4b98ef4dee28d94699e deployed und am 10. Juli 2026 auf die aktuelle 0xe42d38b05d7ff702193ac5ccedb411b7298ffbfc aktualisiert. Da ein Upgrade eine On-Chain-Governance-Aktion ist, können wir genau sehen, wer es genehmigt hat.
upgradeTo erfordert Rolle A plus Rolle B. Das Upgrade bestand aus zwei Transaktionen in benachbarten Blöcken, etwa zwölf Sekunden auseinander:
- der Request, Rolle A, von
0xa9315aadc89ba681f1fe5df3375ab1a87d37eca2, im Block 25500519 (Tx0x742372…85136); - die Genehmigung, Rolle B, von
0x3795300b31429f9d37b0dc805528d9390ce87c50, im Block 25500520 (Tx0xa7a400…630c), die das Quorum erreichte und das Upgrade in derselben Transaktion durchführte.
Zwei Dinge fallen auf. Erstens: Die gesamte Zwei-Signatur-Zeremonie wurde innerhalb eines einzigen Blockintervalls abgeschlossen. Vom Request bis zur Ausführung verging ein Block; es gab kein Fenster, in dem der zweite Signatar die Änderung vor ihrem Livegang unabhängig hätte prüfen können.
Zweitens ist keiner der Signatare aus dem aktuellen Rollenregister identifizierbar. Die Rollen wurden seither rotiert: Der Anfragende 0xa9315a… hält Rolle A nicht mehr (er hält heute Rolle E), und der zweite Signatar 0x3795300b… hält jetzt gar keine Rolle mehr. Ein Blick auf das Register in seinem jetzigen Zustand würde nicht verraten, wer das Upgrade autorisiert hat; das tut nur die Transaktionshistorie. Das ist die Eigenschaft „Widerruf ist nicht sofort“, von der anderen Seite betrachtet: Rollen wechseln, sodass eine Momentaufnahme davon, wer was hält, kein Protokoll darüber ist, wer was getan hat.
Und das Upgrade hat keinen der in dieser Überprüfung genannten Defekte behoben. Die von ihm installierte Implementierung, 0xe42d38b0…, ist genau die, die in Teil 1 beschrieben wird. Wir können der Chain nicht entnehmen, ob vor dem Upgrade ein externes Audit durchgeführt wurde; was wir sagen können, ist: Wenn eines durchgeführt wurde, haben die Defekte aus Teil 1 es überlebt.
Teil 2: Entspricht es Hongkongs Stablecoin-Rahmenwerk?
Hongkongs Stablecoins Ordinance trat am 1. August 2025 in Kraft, und lizenzierte Emittenten werden nach der Leitlinie der HKMA zur Beaufsichtigung lizenzierter Stablecoin-Emittenten beaufsichtigt. Wir haben nur die Klauseln verglichen, die ein Smart Contract allein erfüllen kann; Reservehinterlegung, Verwahrung und Off-Chain-Schlüsselzeremonien sind für eine On-Chain-Überprüfung nicht relevant. Zu jeder unten aufgeführten Klausel geben wir an, was die Leitlinie verlangt, was der Vertrag tut und wo die beiden voneinander abweichen. Der Vergleich ist hier zusammengefasst und in den folgenden Abschnitten ausgeführt.
| HKMA-Klausel | Was sie verlangt | Wo der Vertrag abweicht | Bewertung |
|---|---|---|---|
| 6.5.3 | Hochrisiko-Operationen dürfen nicht einseitig sein (Multisignatur und Mitigationsmaßnahmen wie Velocity-Limits oder Timelocks) | Mint, Burn, Pause und Freeze werden jeweils mit einem Schlüssel ausgeführt; kein Timelock (Ausführung ist atomar mit der letzten Signatur) | Weicht ab |
| 6.5.4 | Aufgabenverteilung zwischen autorisierten Personen; Autorität sofort entziehen | Ein Konto hält sechs Rollen, sodass Ausführung und Prüfung sich überschneiden; eine bereits gezählte Genehmigung überlebt einen späteren Widerruf | Weicht ab |
| 6.5.5 | Externes Audit für jede Codeänderung; korrekt, konsistent, frei von Schwachstellen | Die Defekte aus Teil 1 sind in der deployed Implementierung live; isActive und der Anbieter-Widerruf tun nicht, was ihre Namen versprechen |
Weicht ab |
| Wirksamkeit der Compliance-Kontrollen | Blacklist-, Freeze-, Whitelist- und KYC-Kontrollen müssen wirksam sein | KYC-Gating und Anbieter-Widerruf sind im deployed Code funktionslos | Weicht ab |
| 2.2.3 | Eingefrorene oder vernichtete Coins bleiben vollständig gedeckt und abstimmbar | Destroy emittiert Transfer an address(this) statt an address(0) und lässt die Netto-Emissionszähler unberührt; aus Events rekonstruierter Supply driftet |
Bedenklich |
Absatz 6.5.3: Hochrisiko-Operationen dürfen nicht einseitig sein
Was sie verlangt. Hochrisiko-Operationen sollten so gestaltet sein, dass keine einzelne Partei sie einseitig ausführen kann, zum Beispiel durch ein Multisignatur-Protokoll; die Leitlinie nennt weitere Mitigationsmaßnahmen wie Velocity-Limits und zeitverzögerte (Timelock-)Kontrollen.
Was der Vertrag tut. Gelesen aus der authorizationMatrix des Governance-Vertrags (die vollständige Matrix steht in der Tabelle in 1.2), erfordern die Supply- und Notfalloperationen jeweils eine einzelne Rolle mit einem Quorum von eins:
| Operation | Erforderliche Signatur |
|---|---|
mintToDeposit |
ROLE_C x1 |
burnFrom |
ROLE_C x1 |
freeze |
ROLE_C x1 |
pause |
ROLE_D x1 |
destroyBlackFunds |
ROLE_D x1 |
Wo es abweicht. Minten, Brennen, Pausieren und Einfrieren können jeweils mit einem einzigen Schlüssel ausgeführt werden. Das erfüllt die Anforderung „keine einzelne Partei einseitig“ nicht. Es gibt auch keinen Timelock: Wie in 1.2 gezeigt, wird die Aktion, sobald das Quorum erreicht ist, in derselben Transaktion ausgeführt, sodass auch eine Zeitverzögerung, eine der von der Leitlinie genannten Mitigationsmaßnahmen, fehlt. Der Vertrag implementiert zwar ein Supply-Velocity-Limit (whenWithinRiskThresholds), diese Mitigationsmaßnahme ist also vorhanden, aber sie ersetzt keine Multisignatur-Kontrolle über die Operationen selbst.
Absatz 6.5.4: Aufgabenverteilung und sofortiger Widerruf
Was sie verlangt. Verschiedene Operationen sollten auf verschiedene autorisierte Personen aufgeteilt werden, und die Autorität einer autorisierten Person sollte sofort widerrufbar sein.
Was der Vertrag tut. Ein extern besessenes Konto (EOA) hält sechs Rollen (Ausgabe, Einfrieren, KYC-Verwaltung und alle vier Auditor-Rollen), sodass sich Ausführungs- und Prüfrollen überschneiden. Rollenänderungen selbst erfordern ebenfalls nur Rolle A plus Rolle B (siehe 1.2), sodass dieselbe kleine Gruppe entscheidet, wer autorisiert ist, und ausführen und upgraden kann. Und eine bereits gezählte Signatur wird nicht erneut validiert, wenn die Rolle des Signatars später entzogen wird.
Wo es abweicht. Aufgaben sind konzentriert statt getrennt, und der Widerruf ist nicht sofort: Die frühere Genehmigung eines widerrufenen Signatars zählt weiterhin zu einer späteren Ausführung.
Absatz 6.5.5: Jede Codeänderung auditieren; korrekt, konsistent, keine Schwachstellen
Was sie verlangt. Eine qualifizierte dritte Partei sollte die Smart Contracts bei jeder Codeänderung auditieren und bestätigen, dass sie (i) korrekt implementiert, (ii) mit der beabsichtigten Funktionalität konsistent und (iii) mit hohem Vertrauensniveau frei von Schwachstellen sind.
Was der Vertrag tut. Die aktuelle Implementierung ist die durch das Upgrade vom 10. Juli installierte (in 1.5 zurückverfolgt), und die Defekte aus Teil 1 sind darin live.
Wo es abweicht. Wir können off-chain nicht sehen, ob ein Audit durchgeführt wurde, aber das Ergebnis erfüllt den Standard in jedem Fall nicht. Da isActive und der Anbieter-Widerruf nicht tun, was ihre Namen besagen, sind die Bedingungen (i) und (ii) nicht erfüllt; und da die Defekte aus Teil 1 im deployed Code vorhanden sind, ist auch (iii) nicht erfüllt.
Wirksamkeit der Compliance-Kontrollen
Was sie verlangt. Das Lebenszyklusmodell der Leitlinie (Blacklist, Freeze, Whitelist, KYC) setzt voraus, dass diese Kontrollen wirksam sind.
Was der Vertrag tut. Wie in 1.1 gezeigt, sind KYC-Gating und Anbieter-Widerruf im deployed Code nicht funktionsfähig.
Wo es abweicht. Eine Kontrolle, die funktionieren muss, funktioniert nicht. Das ist eine substanzielle Lücke, keine Formalie.
Absatz 2.2.3: Eingefrorene oder vernichtete Coins bleiben vollständig gedeckt und abstimmbar
Was sie verlangt. Stablecoins, die durch Durchsetzungsmaßnahmen eingefroren oder vernichtet werden, sollten vollständig gedeckt bleiben, sodass Supply und Reserven abgestimmt werden können.
Was der Vertrag tut. Der übliche Sanierungsablauf für eine regulierte Stablecoin besteht darin, die Gelder auf einer schwarzen Adresse zu verbrennen und später dem Opfer einen gleich hohen Betrag als separaten Mint erneut auszugeben (so funktionieren USDTs destroyBlackFunds plus issue). HKDAPs Destroy reduziert _totalSupply, was ein Burn ist, aber es schreibt nie balances[address(this)] gut, es emittiert einen Transfer an address(this) statt an address(0), und es lässt die Netto-Emissionszähler, die das Mint-Limit verwendet, unberührt:
// 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 }
Wo es abweicht. Die Zustandsänderung ist ein Burn, aber das Event sagt, die Token seien an den Vertrag übertragen worden, und dort wird nie etwas gehalten (balances[address(this)] bleibt null). Ein Indexer würde address(this) Token gutschreiben, die es nicht hält, und der aus Events rekonstruierte Gesamtsupply würde nicht zur Chain passen. Es verwirrt auch den Sanierungsprozess selbst: Da die Token verbrannt statt geparkt werden, muss eine Neuausgabe an das Opfer ein frischer Mint sein, doch das irreführende Transfer(..., address(this), ...) suggeriert, der Vertrag verwahre sie nun und könnte sie weiterleiten, was er nicht kann. Ein sauberer Burn an address(0) plus eine separate Neuausgabe wäre sowohl korrekt als auch abstimmbar. Wie geschrieben, driftet die On-Chain-Buchhaltung, von der eine Reserveabstimmung abhängt, vom wahren Zustand der Chain ab. Das ist eher bedenklich als einwandfrei.
Wir beschränken diese Ergebnisse auf das, was die Chain zeigt. Ob Reserven vollständig gedeckt sind, ob Schlüssel in einem HSM oder in einer luftabgeschirmten Umgebung liegen und ob Transaktionen vor der Signatur off-chain simuliert werden, ist vom Vertrag nicht sichtbar, und dazu treffen wir keine Aussage.
Fazit
Das Bild ist entlang beider Achsen der Überprüfung konsistent. Als Software enthält HKDAP funktionale Defekte, einschließlich Compliance-Kontrollen, die nicht wie vorgesehen ausgeführt werden, und zeigt mehrere Anzeichen eines Vorproduktions-Builds. Als regulierte Stablecoin stehen mehrere On-Chain-Eigenschaften im Widerspruch zu konkreten Klauseln der HKMA-Leitlinie. Aufgrund der On-Chain-Beweise und abgesehen von den Off-Chain-Dingen, die wir nicht sehen können, erfüllt der deployed Vertrag noch nicht den Standard, den eine kommerzielle Stablecoin erfüllen sollte.
Zwei Beobachtungen schließen sich an, und es lohnt sich, sie unmissverständlich zu formulieren.
Erstens verändert die Ausgabe auf einer öffentlichen Chain den Ort, an dem Compliance entschieden wird. Eine Anforderung wie „keine einzelne Partei sollte einseitig handeln können“ wird durch die Rollenprüfungen im deployed Code erfüllt oder nicht, und dieser Code ist öffentlich. Die Implementierung ist, nicht die Lizenz oder die Dokumentation, der Ort, an dem eine solche Anforderung tatsächlich erfüllt oder verfehlt wird, und jeder kann verifizieren, welches der Fall ist.
Zweitens ändert das „Beta Access“-Label das Risikoprofil des deployed Code nicht. Der Vertrag ist live im Ethereum-Mainnet, wird von echten Schlüsseln verwaltet und stellt eine Forderung auf Hongkong-Dollar dar. Er sollte daher unabhängig von seinem Label an Produktionsstandards gemessen werden.
Ein gemeinsamer roter Faden zieht sich durch die konkreten Ergebnisse. Die Architektur erfindet in maßgeschneiderter Form Primitive neu, die das Ökosystem bereits bereitstellt und in großem Maßstab auditiert hat: eine von Grund auf eigene Genehmigungs-Engine und Rollenschicht, wo ein Safe-Multisig mit OpenZeppelins AccessManager und TimelockController genügen würde; eine handgeschriebene Linked-List-Sammlung anstelle von EnumerableSet; einen modifizierten Proxy anstelle des Standard-TransparentUpgradeableProxy; und einen handgeschriebenen ERC-20 anstelle von OpenZeppelins. Die meisten Defekte in dieser Überprüfung stecken in dieser eigenen Mechanik, nicht in den Teilen, die Standardkomponenten wiederverwenden. Es liest sich wie ein System, das auf die Weise abstrahiert wurde, wie es bei Allzweck-Software üblich ist, statt aus den kleinen, auditierten Bausteinen komponiert zu werden, die die On-Chain-Entwicklung bevorzugt, wo jede Schicht eigener Abstraktion auch Gas, Angriffsfläche und Upgrade-Risiko bedeutet. Ein Design aus diesen Standardkomponenten wäre kleiner, sicherer und leichter zu prüfen und brächte die Dinge mit, die diesem System derzeit fehlen: ein Überlegungsfenster durch einen Timelock und benannte, lesbare Rollen.
Die hier beschriebenen Probleme sind behebbar. Die Wiederherstellung der Multisignatur bei Hochrisiko-Operationen, die Trennung von Ausführung und Audit, die Korrektur der KYC-Widerrufslogik, die Vereinheitlichung der Transferprüfungen, das Entfernen des Debug-Codes und die Forderung nach einem externen Audit vor jedem Upgrade würden die meisten davon lösen. Dass im Mainnet mit verifiziertem Quellcode deployed wurde, hat diese Überprüfung überhaupt erst ermöglicht, und es ist die richtige Standardeinstellung. Kontinuierliche On-Chain-Sicherheits- und Compliance-Überprüfung dieser Art ist das, was wir bei BlockSec tun, und wir helfen gern.



