A BlockSec güvenlik ve uyum incelemesi: HKDAP (Anchorpoint), 13 Ağustos 2026 itibarıyla.
TL;DR. Hong Kong'da ihraç edilen ilk düzenlenmiş stablecoin olan ve Ethereum ana ağında canlı çalışan HKDAP'ın dağıtılmış sözleşmesini inceledik ve üretime hazır değil. KYC ve iptal kontrolleri yazıldığı gibi çalışmıyor; yönetimi, tek bir anahtarın mint, burn veya freeze yapabileceği kadar yoğunlaşmış durumda ve zincir üstü özelliklerinin birçoğu HKMA'nın kendi kılavuzuyla çelişiyor. Altında yatan ortak bir izlek var: sözleşme, ekosistemin hâlihazırda sağladığı ve ölçekte denetlediği ilkel yapıları sıfırdan yeniden icat ediyor (bir multisig, erişim kontrol katmanı, bir timelock, ERC-20'nin kendisi) ve kusurların çoğu standart bileşenleri yeniden kullanan kısımlarda değil, bu özel mekanizmada yaşıyor. "Beta Access" etiketi bu açığı kapatmıyor.
12 Ağustos 2026'da Anchorpoint, bir Hong Kong doları stablecoin'i olan HKDAP'ın ilk aşamasını başlattı. Bu önemli bir lansman. Standard Chartered Bank (Hong Kong) liderliğinde HKT ve Animoca Brands ile kurulan bir ortak girişim olan Anchorpoint, 36 başvuru arasından HKMA'nın verdiği yalnızca iki stablecoin ihraççısı lisansından birine sahip ve HKDAP, Hong Kong'un Stablecoins Yönetmeliği kapsamında ihraç edilen ilk stablecoin'ler arasında.
Düzenlenmiş bir bankanın çoğu ürününden farklı olarak HKDAP doğrudan incelenebilir. Ethereum ana ağında çalışıyor ve sözleşme kaynak kodu Etherscan'de doğrulanmış durumda. Bu yararlı bir özellik: bu şekilde ihraç edilen bir stablecoin için ihraç, transfer ve dondurmayı yöneten kurallar bir belgede anlatılmaz; herkesin okuyabildiği ve yazıldığı gibi yürütülen kodda uygulanır. Lisans bir iddiadır; dağıtılan sözleşme bu iddianın uygulamasıdır ve herkese açıktır.
Bu, somut bir incelemeyi mümkün kılıyor ve biz de bunu yaptık. Dağıtılmış sözleşmeyi iki eksende inceledik. Birincisi, yazılım olarak: doğru ve üretim sınıfı mı? İkincisi, düzenlenmiş bir stablecoin olarak: zincir üstü davranışı HKMA'nın Lisanslı Stablecoin İhraççılarının Denetimine İlişkin Kılavuzu ile örtüşüyor mu?
Bulgular her iki eksende de tutarlı. Sözleşme, yazıldığı gibi çalışmayan uyum kontrolleri de dahil olmak üzere birden çok işlevsel kusur içeriyor. Yönetimi oldukça yoğunlaşmış durumda ve birkaç yüksek riskli işlem tek bir anahtarla yürütülebiliyor. Ayrıca, zincir üstü özelliklerinden bir kısmı HKMA kılavuzunun belirli maddeleriyle çelişiyor. Değerlendirmemiz, bir beta sürümü olarak bile sözleşmenin ticari bir stablecoin'in gerektirdiği kalite çıtasını karşılamadığı yönünde.
Bu yazının geri kalanı, 13 Ağustos 2026 itibarıyla güncel olan bu incelemeyi sunuyor. İncelememiz herkese açık olarak dağıtılmış koda ve gözlemlenebilir zincir üstü gerçeklere dayanıyor. Blok zincirinin belirleyemeyeceği konularda, örneğin rezerv karşılığı veya zincir dışı anahtar saklama hakkında herhangi bir iddiada bulunmuyoruz.
Sözleşmeyi nasıl bulduk
Bir token listesinden değil, ihraççıdan başladık. Anchorpoint'in şirket varlığı, anchorpoint.hk adresindeki sitesine bağlanıyor. Beta Access sayfası dağıtımı adlandırıyor: Ethereum ana ağı, proxy 0x87622385F960fcCB3121d6D0A9513bd1D9Bed6cA; kaynak kodu Etherscan'de doğrulanmış.
Buradan tüm sistemi zincir üstünde haritalandırdık: token proxy'si ve uygulaması (ControllableAHKD, 0xe42d38b05d7ff702193ac5ccedb411b7298ffbfc), onu yöneten yönetim sözleşmesi (0x47dd47a776902bee03abdd5aeff43f41b0ffbc2b), üst düzey rol kayıt defteri (0xa7287701f7ab8f38ef57cdb2a2a69a40128ec89c) ve her biri kendi yönetim sözleşmesine sahip beş uyum modülü. Aşağıdaki her ilişki, yalnızca kaynak koddan çıkarım yapılarak değil, ana ağda depolama slotları okunarak ve view fonksiyonları çağrılarak doğrulandı.
Sözleşme neye benziyor

HKDAP token'ı, OpenZeppelin'in standart ERC-20 referans uygulamasını kullanmıyor; ERC-20 mantığı sıfırdan yazılmış (aşağıdaki hataların bir kısmı buradan geliyor). Sistemin üç katmanı var:
- Token. Yükseltilebilir bir proxy arkasındaki
ControllableAHKD. Mint, burn, pause ve zorla imha işlevlerine sahip, ayrıca her transferde uyum kontrolleri yapılan kontrollü bir ERC-20. - Özel yapım bir M-of-N yönetim motoru. Her ayrıcalıklı işlem (yükseltme, mint, burn, pause, blacklist, freeze, uyum modülü değiştirme) düz bir multisig yerine bir request, approve, execute töreninden geçer.
- Beş uyum modülü. Blacklist, dondurma, bir KYC aktivasyon hizmeti ve depozito ile itfa beyaz listeleri. Her modül, kendi kontrol-otoritesi sözleşmesi tarafından yönetilen bir proxydir.
Yapısal olarak, aynı "kontrol otoritesi artı proxy artı uygulama" birimi altı kez tekrarlanıyor (token artı beş modül) ve altı kontrol otoritesinin tamamı rollerini tek bir kayıt defterinde çözümlüyor. Sistemdeki tüm kontrol nihayetinde bu tek kayıt defterinde toplanıyor ve içindeki rolleri kimin yeniden yazabileceği, aşağıda ayrıntılı olarak gösterdiğimiz gibi, çok küçük bir imzacı anahtar kümesine indirgeniyor.
Bölüm 1: Güvenlik ve hatalar
Bu bölümdeki bulgular aşağıda özetlenmiştir; her satır belirtilen bölümde ayrıntılandırılmıştır.
| Alan | Bulgu | Nerede | Etki |
|---|---|---|---|
| 1.1 KYC ve iptal kontrolleri başarısız oluyor | KYC iptali ölü kod | TokenHolderActivationServerLibrary.sol:221-235 |
isActive, sağlayıcı kaydının kaldırılmasını yok sayar (döngü asla çalışmaz ve = yerine == kullanır); fail-open. |
| Doğrulayıcı kaydının kaldırılması asla INACTIVE ayarlamaz | TokenHolderActivationServer.sol:504-511 |
"Kaydı kaldırılmış" bir doğrulayıcı yine de cüzdanları dahil edip devre dışı bırakabilir; ikinci çağrı revert olur. | |
| KYC kanıtı zincir üstünde asla doğrulanmaz | TokenHolderActivationServer.sol:575-578 |
Kanıt iletilir ancak atılır; boş dize dahil herhangi bir kanıt kabul edilir. | |
| Ücretsiz transfer muafiyeti parçalara bölünerek aşılabilir | ControllableAHKD.sol:337-535 |
freeTransferLimit kümülatif olarak değil, çağrı başına kontrol edilir; KYC'siz etkinlik için bir sınır olarak kullanılırsa, transferleri alt sınıra bölerek bypass edilebilir. |
|
| 1.2 Yönetim aşırı yoğunlaşmış | Yüksek riskli işlemler tek imzalı | authorizationMatrix; mint tx 0xa7e53c…b33d7 |
mint / burn / freeze / KYC-deactivate (rol C), pause / destroy (rol D), blacklist / unfreeze (rol F) her biri tek anahtarla yürütülür. |
| Bir anahtar çifti (A + B) her şeyi yükseltir | authorizationMatrix |
Token ve beş modülün tamamı için upgradeTo, rol A + B gerektirir; iki kişi herhangi bir uygulamayı değiştirebilir. |
|
| Aynı çift her rolü yeniden yazar | kayıt defteri 0xa728… authorizationMatrix |
Kayıt defterindeki grantRole / revokeRole da rol A + B gerektirir (ADMIN_ROLE yalnızca sözleşmede tutulur; DEFAULT_ADMIN_ROLE = address(0)); hiçbir dış anahtar rolleri doğrudan değiştiremez. |
|
| Bir adres altı rol tutuyor | rol kayıt defteri 0xa728… |
0x2f7f00…, rol C'nin yanı sıra ADMIN_TOKEN_HOLDER ve dört denetçi rolünün tamamını tutar; yürütme ve denetim çakışır. |
|
| İptal anında gerçekleşmez; timelock yok | HybridControlEngine.sol:158-227 |
Sayılmış bir imza, bir rol iptal edildikten sonra yeniden doğrulanmaz; yürütme, son imzayla atomiktir ve gecikmesizdir. | |
| Motorun denetim izi güvenilmez | HybridControlEngine.sol:36-129, 177-196 |
evtApprove, imzacı olarak address(0) yayar; etkin-istek listesi nonce 0'ı hem gerçek bir kimlik hem de boş işaretçi olarak kullanır, bu nedenle geriye doğru gezinme ilk isteği kaçırır. |
|
| 1.3 Tutarsız transfer kontrolleri | transfer ve transferFrom farklı kurallar uygular |
ControllableAHKD.sol:313-502 |
Beyaz liste erken dönüşü KYC'yi yalnızca transferFrom ile atlar; checkingMode farklı tarafları kontrol eder; aynı transfer farklı kurallara tabidir. |
| 1.4 Üretim öncesi yapıya işaretler | hata ayıklama günlükleri, yanlış yorum, ad/karma uyuşmazlığı, testlerin yüklenmesi | UpgradeableProxy.sol:115-119; ControllableAHKD.sol:25 |
Proxy fallback'inde console.log (kalıcı gas); proxy yorumu kodla çelişiyor; rol adı/karma uyuşmazlığı; testler dahil 117 dosya yüklenmiş; optimizer runs = 0. |
| 1.5 Tek yükseltme kusurları yerinde bıraktı | tek yükseltmenin izini sürme | tx 0x742372…85136, 0xa7a400…630c |
Dağıtım 28 Nis → yükseltme 10 Tem; A + B töreninin iki işlemi bir blok aralıklaydı (~12sn); Bölüm 1'deki her kusur kurulu uygulamada mevcut. |
1.1 KYC ve iptal kontrolleri yazıldığı gibi çalışmıyor
Düzenlemeye göre, HKDAP'ın hizmet verdiği B-tarafı (kurumsal) kullanıcıların KYC'den geçmesi gerekiyor. Sözleşmede bu, en az iki şey yapması anlamına gelir: KYC'den geçmemiş bir cüzdanın engellenmesi için transferlerde KYC'yi zorunlu kılmak ve bir kimlik sağlayıcısı veya bir kullanıcı kaldırıldığında erişimi iptal etmek. HKDAP'ta bu yol üç bağımsız noktada bozuk.
KYC iptali ölü kod. Token, transferleri KYC modülünde isActive(address) çağrısıyla kapılandırır. isActive iki şey yapmalıdır: önce, cüzdan başına sayaçtan bir taban değer belirler; cüzdan, KYC ile devre dışı bırakıldığından daha fazla kez KYC ile etkinleştirilmişse etkindir; ardından, cüzdan için kefil olan herhangi bir kimlik sağlayıcısı o zamandan beri kayıttan kaldırılmışsa bu değeri false'a daraltır. İkisi iki tür iptali kapsar: bir kullanıcının KYC'sini iptal etmek (sayaç) ve bütün bir sağlayıcıyı iptal ederek onun eklediği her cüzdanı da birlikte düşürmek (döngü). Yalnızca ilki gerçekleşir.
// 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 }
Satır 221 gerçek bir atamadır (=) ve etkili olan tek atama budur: active, deactivationCount < activationCount değerine dönüşür. Döngü (satır 226-235) bu değeri daraltması gereken yerdir, ancak iki kez başarısız olur. Satır 226'daki koşulu entryCount > chainedItemList.entryCount, 0 > N olduğundan gövde asla çalışmaz; ve çalışsaydı bile, satır 231 sonucu atılan bir karşılaştırma olan == kullanır, oysa = ile atama yapmalıdır. isActive bu nedenle yalnızca sayaç karşılaştırmasını döndürür ve sağlayıcı iptalini tamamen yok sayar. Bu fail-open'dur: ele geçirilmiş bir KYC sağlayıcısının kaydını kaldırmak, onun eklediği cüzdanların işlem yapmasını engellemez. Ayrıca unregisterVerifier bu sayaçlara dokunmadığı için, kaydı kaldırılmış bir sağlayıcının cüzdanları pozitif sayıyı korur ve etkin kalır.
Sağlayıcı iptali diğer tarafta da bozuk. unregisterVerifier yalnızca bağlı listedeki bir düğümü taşır; sağlayıcının dizin durumunu asla INACTIVE yapmaz:
// 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 }
Durum ACTIVE kaldığı için, "kaydı kaldırılmış" bir doğrulayıcı yine de kullanıcıları kaydedip devre dışı bırakabilir; bir sağlayıcıyı yeniden etkinleştirmeyi amaçlayan dal erişilemezdir ve ikinci bir unregisterVerifier çağrısı underflow olup revert eder.
KYC kanıtı zincir üstünde asla doğrulanmaz. Yetkili bir doğrulayıcı, registerOrRenew aracılığıyla bir cüzdanı kaydettiğinde veya yenilediğinde (yalnızca o anda etkin bir doğrulayıcının çağırabilmesi için kapılandırılmıştır), akış _checkKYCProofa ulaşır; bu fonksiyon, sunulan kanıtı sağlayıcının şemasına göre doğrulamalıdır. Arayüze göre bu kanıt, "bir Oracle için URI veya doğrulanabilir bir kaynaktan imzalı bir hash"tir. Fonksiyon onu yok sayar:
// 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 }
Kanıt bu fonksiyona ulaşır. registerOrRenew, gönderilen kycProofu _registerOrRenew üzerinden taşır ve ikinci argüman olarak iletir. Ancak satır 575'te bu argüman hiç adı olmayan bir parametreye düşer; fonksiyonun üzerindeki doc yorumu da bunu atlar ve gövde onu asla okumaz. Fonksiyon yalnızca sağlayıcının etkin olduğunu doğrular ve satır 577'de true döndürür; böylece kanıt kontrol edilmek yerine atılır. Bu bir dışarıdan bypass değildir, çünkü oraya yalnızca etkin bir doğrulayıcı ulaşabilir; ancak zincir üstünde sözleşme KYC kanıtı üzerinde hiçbir doğrulama yapmaz, bu nedenle bütünlüğü tamamen zincir dışı doğrulayıcıya dayanır. Ele geçirilmiş veya dikkatsiz bir doğrulayıcı, boş dize dahil herhangi bir kanıtla herhangi bir cüzdanı etkinleştirebilir.
Birlikte ele alındığında bu üçü, temel bir uyum özelliği olan erişimi denetleme ve iptal etme yeteneğinin yazıldığı gibi çalışmadığı anlamına geliyor.
Bu üçünün ötesinde ilişkili bir zayıflık daha var: ücretsiz transfer muafiyeti parçalara bölünerek aşılabilir. Token'ın bir freeTransferLimit değeri var ve bunun altındaki bir transfer isActive (KYC) kontrolünü atlar. Ancak limit yalnızca mevcut transferin tutarıyla karşılaştırılır; sözleşme adres veya dönem başına kümülatif bir toplam tutmaz. Dolayısıyla limit, KYC'siz etkinlik için bir tavan olarak kullanılıyorsa, bir kullanıcı transferleri her biri limitin hemen altında olacak şekilde tekrar ederek toplamda istediği tutarı taşıyabilir; bu da tavanı etkisiz kılar.
1.2 Yönetim aşırı yoğunlaşmış ve yüksek riskli işlemler tek imzalı
Zincir üstünde her rolü ve her rol sahibini numaralandırdık. Ayrıntılardan önce iki şey öne çıkıyor.
Birincisi, M-of-N törenlerini yetkilendiren rollerin okunabilir adları yok. Dağıtılmış yapılandırmada yalnızca 32 baytlık hash'ler olarak görünüyorlar ve hiçbiri doğrulanmış kaynakta adlandırılmış bir rol sabitiyle eşleşmiyor (SUPPLY_CONTROLLER_ROLE gibi adlandırılmış roller, imzacılar tarafından değil, sözleşmelerin kendisi tarafından tutuluyor). Altı imzacı rolünü A'dan F'ye etiketliyoruz. Sistemdeki en güçlü rollerin opak tanımlayıcılar olması başlı başına bir zayıflıktır: yönetimi, adlandırılmış rollere kıyasla daha zor incelenebilir kılar.
İkincisi, sahip sayısı azdır. Aşağıdaki tablo zincir üstünde rol kayıt defterinden okunmuştur; adresler kısaltılmıştır.
| Rol (bizim etiketimiz) | Zincir üstü hash | Sahip(ler) | Yetki verdiği |
|---|---|---|---|
| A | 0x37f0f656… |
0x747356d525bf47f495af7c6e42f3ab9b8ba87a4b |
upgradeTo / changeAdmin / setControlAuthority için ilk imza |
| B | 0x95c27f81… |
0x8117a657df7a1c399231bfe26a096eb8f8ad5973, 0x5d9d9b28e83382e694916b0feeaf7a9badbb659c, 0xebb73f78e253539acceb4e6cc287095788eee5d4 |
neredeyse her iki imzalı işlemde ikinci imza |
| C | 0xfa2fe896… |
0x2f7f00cc5334fe2861e485ff610f74890a0316ed |
mintToDeposit, burnFrom, freeze, KYC deactivate (tek) |
| D | 0x3dce3265… |
0x5092af62a1625fa57404557d3ad417474f3f494c |
pause, destroyBlackFunds, depozito ve itfa adreslerini kaydet/kaydını kaldır, registerVerifier (tek) |
| E | 0xfd21a76d… |
0xa9315aadc89ba681f1fe5df3375ab1a87d37eca2 |
uyum modüllerini değiştirme (setBlacklistServer vb.) |
| F | 0x510ac1ff… |
0x7daabe5092d3feea5cc19ddbb5380a4eac38f89c |
addBlackList, unfreeze (tek) |
Tablodan birkaç sonuç çıkıyor.
Tek imzacılı yüksek riskli işlemler. Yüksek riskli işlemlerin çoğu, kotası bir olan tek bir rol gerektirir. Aşağıdaki tablo, token'ın yönetim sözleşmesinin ve beş modül yönetim sözleşmesinin authorizationMatrix değerinden zincir üstünde okunmuştur (bir + B ikinci imzası gösterilmedikçe tek imza):
| İşlem | Tarafından yönetilir | Gerekli imzalar |
|---|---|---|
mintToDeposit, burnFrom |
token | C x1 |
freeze, batchFreeze |
dondurma modülü | C x1 |
deactivate, adminDeactivate (KYC) |
KYC modülü | C x1 |
pause, destroyBlackFunds |
token | D x1 |
register, unregister (depozito / itfa) |
dizin modülleri | D x1 |
registerVerifier, unregisterVerifier |
KYC modülü | D x1 |
addBlackList, batchBlackList |
blacklist modülü | F x1 |
unfreeze |
dondurma modülü | F x1 |
removeBlackList |
blacklist modülü | F x1 + B x1 |
setBlacklistServer, setFreezingServer, setCheckingMode |
token | E x1 + B x1 |
| dizin sunucusu ve arz limiti ayarlayıcıları | token | D x1 + B x1 |
upgradeTo / changeAdmin / setControlAuthority (token ve her modül), unpause |
token + modüller | A x1 + B x1 |
İhraç, dondurma ve KYC devre dışı bırakma tek imzalıdır (hepsi rol C altında); pause, destroy, dizin değişiklikleri ve doğrulayıcı kaydı, rol D altında tek imzalıdır; blacklist ve unfreeze, rol F altında tek imzalıdır. Yalnızca yükseltmeler ve yapılandırma değişiklikleri ikinci bir imza gerektirir. Asimetriye dikkat edin: freeze ve addBlackList tek imza gerektirirken removeBlackList iki imza gerektirir; yani bir hesabı kısıtlamak, serbest bırakmaktan daha kolaydır.
Bu yalnızca matrisin okunması değil; canlı bir mint işleminde gözlemlenebilir. Bu yazı hazırlanırken en son ihraç olan tx 0xa7e53c…b33d7, tek rol-C hesabı (0x2f7f00cc5334fe2861e485ff610f74890a0316ed) tarafından token'ın yönetim sözleşmesine gönderilen tek bir işlemdir. request çağrısı yapar ve aynı işlem yeter sayıya ulaşıp address(0)'dan mint Transfer olayını yayar; ayrı bir onay işlemi ve ikinci bir imzacı yoktur. İhraç, talep edenin kendi işlemi içinde kesinleştiği için, tek anahtar mint'i hem talep etmiş hem de yürütmüştür.
Ayrıca motorda hiçbir yerde timelock yoktur. Gereken son imza düştüğü anda işlem aynı işlem içinde yürütülür; incelenebileceği, iptal edilebileceği veya itiraz edilebileceği bir gecikme yoktur. Motor bir executedAt zaman damgası kaydeder ancak asla kontrol etmez. Bu nedenle iki imzalı işlemler bile ikinci anahtar imzaladığı anda anında kesinleşir.
Bir anahtar çifti her şeyi yükseltir. Token'ı yükseltmek ve beş uyum modülünün tamamını yükseltmek aynı gereksinimi kullanır: A artı B. A tek bir hesap ve B aynı rolü paylaşan üç hesap olduğundan, iki kişi sistemdeki herhangi bir uygulamayı değiştirebilir.
Aynı çift rol tablosunu da kontrol eder. Roller yalnızca kayıt defterinde (HybridControlledAuthority, 0xa7287701f7ab8f38ef57cdb2a2a69a40128ec89c) kendi töreni aracılığıyla verilebilir ve iptal edilebilir; hiçbir dış anahtar bunları doğrudan değiştiremez. Zincir üstünde okunan authorizationMatrix de bir rol değişikliği için yükseltmeyle aynı imzaları gerektirir: rol A artı rol B. Dolayısıyla herhangi bir uygulamayı değiştirebilen iki kişi, rol C'ye bir anahtar ekleyebilir, mevcut bir sahibi kaldırabilir ve tüm rol tablosunu yeniden yazabilir. Bu iki imzalı kapı, yukarıdaki tek imzalı işlemlerden daha iyidir, ancak sistemin kökü için hâlâ düşük bir çıtadır; çünkü rol A, artıklığı olmayan tek bir hesaptır.
Bir adres, altı rol. Rol C'nin sahibi (0x2f7f00cc5334fe2861e485ff610f74890a0316ed) ayrıca ADMIN_TOKEN_HOLDER_ROLE (KYC doğrulayıcı yönetimi) rolünü elinde tutar ve dört denetçi rolünün tamamının üyesidir (BLACKLIST_AUDITOR_ROLE, FREEZING_AUDITOR_ROLE, WHITELIST_AUDITOR_ROLE ve AFL_TOKEN_AUDITOR_HOLDER_ROLE). Bu tek anahtarın ele geçirilmesi, ihraç, dondurma ve KYC yönetiminin aynı anda kaybı anlamına gelir.
Denetçi rollerinin kendine özgü iki sorunu var. Birincisi, uyum listelerinin salt okunur getter'larını kapılayarak görünüşte bunları kimin okuyabileceğini kontrol ediyorlar; ancak halka açık bir blok zincirinde bu anlamsızdır: temeldeki depolama herkes tarafından okunabilir (depolama slotlarını okumak bu sistemi haritalandırma şeklimizdi), bu nedenle listeler her iki durumda da halka açıktır ve bu kısıtlama tasarımın halka açık bir zincirde olmayı hesaba katmadığını gösterir. İkincisi, yoğunlaşma: dört denetçi rolünün tamamı aynı 23 hesapta toplanmıştır ve yürütme rolleri A, C, D ve F'nin sahipleri de bunların arasındadır; yani mint, burn, freeze ve blacklist yapan aynı anahtarlar, bu eylemleri incelemesi gereken grupta da yer alır.
İptal anında değildir. Tören motorunda, bir imzacının rolü bir kez kontrol edilir ve kota azaltılır; daha önceki imzacılar asla yeniden doğrulanmaz; bu nedenle sonradan bir rolü iptal etmek, zaten sayılmış bir oyu geri çekmez:
// 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 }
Son olarak, motorun kendi denetim izi güvenilmez. Her onayda evtApprove olayı, gerçek onaylayan yerine imzacı olarak address(0) yayar; gerçek imzacı yalnızca işlemi gönderen adreste ve dahili bir kayıtta hayatta kalır; bu nedenle olay günlükleri bir isteği kimin onayladığını ilişkilendiremez. Ayrıca etkin-istek listesi nonce 0'ı hem gerçek bir istek kimliği hem de boş işaretçi olarak kullanır; bu nedenle listeyi geriye doğru tarayan izleme veya onay araçları nonce 0'daki isteği kaçırır. Bunların hiçbiri kritik değildir, ancak temiz bir denetim izi gerektiren düzenlenmiş bir sistem için ikisi de ondan eksiltir.
1.3 transfer ve transferFrom arasında transfer kontrolleri tutarsız
İkisi arasındaki farkın bir kısmı beklenendir. transferFrom'da başlatan, msg.sender, fonların kaynağı değil onaylanmış bir harcayıcıdır; bu nedenle kod her modda gerçek kaynağı from'u açıkça kontrol eder. transfer'in buna ihtiyacı yoktur, çünkü orada msg.sender kaynağın kendisidir. Bu uyarlama makuldür. Diğer iki fark, başlatan tarafından açıklanmaz ve aynı ekonomik eylemi farklı kurallara tabi kılar.
Birincisi, depozito ve itfa beyaz listeleri yalnızca transferFrom yolunda kontrol edilir; üyelik, isActive (KYC) kontrolünü atlayan erken bir dönüşü tetikler. transfer onlara asla danışmaz. Bir alıcının beyaz listede olup olmamasının, transferi kimin başlattığıyla hiçbir ilgisi yoktur; bu nedenle aynı alıcı transfer ile KYC'ye tabidir ancak transferFrom ile bunu atlayabilir:
// 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" ) ) ;
İkincisi, checkingMode iki fonksiyonda farklı anlamlara gelir. transfer'de "Source" modu göndereni kontrol eder; transferFrom'da "Source" modu harcayıcıyı (msg.sender) kontrol ederken, from her modda kontrol edilir. Yani tek bir yapılandırma ayarı, giriş noktasına bağlı olarak iki farklı politika uygular.
Sonuç olarak, düzenlenmiş bir token'ın transfer kontrolleri hangi fonksiyonun kullanıldığına bağlıdır; bu da onları akıl yürütmeyi zorlaştırır ve yapılandırmaya bağlı olarak kaçınılabilir kılar.
1.4 Üretim öncesi yapıya işaretler
Belirli mantık hatalarının ötesinde, kod tabanının birkaç özelliği ana ağa üretim öncesi bir yapının dağıtıldığını gösteriyor.
Üretimde hata ayıklama günlüğü. hardhat/console.log çağrıları her yerde duruyor; her kullanıcı işleminde çalışan proxy fallback'inin içi dahil. Proxy'nin kendisi yükseltilebilir olmadığı için bu ek yük kalıcıdır:
// 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 }
Kodun tersini anlatan bir proxy başlık yorumu. Aynı dosya, OpenZeppelin'in TransparentUpgradeableProxy belgelerini taşıyor; bu belgeler admin'in asla uygulamaya düşemeyeceğini söyler. Bu sözleşme bilinçli olarak tersini yapar: satır 117'deki koruma yorum satırı yapılmıştır ve admin gerçekten de uygulamaya düşer. Yoruma güvenen bir inceleyici, güven sınırını yanlış modeller.
Rol adları karma değerleriyle eşleşmiyor. "Elevated risk" rolü, token ve modüllerde aynı sabit adla ancak farklı bir keccak dizesiyle tanımlanmış; bu da iki farklı rol üretir:
// 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') ;
Dağıtım, yalnızca her modül kendi kopyasını tuttuğu için sorundan kaçınır; ikinci bir rol adı (AFL_TOKEN_HOLDER_AUDITOR_ROLE) aynı şekilde yer değiştirmiştir.
Diğer işaretler. Proxy'nin doğrulama paketi, projenin test paketi dahil 117 dosyayı herkese açık gezgine yükledi; bu da okuyucuya dahili testleri ve uç durumları verir. En son yükseltme, döngüde üçüncü taraf denetimi olmadan yalnızca derleyici uyarısı temizliğini değiştirdi. Ayrıca optimizer sıfır çalıştırma olarak ayarlanmış; bu da yoğun kullanılan bir token'ın sık kullanılan kod yollarını daha ucuz değil daha pahalı hale getirir.
Bunların hiçbiri tek başına ciddi değildir. Birlikte, kodun ana ağda değer tutan bir sözleşmeden beklenen yayın disiplininden geçmediğini gösteriyorlar.
1.5 Sözleşmenin tek yükseltmesinin izini sürmek
Proxy bir kez yükseltildi. 28 Nisan 2026'da 0x8ff12fe3bed22d9e40afb4b98ef4dee28d94699e uygulamasıyla dağıtıldı ve 10 Temmuz 2026'da mevcut 0xe42d38b05d7ff702193ac5ccedb411b7298ffbfc sürümüne yükseltildi. Yükseltme zincir üstü bir yönetim eylemi olduğu için, onu tam olarak kimin onayladığını görebiliriz.
upgradeTo, rol A artı rol B gerektirir. Yükseltme, bitişik bloklarda yaklaşık on iki saniye arayla yapılan iki işlemdi:
- istek, rol A,
0xa9315aadc89ba681f1fe5df3375ab1a87d37eca2adresinden, 25500519 bloğunda (tx0x742372…85136); - onay, rol B,
0x3795300b31429f9d37b0dc805528d9390ce87c50adresinden, 25500520 bloğunda (tx0xa7a400…630c); bu işlem yeter sayıya ulaştı ve yükseltmeyi aynı işlem içinde gerçekleştirdi.
İki şey öne çıkıyor. Birincisi, iki imzalı törenin tamamı tek bir blok aralığında tamamlandı. İstekten yürütmeye kadar bir blok vardı; değişiklik canlıya geçmeden önce ikinci imzacının bağımsız olarak inceleyebileceği bir pencere yoktu.
İkincisi, imzacıların hiçbiri mevcut rol kayıt defterinden tanımlanamıyor. Roller o zamandan beri döndürülmüş: istek sahibi 0xa9315a… artık rol A'yı tutmuyor (bugün rol E'yi tutuyor) ve ikinci imzacı 0x3795300b… şu anda hiçbir role sahip değil. Kayıt defterini olduğu gibi okumak size yükseltmeyi kimin yetkilendirdiğini söylemez; bunu yalnızca işlem geçmişi söyler. Bu, "iptalin anında olmaması" özelliğinin diğer taraftan görünümüdür: roller hareket eder, bu nedenle kimin neye sahip olduğunun anlık görüntüsü, kimin ne yaptığının kaydı değildir.
Ayrıca yükseltme, bu incelemedeki kusurların hiçbirini düzeltmedi. Kurduğu uygulama 0xe42d38b0…, Bölüm 1'in tamamının tanımladığı uygulamadır. Zincirden, yükseltmeden önce üçüncü taraf denetimi yapılıp yapılmadığını söyleyemeyiz; diyebileceğimiz şey, yapıldıysa Bölüm 1'deki kusurların ondan sağ çıktığıdır.
Bölüm 2: Hong Kong'un stablecoin çerçevesiyle uyumlu mu?
Hong Kong'un Stablecoins Yönetmeliği 1 Ağustos 2025'te yürürlüğe girdi ve lisanslı ihraççılar, HKMA'nın Lisanslı Stablecoin İhraççılarının Denetimine İlişkin Kılavuzu uyarınca denetleniyor. Yalnızca bir akıllı sözleşmenin kendi başına karşılayabileceği maddeleri karşılaştırdık; rezerv karşılığı, saklama ve zincir dışı anahtar törenleri zincir üstü bir incelemenin kapsamı dışındadır. Aşağıdaki her madde için kılavuzun ne gerektirdiğini, sözleşmenin ne yaptığını ve ikisinin nerede ayrıştığını belirtiyoruz. Karşılaştırma burada özetlenmiş ve takip eden bölümlerde ayrıntılandırılmıştır.
| HKMA maddesi | Gerektirdiği | Sözleşmenin ayrıştığı nokta | Değerlendirme |
|---|---|---|---|
| 6.5.3 | Yüksek riskli işlemler tek taraflı olmamalı (çoklu imza ve hız limitleri veya timelock gibi azaltıcı önlemler) | mint, burn, pause ve freeze işlemlerinin her biri tek anahtarla yürütülür; timelock yoktur (yürütme son imzayla atomiktir) | Ayrışıyor |
| 6.5.4 | Görevleri yetkili kişiler arasında ayırın; yetkiyi derhal iptal edin | bir hesap altı rol tutuyor, bu nedenle yürütme ve denetim çakışıyor; sayılmış bir onay daha sonraki bir iptalden sağ çıkar | Ayrışıyor |
| 6.5.5 | Her kod değişikliği için üçüncü taraf denetimi; doğru, tutarlı, güvenlik açığı içermeyen | Bölüm 1'deki kusurlar dağıtılmış uygulamada canlıdır; isActive ve sağlayıcı iptali adlarının yaptığını yapmıyor |
Ayrışıyor |
| Uyum kontrollerinin etkinliği | blacklist, freeze, whitelist ve KYC kontrolleri etkili olmalı | KYC denetimi ve sağlayıcı iptali dağıtılmış kodda işlevsel değil | Ayrışıyor |
| 2.2.3 | Dondurulmuş veya imha edilmiş coinler tamamen karşılıklı ve mutabakat edilebilir kalmalı | destroy, address(0) yerine address(this)e Transfer yayar ve net ihraç sayaçlarına dokunmaz; olaylardan yeniden oluşturulan arz sapar |
Endişe |
Paragraf 6.5.3: yüksek riskli işlemler tek taraflı olmamalı
Gerektirdiği. Yüksek riskli işlemler, hiçbir tek tarafın bunları tek taraflı olarak gerçekleştiremeyeceği şekilde tasarlanmalıdır; örneğin çoklu imza protokolüyle ve kılavuz, hız limitleri ve zaman gecikmeli (timelock) kontroller gibi ek azaltıcı önlemler sıralar.
Sözleşmenin yaptığı. Yönetim sözleşmesinin authorizationMatrix değerinden okunduğunda (tam matris 1.2'deki tablodadır), arz ve acil durum işlemlerinin her biri, kotası bir olan tek bir rol gerektirir:
| İşlem | Gerekli imza |
|---|---|
mintToDeposit |
ROLE_C x1 |
burnFrom |
ROLE_C x1 |
freeze |
ROLE_C x1 |
pause |
ROLE_D x1 |
destroyBlackFunds |
ROLE_D x1 |
Ayrıştığı nokta. Mint, burn, pause ve freeze işlemlerinin her biri tek bir anahtarla yürütülebilir. Bu, "hiçbir tek taraf tek taraflı olarak" gereksinimini karşılamaz. Ayrıca bir timelock yoktur: 1.2'de gösterildiği gibi, yeter sayıya ulaşıldığında işlem aynı işlem içinde yürütülür; bu nedenle kılavuzun saydığı azaltıcı önlemlerden biri olan zaman gecikmesi de yoktur. Sözleşme bir arz hız limiti (whenWithinRiskThresholds) uygular, bu nedenle bu azaltıcı önlem mevcuttur; ancak işlemlerin kendisinde çoklu imza kontrolünün yerini tutmaz.
Paragraf 6.5.4: görev ayrılığı ve derhal iptal
Gerektirdiği. Farklı işlemler, farklı yetkili kişiler arasında ayrıştırılmalı ve bir yetkili kişinin yetkisi derhal iptal edilebilmelidir.
Sözleşmenin yaptığı. Bir harici sahipli hesap altı rol tutuyor (ihraç, dondurma, KYC yönetimi ve dört denetçi rolünün tamamı), bu nedenle yürütme ve denetim rolleri çakışıyor. Rol değişiklikleri de yalnızca rol A artı rol B gerektirir (bkz. 1.2); bu nedenle kimin yetkili olduğuna aynı küçük grup karar verir ve bu grup yürütme ve yükseltme yapabilir. Ayrıca, imzacının rolü daha sonra iptal edilirse zaten sayılmış bir imza yeniden doğrulanmaz.
Ayrıştığı nokta. Görevler ayrıştırılmak yerine yoğunlaşmıştır ve iptal anında değildir: iptal edilmiş bir imzacının daha önceki onayı, daha sonraki bir yürütme için hâlâ sayılır.
Paragraf 6.5.5: her kod değişikliğini denetleyin; doğru, tutarlı, güvenlik açığı içermeyen
Gerektirdiği. Nitelikli bir üçüncü taraf, her kod değişikliği için akıllı sözleşmeleri denetlemeli ve bunların (i) doğru uygulandığını, (ii) amaçlanan işlevsellikle tutarlı olduğunu ve (iii) yüksek düzeyde güvenle güvenlik açıklarından arınmış olduğunu doğrulamalıdır.
Sözleşmenin yaptığı. Mevcut uygulama, 10 Temmuz yükseltmesiyle kurulan uygulamadır (1.5'te izi sürüldü) ve Bölüm 1'deki kusurlar onun içinde canlıdır.
Ayrıştığı nokta. Zincir dışında bir denetim yapılıp yapılmadığını göremiyoruz, ancak sonuç her iki durumda da standardı karşılamıyor. isActive ve sağlayıcı iptali adlarının yaptığını yapmadığı için (i) ve (ii) koşulları karşılanmıyor; Bölüm 1'deki kusurlar dağıtılmış kodda mevcut olduğundan (iii) de karşılanmıyor.
Uyum kontrollerinin etkinliği
Gerektirdiği. Kılavuzun yaşam döngüsü modeli (blacklist, freeze, whitelist, KYC) bu kontrollerin etkili olduğunu varsayar.
Sözleşmenin yaptığı. 1.1'de gösterildiği gibi, KYC denetimi ve sağlayıcı iptali dağıtılmış kodda işlevsel değildir.
Ayrıştığı nokta. Çalışması gereken bir kontrol çalışmıyor. Bu, biçimsel değil, önemli bir boşluktur.
Paragraf 2.2.3: dondurulmuş veya imha edilmiş coinler tamamen karşılıklı ve mutabakat edilebilir kalmalı
Gerektirdiği. Yaptırım eylemiyle dondurulan veya imha edilen stablecoin'ler tamamen karşılıklı kalmalıdır; böylece arz ve rezervler mutabakat edilebilir.
Sözleşmenin yaptığı. Düzenlenmiş bir stablecoin için olağan iyileştirme akışı, kötü bir adresteki fonları yakmak ve daha sonra mağdura ayrı bir mint olarak eşit miktarı yeniden ihraç etmektir (USDT'nin destroyBlackFunds artı issue mekanizması böyle çalışır). HKDAP'ın destroy'u _totalSupply'ı azaltır; bu bir yakmadır, ancak balances[address(this)] alanına asla alacak kaydetmez, address(0) yerine address(this)e bir Transfer yayar ve mint limitinin kullandığı net ihraç sayaçlarına dokunmaz:
// 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 }
Ayrıştığı nokta. Durum değişikliği bir yakmadır, ancak olay token'ların sözleşmeye taşındığını söyler ve orada hiçbir şey tutulmaz (balances[address(this)] sıfır kalır). Bir dizinleyici, sahip olmadığı token'ları address(this)e alacak kaydeder ve olaylardan yeniden oluşturulan toplam arz zincirle eşleşmez. Ayrıca iyileştirmenin kendisini de karıştırır: token'lar bekletilmek yerine yakıldığı için, mağdura yeniden ihraç yeni bir mint olmalıdır; ancak yanıltıcı Transfer(..., address(this), ...), sözleşmenin artık onları sakladığını ve iletebileceğini ima eder; oysa yapamaz. address(0)e temiz bir yakma artı ayrı bir yeniden ihraç hem doğru hem de mutabakat edilebilir olurdu. Yazıldığı gibi, bir rezerv mutabakatının bağlı olduğu zincir üstü muhasebe, zincirin gerçek durumundan sapar. Bu, temiz bir geçiş değil, bir endişe kaynağıdır.
Bu bulguları zincirin gösterdikleriyle sınırlıyoruz. Rezervlerin tamamen karşılıklı olup olmadığı, anahtarların bir HSM'de mi yoksa hava boşluklu bir ortamda mı tutulduğu ve işlemlerin imzalanmadan önce zincir dışında simüle edilip edilmediği sözleşmeden görünmez ve bu konularda herhangi bir iddiada bulunmuyoruz.
Sonuç
İncelemenin her iki ekseninde de tablo tutarlıdır. Bir yazılım olarak HKDAP, yazıldığı gibi çalışmayan uyum kontrolleri de dahil olmak üzere işlevsel kusurlar içeriyor ve üretim öncesi bir yapıya dair birçok işaret gösteriyor. Düzenlenmiş bir stablecoin olarak, zincir üstü özelliklerinden birkaçı HKMA kılavuzunun belirli maddeleriyle çelişiyor. Zincir üstü kanıtlara dayanarak ve göremediğimiz zincir dışı konuları bir kenara bırakarak, dağıtılmış sözleşme henüz ticari bir stablecoin'in karşılaması gereken standardı karşılamıyor.
Bunu takip eden iki gözlem var ve açıkça belirtilmeye değer.
Birincisi, halka açık bir zincirde ihraç etmek, uyumun nerede kararlaştırıldığını değiştirir. "Hiçbir tek taraf tek taraflı hareket edememeli" gibi bir gereksinim, dağıtılmış koddaki rol kontrolleri tarafından karşılanır veya karşılanmaz ve bu kod herkese açıktır. Böyle bir gereksinimin gerçekten karşılandığı ya da kaçırıldığı yer lisans veya dokümantasyon değil, uygulamadır ve herkes hangisinin geçerli olduğunu doğrulayabilir.
İkincisi, "Beta Access" etiketi dağıtılmış kodun risk profilini değiştirmez. Sözleşme Ethereum ana ağında canlıdır, gerçek anahtarlarla yönetilir ve Hong Kong doları üzerinde bir hak talebini temsil eder. Bu nedenle, nasıl etiketlendiğinden bağımsız olarak üretim standartlarına tabi tutulmalıdır.
Belirli bulguların altında ortak bir izlek var. Mimari, ekosistemin hâlihazırda sağladığı ve ölçekte denetlediği ilkel yapıları özel biçimde yeniden icat ediyor: bir Safe multisig ile OpenZeppelin'in AccessManager ve TimelockController'ının yeterli olacağı yerde sıfırdan bir onay motoru ve rol katmanı; EnumerableSet yerine elle yazılmış bağlı liste koleksiyonu; standart TransparentUpgradeableProxy yerine değiştirilmiş bir proxy; ve OpenZeppelin'in yerine elle yazılmış bir ERC-20. Bu incelemedeki kusurların çoğu, standart bileşenleri yeniden kullanan kısımlarda değil, bu özel mekanizmada yaşıyor. Sistem, zincir üstü geliştirmenin desteklediği küçük, denetlenmiş yapı taşlarından oluşmak yerine, genel amaçlı yazılımların soyutlandığı gibi soyutlanmış görünüyor; özel soyutlamanın her katmanı aynı zamanda gas, saldırı yüzeyi ve yükseltme riskidir. Bu standart bileşenlerden inşa edilmiş bir tasarım daha küçük, daha güvenli ve incelenmesi daha kolay olur ve bu sistemin şu anda eksik olduğu şeyleri beraberinde getirirdi: bir timelock'tan gelen bir müzakere penceresi ve adlandırılmış, okunaklı roller.
Burada açıklanan sorunlar giderilebilir. Yüksek riskli işlemlerde çoklu imzayı yeniden sağlamak, yürütmeyi denetimden ayırmak, KYC iptal mantığını düzeltmek, transfer kontrollerini birleştirmek, hata ayıklama kodunu kaldırmak ve her yükseltmeden önce üçüncü taraf denetimi zorunlu kılmak bunların çoğunu çözecektir. Doğrulanmış kaynakla ana ağda dağıtım yapmak da bu incelemeyi mümkün kılan şeydir ve doğru varsayılandır. Bu tür sürekli zincir üstü güvenlik ve uyum incelemesi, BlockSec'te yaptığımız iştir ve yardımcı olmaktan mutluluk duyarız.



