A BlockSec security and compliance review of HKDAP (Anchorpoint), as of August 13, 2026.
TL;DR. Analisamos o contrato implantado do HKDAP, a primeira stablecoin regulamentada emitida em Hong Kong, ativa na mainnet da Ethereum, e ele não está pronto para produção. Seus controles de KYC e revogação não funcionam como descrito, sua governança é concentrada o suficiente para que uma única chave possa cunhar, queimar ou congelar, e várias de suas propriedades on-chain conflitam com a própria diretriz do HKMA. Um fio condutor perpassa tudo: o contrato reinventa do zero os primitivos que o ecossistema já fornece e audita em escala (um multisig, uma camada de controle de acesso, um timelock, o próprio ERC-20), e a maioria dos defeitos está nessa mecânica personalizada, não nas partes que reutilizam componentes padrão. O rótulo "Beta Access" não elimina essa lacuna.
Em 12 de agosto de 2026, a Anchorpoint lançou a primeira fase do HKDAP, uma stablecoin atrelada ao dólar de Hong Kong. É um lançamento significativo. A Anchorpoint, uma joint venture liderada pelo Standard Chartered Bank (Hong Kong) com a HKT e a Animoca Brands, possui uma das únicas duas licenças de emissor de stablecoin concedidas pelo HKMA, entre 36 candidatos, e o HKDAP está entre as primeiras stablecoins emitidas sob a Stablecoins Ordinance de Hong Kong.
Diferentemente da maioria dos produtos de um banco regulamentado, o HKDAP é diretamente inspecionável. Ele roda na mainnet da Ethereum e o código-fonte do seu contrato é verificado no Etherscan. Isso é uma propriedade útil: para uma stablecoin emitida dessa forma, as regras que governam emissão, transferência e congelamento não são descritas em um documento; são implementadas em código que qualquer pessoa pode ler e que executa exatamente como escrito. Uma licença é uma declaração; o contrato implantado é a implementação dessa declaração, e é público.
Isso torna possível uma análise concreta, e foi o que fizemos. Examinamos o contrato implantado em dois eixos. Primeiro, como software: ele é correto e tem qualidade de produção? Segundo, como stablecoin regulamentada: seu comportamento on-chain corresponde à Diretriz de Supervisão de Emissores de Stablecoin Licenciados do HKMA?
Os resultados são consistentes nos dois eixos. O contrato contém múltiplos defeitos funcionais, incluindo controles de conformidade que não funcionam como escritos. Sua governança é altamente concentrada, com várias operações de alto risco executáveis por uma única chave. E várias de suas propriedades on-chain conflitam com cláusulas específicas da diretriz do HKMA. Nossa avaliação é que, mesmo como beta, o contrato não atinge o padrão de qualidade que uma stablecoin comercial exige.
O restante deste artigo apresenta essa análise, atualizada em 13 de agosto de 2026. Nossa análise é baseada em código implantado publicamente e em fatos on-chain observáveis. Não fazemos afirmações sobre questões que a blockchain não pode estabelecer, como lastro de reservas ou custódia de chaves off-chain.
Como encontramos o contrato
Começamos pelo emissor, não por uma lista de tokens. A presença corporativa da Anchorpoint remete ao seu site em anchorpoint.hk. A página Beta Access nomeia a implantação: mainnet da Ethereum, proxy 0x87622385F960fcCB3121d6D0A9513bd1D9Bed6cA, cujo código-fonte é verificado no Etherscan.
A partir daí, mapeamos todo o sistema on-chain: o proxy do token e sua implementação (ControllableAHKD, 0xe42d38b05d7ff702193ac5ccedb411b7298ffbfc), o contrato de governança que o administra (0x47dd47a776902bee03abdd5aeff43f41b0ffbc2b), o registro de papéis de nível superior (0xa7287701f7ab8f38ef57cdb2a2a69a40128ec89c) e cinco módulos de conformidade, cada um com seu próprio contrato de governança. Cada relação abaixo foi verificada lendo slots de armazenamento e chamando funções view na mainnet, não inferida apenas do código-fonte.
Como o contrato se parece

O token do HKDAP não usa a implementação de referência ERC-20 padrão da OpenZeppelin; sua lógica ERC-20 é escrita do zero (é daí que vêm vários dos bugs abaixo). O sistema tem três camadas:
- O token.
ControllableAHKD, atrás de um proxy atualizável. Um ERC-20 controlado, com mint, burn, pause e destruição forçada, além de verificações de conformidade embutidas em cada transferência. - Um mecanismo de governança M-de-N caseiro. Toda ação privilegiada (upgrade, mint, burn, pause, blacklist, freeze, alterar um módulo de conformidade) passa por uma cerimônia de solicitação, aprovação e execução, em vez de um multisig simples.
- Cinco módulos de conformidade. Blacklist, congelamento, um serviço de ativação KYC e listas brancas de depósito e resgate. Cada módulo é, ele próprio, um proxy governado por seu próprio contrato de autoridade de controle.
Estruturalmente, essa mesma unidade "autoridade de controle mais proxy mais implementação" se repete seis vezes (o token mais cinco módulos), e todas as seis autoridades de controle resolvem seus papéis em um único registro. Todo o controle do sistema converge, em última instância, para esse único registro, e quem pode reescrever os papéis dentro dele se resume a um conjunto muito pequeno de chaves de assinatura, como mostramos em detalhes abaixo.
Parte 1: Segurança e bugs
Os resultados desta parte estão resumidos abaixo; cada linha é detalhada na seção indicada.
| Área | Constatação | Onde | Efeito |
|---|---|---|---|
| 1.1 Controles de KYC e revogação falham | A revogação de KYC é código morto | TokenHolderActivationServerLibrary.sol:221-235 |
isActive ignora o cancelamento de registro do provedor (o loop nunca executa e usa == em vez de =); falha aberta (fail-open). |
| O cancelamento de registro do verificador nunca define INACTIVE | TokenHolderActivationServer.sol:504-511 |
Um verificador com registro "cancelado" ainda pode integrar e desativar carteiras; uma segunda chamada reverte. | |
| A prova de KYC nunca é validada on-chain | TokenHolderActivationServer.sol:575-578 |
A prova é passada, mas descartada; qualquer prova, inclusive uma string vazia, é aceita. | |
| A isenção de transferência gratuita pode ser contornada por fracionamento | ControllableAHKD.sol:337-535 |
freeTransferLimit é verificado por chamada, não cumulativamente; se usado como limite para atividades sem KYC, pode ser contornado dividindo em transferências abaixo do limite. |
|
| 1.2 Governança excessivamente concentrada | Operações de alto risco são de assinatura única | authorizationMatrix; mint tx 0xa7e53c…b33d7 |
mint / burn / freeze / desativação de KYC (papel C), pause / destroy (papel D), blacklist / unfreeze (papel F) são executados com uma única chave. |
| Um par de chaves (A + B) atualiza tudo | authorizationMatrix |
upgradeTo para o token e todos os cinco módulos é papel A + B; duas pessoas podem substituir qualquer implementação. |
|
| O mesmo par reescreve todos os papéis | registro 0xa728… authorizationMatrix |
grantRole / revokeRole no registro também são papel A + B (ADMIN_ROLE é detido apenas pelo contrato; DEFAULT_ADMIN_ROLE = address(0)); nenhuma chave externa pode alterar papéis diretamente. |
|
| Um endereço detém seis papéis | registro de papéis 0xa728… |
0x2f7f00… detém o papel C, além de ADMIN_TOKEN_HOLDER e todos os quatro papéis de auditor; execução e auditoria se sobrepõem. |
|
| A revogação não é imediata; não há timelock | HybridControlEngine.sol:158-227 |
Uma assinatura contabilizada não é revalidada depois que um papel é revogado; a execução é atômica com a assinatura final, sem atraso. | |
| A trilha de auditoria do mecanismo não é confiável | HybridControlEngine.sol:36-129, 177-196 |
evtApprove emite address(0) como assinante; a lista de solicitações ativas usa nonce 0 tanto como id real quanto como marcador de vazio, então a travessia reversa perde a primeira solicitação. |
|
| 1.3 Verificações de transferência inconsistentes | transfer e transferFrom aplicam regras diferentes |
ControllableAHKD.sol:313-502 |
O retorno antecipado da lista branca ignora o KYC apenas via transferFrom; checkingMode verifica partes diferentes; a mesma transferência é governada de forma diferente. |
| 1.4 Sinais de uma build de pré-produção | logs de depuração, comentário errado, incompatibilidade de nome/hash, testes enviados | UpgradeableProxy.sol:115-119; ControllableAHKD.sol:25 |
console.log no fallback do proxy (gás permanente); comentário do proxy contradiz o código; incompatibilidade nome/hash de papel; 117 arquivos, incluindo testes, enviados; runs do otimizador = 0. |
| 1.5 O único upgrade deixou defeitos no lugar | rastreando o único upgrade | tx 0x742372…85136, 0xa7a400…630c |
Deploy 28 de abr. → upgrade 10 de jul.; as duas transações da cerimônia A + B foram separadas por um bloco (~12s); todo defeito da Parte 1 está na implementação instalada. |
1.1 Os controles de KYC e revogação não funcionam como escritos
De acordo com a regulamentação, os usuários do lado B (institucionais) atendidos pelo HKDAP são obrigados a passar pelo KYC. No contrato, isso significa que ele precisa fazer pelo menos duas coisas: aplicar o KYC nas transferências, para que uma carteira que não passou pelo KYC seja bloqueada, e revogar o acesso quando um provedor de identidade ou um titular for removido. No HKDAP, esse caminho está quebrado em três pontos independentes.
A revogação de KYC é código morto. O token controla as transferências chamando isActive(address) no módulo de KYC. O isActive deveria fazer duas coisas: primeiro, definir um valor base a partir de um contador por carteira, ativo quando essa carteira foi ativada por KYC mais vezes do que foi desativada; depois, reduzir esse valor para falso se algum provedor de identidade que atestou a carteira tiver sido cancelado posteriormente. Os dois casos cobrem dois tipos de revogação: revogar o KYC de um titular (o contador) e revogar um provedor inteiro, de modo que todas as carteiras que ele integrou caiam junto (o loop). Apenas o primeiro acontece.
// 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 }
A linha 221 é uma atribuição real (=) e é a única que tem efeito: active se torna deactivationCount < activationCount. O loop (linhas 226-235) é o que deveria reduzir esse valor, mas falha duas vezes. Sua condição na linha 226, entryCount > chainedItemList.entryCount, é 0 > N, então o corpo nunca executa; e mesmo se executasse, a linha 231 usa ==, uma comparação cujo resultado é descartado, onde deveria atribuir com =. Portanto, isActive retorna apenas a comparação do contador e ignora completamente a revogação do provedor. Isso é fail-open: cancelar o registro de um provedor de KYC comprometido não impede que as carteiras que ele integrou façam transações. E como unregisterVerifier não toca nesses contadores, as carteiras de um provedor revogado continuam com contagem positiva e permanecem ativas.
A revogação do provedor também está quebrada no outro lado. unregisterVerifier apenas move um nó da lista encadeada; nunca define o status do provedor no diretório como 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 }
Como o status permanece ACTIVE, um verificador com registro "cancelado" ainda pode registrar e desativar titulares, o ramo destinado a reativar um provedor é inalcançável, e uma segunda chamada a unregisterVerifier causa underflow e reverte.
A prova de KYC nunca é validada on-chain. Quando um verificador autorizado registra ou renova uma carteira por meio de registerOrRenew (restrito para que apenas um verificador atualmente ativo possa chamá-lo), o fluxo chega a _checkKYCProof, que deveria validar a prova enviada contra o esquema do provedor. De acordo com a interface, essa prova é "uma URI para um Oracle, ou um hash assinado de uma fonte verificável". A função a ignora:
// 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 }
A prova realmente chega a essa função. registerOrRenew carrega o kycProof enviado através de _registerOrRenew e o passa como segundo argumento. Mas na linha 575 esse argumento cai em um parâmetro sem nome algum, que o comentário de documentação acima da função também omite, e o corpo nunca o lê. A função apenas confirma que o provedor está ativo e retorna true na linha 577, então a prova é descartada em vez de verificada. Isso não é uma bypass por um externo, já que apenas um verificador ativo pode alcançá-la, mas on-chain o contrato não realiza nenhuma validação da evidência de KYC, portanto sua integridade depende inteiramente do verificador off-chain. Um verificador comprometido ou descuidado pode ativar qualquer carteira com qualquer prova, inclusive uma string vazia.
Juntas, essas três questões significam que uma propriedade central de conformidade — a capacidade de controlar e revogar acesso — não funciona como escrita.
Além dessas três, há uma fraqueza relacionada: a isenção de transferência gratuita pode ser contornada por fracionamento. O token tem um freeTransferLimit, e uma transferência abaixo dele pula a verificação isActive (KYC). Mas o limite é comparado apenas com o valor da transferência atual; o contrato não mantém um total acumulado por endereço ou período. Assim, se o limite for usado como teto para atividades sem KYC, um titular pode mover um total arbitrário dividindo-o em transferências repetidas, cada uma pouco abaixo do limite, o que torna o teto ineficaz.
1.2 A governança é excessivamente concentrada, e operações de alto risco são de assinatura única
Enumeramos todos os papéis e todos os detentores de papéis on-chain. Duas coisas se destacam antes dos detalhes.
Primeiro, os papéis que autorizam as cerimônias M-de-N não têm nomes legíveis. Na configuração implantada, eles aparecem apenas como hashes de 32 bytes, e nenhum deles corresponde a uma constante de papel nomeada no código-fonte verificado (os papéis nomeados, como SUPPLY_CONTROLLER_ROLE, são detidos pelos próprios contratos, não pelos signatários). Rotulamos os seis papéis de signatário de A a F. O fato de os papéis mais poderosos do sistema serem identificadores opacos já é, por si só, uma fraqueza: torna a governança mais difícil de revisar do que papéis nomeados.
Segundo, o número de detentores é pequeno. A tabela abaixo é lida do registro de papéis on-chain; os endereços estão abreviados.
| Papel (nosso rótulo) | Hash on-chain | Detentor(es) | O que autoriza |
|---|---|---|---|
| A | 0x37f0f656… |
0x747356d525bf47f495af7c6e42f3ab9b8ba87a4b |
primeira assinatura para upgradeTo / changeAdmin / setControlAuthority |
| B | 0x95c27f81… |
0x8117a657df7a1c399231bfe26a096eb8f8ad5973, 0x5d9d9b28e83382e694916b0feeaf7a9badbb659c, 0xebb73f78e253539acceb4e6cc287095788eee5d4 |
a segunda assinatura em quase toda ação de duas assinaturas |
| C | 0xfa2fe896… |
0x2f7f00cc5334fe2861e485ff610f74890a0316ed |
mintToDeposit, burnFrom, freeze, desativação de KYC (única) |
| D | 0x3dce3265… |
0x5092af62a1625fa57404557d3ad417474f3f494c |
pause, destroyBlackFunds, registrar/cancelar endereços de depósito e resgate, registerVerifier (única) |
| E | 0xfd21a76d… |
0xa9315aadc89ba681f1fe5df3375ab1a87d37eca2 |
alterar os módulos de conformidade (setBlacklistServer, e assim por diante) |
| F | 0x510ac1ff… |
0x7daabe5092d3feea5cc19ddbb5380a4eac38f89c |
addBlackList, unfreeze (única) |
Vários pontos decorrem da tabela.
Operações de alto risco com um único signatário. A maioria das operações de alto risco exige um único papel com cota de um. A tabela abaixo é lida on-chain a partir do authorizationMatrix do contrato de governança do token e dos cinco contratos de governança dos módulos (assinatura única, a menos que uma segunda assinatura + B seja indicada):
| Operação | Governado por | Assinaturas exigidas |
|---|---|---|
mintToDeposit, burnFrom |
token | C x1 |
freeze, batchFreeze |
módulo de congelamento | C x1 |
deactivate, adminDeactivate (KYC) |
módulo de KYC | C x1 |
pause, destroyBlackFunds |
token | D x1 |
register, unregister (depósito / resgate) |
módulos de diretório | D x1 |
registerVerifier, unregisterVerifier |
módulo de KYC | D x1 |
addBlackList, batchBlackList |
módulo de blacklist | F x1 |
unfreeze |
módulo de congelamento | F x1 |
removeBlackList |
módulo de blacklist | F x1 + B x1 |
setBlacklistServer, setFreezingServer, setCheckingMode |
token | E x1 + B x1 |
| setters de servidor de diretório e limite de supply | token | D x1 + B x1 |
upgradeTo / changeAdmin / setControlAuthority (token e todos os módulos), unpause |
token + módulos | A x1 + B x1 |
Emissão, congelamento e desativação de KYC são de assinatura única (todos sob o papel C); pause, destroy, alterações de diretório e registro de verificador são de assinatura única sob o papel D; blacklist e unfreeze são de assinatura única sob o papel F. Apenas upgrades e alterações de configuração exigem uma segunda assinatura. Note a assimetria: freeze e addBlackList exigem uma assinatura, enquanto removeBlackList exige duas; portanto, restringir uma conta é mais fácil do que liberá-la.
Isso não é apenas uma leitura da matriz; é observável em um mint real. A emissão mais recente no momento da escrita, tx 0xa7e53c…b33d7, é uma única transação enviada pela única conta com papel C (0x2f7f00cc5334fe2861e485ff610f74890a0316ed) ao contrato de governança do token. Ela chama request, e essa mesma transação atinge quórum e emite o Transfer de mint a partir de address(0), sem transação de aprovação separada e sem segundo signatário. Como a emissão é liquidada dentro da própria transação do solicitante, uma única chave solicitou e executou o mint.
Também não há timelock em nenhum lugar do mecanismo. No momento em que a última assinatura exigida chega, a ação é executada na mesma transação, sem nenhum atraso em que possa ser revisada, cancelada ou contestada; o mecanismo registra um timestamp executedAt, mas nunca o verifica. Assim, até as operações de duas assinaturas são liquidadas instantaneamente quando a segunda chave assina.
Um par de chaves atualiza tudo. Atualizar o token e atualizar todos os cinco módulos de conformidade usam o mesmo requisito, A mais B. Como A é uma conta e B são três contas compartilhando um papel, duas pessoas podem substituir qualquer implementação no sistema.
O mesmo par também controla a tabela de papéis. Papéis só podem ser concedidos e revogados no registro (HybridControlledAuthority, 0xa7287701f7ab8f38ef57cdb2a2a69a40128ec89c), por meio de sua própria cerimônia; nenhuma chave externa pode alterá-los diretamente. E seu authorizationMatrix, lido on-chain, exige as mesmas assinaturas para uma alteração de papel que para um upgrade: papel A mais papel B. Assim, as duas pessoas que podem substituir qualquer implementação também podem adicionar uma chave ao papel C, remover um detentor existente e reescrever toda a tabela de papéis. Essa barreira de duas assinaturas é melhor do que as operações de assinatura única acima, mas ainda é um padrão baixo para a raiz do sistema, já que o papel A é uma única conta, sem redundância.
Um endereço, seis papéis. O detentor do papel C (0x2f7f00cc5334fe2861e485ff610f74890a0316ed) também detém o ADMIN_TOKEN_HOLDER_ROLE nomeado (gestão de verificadores de KYC) e é membro de todos os quatro papéis de auditor (BLACKLIST_AUDITOR_ROLE, FREEZING_AUDITOR_ROLE, WHITELIST_AUDITOR_ROLE e AFL_TOKEN_AUDITOR_HOLDER_ROLE). O comprometimento dessa única chave significa a perda simultânea de emissão, congelamento e administração de KYC.
Os papéis de auditor têm dois problemas próprios. Primeiro, eles controlam os getters somente-leitura das listas de conformidade, aparentemente para controlar quem pode lê-las, mas em uma chain pública isso é inútil: o armazenamento subjacente é legível por qualquer pessoa (ler slots de armazenamento foi como mapeamos este sistema), portanto as listas são públicas de qualquer forma, e a restrição mostra que o design não levou em conta estar em uma chain pública. Segundo, concentração: todos os quatro papéis de auditor estão com as mesmas 23 contas, e os detentores dos papéis de execução A, C, D e F estão entre elas; assim, as mesmas chaves que cunham, queimam, congelam e colocam na blacklist também estão no grupo que deveria revisar essas ações.
A revogação não é imediata. No mecanismo de cerimônia, o papel de um signatário é verificado uma vez e a cota é decrescida; signatários anteriores nunca são revalidados, portanto revogar um papel depois não retira um voto já contabilizado:
// 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 }
Por fim, a trilha de auditoria do próprio mecanismo não é confiável. Em cada aprovação, o evento evtApprove emite address(0) como signatário, em vez do aprovador real; o signatário real sobrevive apenas no remetente da transação e em um registro interno, portanto os logs de eventos não conseguem atribuir quem aprovou uma solicitação. E a lista de solicitações ativas usa nonce 0 tanto como id real quanto como marcador de vazio, então ferramentas de monitoramento ou aprovação que percorrem a lista para trás perdem a solicitação no nonce 0. Nenhum dos dois é crítico, mas, para um sistema regulamentado que precisa de uma trilha de auditoria limpa, ambos a enfraquecem.
1.3 Os controles de transferência são inconsistentes entre transfer e transferFrom
Parte da diferença entre os dois é esperada. Em transferFrom, o iniciador, msg.sender, é um gastador aprovado, e não a origem dos fundos; portanto, o código verifica a origem real from explicitamente em todos os modos; transfer não precisa, porque ali msg.sender é a origem. Essa adaptação é razoável. Outras duas diferenças não são explicadas pelo iniciador e deixam a mesma ação econômica governada por regras diferentes.
Primeiro, as listas brancas de depósito e resgate são consultadas apenas no caminho de transferFrom, onde a associação a uma delas aciona um retorno antecipado que ignora a verificação isActive (KYC). transfer nunca as consulta. O fato de um destinatário estar na lista branca não tem relação com quem iniciou a transferência; assim, o mesmo destinatário está sujeito ao KYC via transfer, mas pode ignorá-lo via 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" ) ) ;
Segundo, checkingMode significa coisas diferentes nas duas funções. Em transfer, o modo "Source" verifica o remetente; em transferFrom, o modo "Source" verifica o gastador (msg.sender), enquanto from é verificado em todos os modos. Assim, uma única definição de configuração aplica duas políticas diferentes dependendo do ponto de entrada.
O efeito é que os controles de transferência de um token regulamentado dependem de qual função é usada, o que os torna difíceis de entender e, dependendo da configuração, contornáveis.
1.4 Sinais de uma build de pré-produção
Além dos bugs de lógica específicos, várias propriedades do código indicam que uma build de pré-produção foi implantada na mainnet.
Logs de depuração em produção. Chamadas a hardhat/console.log permanecem por todo o código, inclusive no fallback do proxy, que executa em toda transação de usuário. Como o proxy não é atualizável, esse custo extra é permanente:
// 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 }
Um comentário no cabeçalho do proxy que descreve o oposto do código. O mesmo arquivo carrega a documentação do TransparentUpgradeableProxy da OpenZeppelin, que afirma que o admin nunca pode ser repassado para a implementação. Este contrato deliberadamente faz o inverso: a proteção na linha 117 está comentada, e o admin é repassado. Um revisor que confiasse no comentário modelaria incorretamente o limite de confiança.
Nomes de papéis que não correspondem aos seus hashes. O papel de "risco elevado" é declarado com o mesmo nome de constante, mas com uma string keccak diferente no token e nos módulos, o que produz dois papéis diferentes:
// 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') ;
A implantação só evita problemas porque cada módulo tem sua própria cópia; um segundo nome de papel (AFL_TOKEN_HOLDER_AUDITOR_ROLE) é transposto da mesma maneira.
Outros sinais. O pacote de verificação do proxy enviou 117 arquivos, incluindo a suíte de testes do projeto, ao explorador público, o que entrega ao leitor os testes internos e casos de borda. O upgrade mais recente alterou apenas a limpeza de avisos do compilador, sem auditoria de terceiros no processo. E o otimizador está definido para zero runs, o que torna os caminhos críticos de um token muito usado mais caros, em vez de menos caros.
Nenhum desses problemas é individualmente grave. Juntos, eles indicam que o código não passou pela disciplina de lançamento esperada de um contrato que detém valor na mainnet.
1.5 O único upgrade do contrato, rastreado
O proxy foi atualizado uma vez. Foi implantado em 28 de abril de 2026 com a implementação 0x8ff12fe3bed22d9e40afb4b98ef4dee28d94699e e, em 10 de julho de 2026, foi atualizado para a atual 0xe42d38b05d7ff702193ac5ccedb411b7298ffbfc. Como um upgrade é uma ação de governança on-chain, podemos ver exatamente quem o aprovou.
upgradeTo exige papel A mais papel B. O upgrade consistiu em duas transações em blocos adjacentes, com cerca de doze segundos de diferença:
- a solicitação, papel A, de
0xa9315aadc89ba681f1fe5df3375ab1a87d37eca2, no bloco 25500519 (tx0x742372…85136); - a aprovação, papel B, de
0x3795300b31429f9d37b0dc805528d9390ce87c50, no bloco 25500520 (tx0xa7a400…630c), que atingiu o quórum e realizou o upgrade na mesma transação.
Duas coisas se destacam. Primeiro, toda a cerimônia de duas assinaturas foi concluída dentro de um único intervalo de bloco. Da solicitação à execução foi um bloco; não houve janela em que o segundo signatário pudesse revisar de forma independente antes de a alteração entrar no ar.
Segundo, nenhum dos signatários é identificável a partir do registro de papéis atual. Os papéis foram rotacionados desde então: o solicitante 0xa9315a… não detém mais o papel A (hoje detém o papel E), e o segundo signatário 0x3795300b… não detém papel algum agora. Ler o registro como está não diria quem autorizou o upgrade; apenas o histórico de transações diz. Essa é a propriedade de "revogação não imediata" vista do outro lado: os papéis mudam, então um instantâneo de quem detém o quê não é um registro de quem fez o quê.
E o upgrade não corrigiu nenhum dos defeitos desta análise. A implementação que ele instalou, 0xe42d38b0…, é exatamente a que toda a Parte 1 descreve. Não podemos dizer pela chain se uma auditoria de terceiros foi realizada antes do upgrade; o que podemos dizer é que, se foi, os defeitos da Parte 1 sobreviveram a ela.
Parte 2: Ele corresponde à estrutura de stablecoins de Hong Kong?
A Stablecoins Ordinance de Hong Kong entrou em vigor em 1º de agosto de 2025, e os emissores licenciados são supervisionados pela Diretriz de Supervisão de Emissores de Stablecoin Licenciados do HKMA. Comparamos apenas as cláusulas que um contrato inteligente pode cumprir por conta própria; lastro de reservas, custódia e cerimônias de chaves off-chain estão fora do escopo de uma análise on-chain. Para cada cláusula abaixo, declaramos o que a diretriz exige, o que o contrato faz e onde os dois divergem. A comparação está resumida aqui e detalhada nas seções a seguir.
| Cláusula do HKMA | O que exige | Onde o contrato diverge | Veredito |
|---|---|---|---|
| 6.5.3 | Operações de alto risco não devem ser unilaterais (multisignatura e mitigadores como limites de velocidade ou timelocks) | mint, burn, pause e freeze são executados cada um com uma chave; sem timelock (a execução é atômica com a assinatura final) | Diverge |
| 6.5.4 | Segregar funções entre pessoas autorizadas; revogar autoridade imediatamente | uma conta detém seis papéis, então execução e auditoria se sobrepõem; uma aprovação contabilizada sobrevive a uma revogação posterior | Diverge |
| 6.5.5 | Auditoria de terceiros para cada alteração de código; correto, consistente, livre de vulnerabilidades | os defeitos da Parte 1 estão ativos na implementação implantada; isActive e a revogação de provedor não fazem o que seus nomes dizem |
Diverge |
| Eficácia dos controles de conformidade | controles de blacklist, freeze, whitelist e KYC devem ser eficazes | a aplicação de KYC e a revogação de provedor não são funcionais no código implantado | Diverge |
| 2.2.3 | Moedas congeladas ou destruídas permanecem totalmente lastreadas e reconciliáveis | destroy emite Transfer para address(this) em vez de address(0) e deixa os contadores de emissão líquida intocados; o supply reconstruído a partir de eventos se desvia |
Preocupação |
Parágrafo 6.5.3: operações de alto risco não devem ser unilaterais
O que exige. Operações de alto risco devem ser projetadas de modo que nenhuma parte única possa executá-las unilateralmente, por exemplo por meio de um protocolo de multisignatura, e a diretriz lista mitigadores adicionais, como limites de velocidade e controles com atraso de tempo (timelock).
O que o contrato faz. Lido a partir do authorizationMatrix do contrato de governança (a matriz completa está na tabela em 1.2), as operações de supply e emergência exigem cada uma um único papel, com cota de um:
| Operação | Assinatura exigida |
|---|---|
mintToDeposit |
ROLE_C x1 |
burnFrom |
ROLE_C x1 |
freeze |
ROLE_C x1 |
pause |
ROLE_D x1 |
destroyBlackFunds |
ROLE_D x1 |
Onde diverge. Cunhar, queimar, pausar e congelar podem ser executados cada um por uma única chave. Isso não atende ao requisito de "nenhuma parte única unilateralmente". Também não há timelock: como mostrado em 1.2, uma vez atingido o quórum, a ação é executada na mesma transação, portanto um atraso de tempo, um dos mitigadores citados pela diretriz, também está ausente. O contrato implementa um limite de velocidade de supply (whenWithinRiskThresholds), então esse mitigador está presente, mas não substitui o controle de multisignatura nas próprias operações.
Parágrafo 6.5.4: segregação de funções e revogação imediata
O que exige. Operações diferentes devem ser segregadas entre pessoas autorizadas diferentes, e a autoridade de uma pessoa autorizada deve ser revogável imediatamente.
O que o contrato faz. Uma conta de propriedade externa detém seis papéis (emissão, congelamento, administração de KYC e todos os quatro papéis de auditor), portanto os papéis de execução e auditoria se sobrepõem. As próprias alterações de papéis também exigem apenas o papel A mais o papel B (veja 1.2), então o mesmo pequeno grupo decide quem é autorizado e pode executar e atualizar. E uma assinatura já contabilizada não é revalidada se o papel do signatário for revogado posteriormente.
Onde diverge. As funções estão concentradas, e não segregadas, e a revogação não é imediata: a aprovação anterior de um signatário revogado ainda conta para uma execução posterior.
Parágrafo 6.5.5: auditar toda alteração de código; correto, consistente, sem vulnerabilidades
O que exige. Um terceiro qualificado deve auditar os contratos inteligentes para cada alteração de código e confirmar que eles estão (i) implementados corretamente, (ii) consistentes com a funcionalidade pretendida e (iii) livres de vulnerabilidades com um alto nível de confiança.
O que o contrato faz. A implementação atual é a instalada pelo upgrade de 10 de julho (rastreado em 1.5), e os defeitos da Parte 1 estão ativos nela.
Onde diverge. Não podemos ver off-chain se uma auditoria foi realizada, mas o resultado não atende ao padrão de qualquer forma. Como isActive e a revogação de provedor não fazem o que seus nomes indicam, as condições (i) e (ii) não são atendidas; e, com os defeitos da Parte 1 presentes no código implantado, (iii) também não é atendida.
Eficácia dos controles de conformidade
O que exige. O modelo de ciclo de vida da diretriz (blacklist, freeze, whitelist, KYC) pressupõe que esses controles sejam eficazes.
O que o contrato faz. Como mostrado em 1.1, a aplicação de KYC e a revogação de provedor não são funcionais no código implantado.
Onde diverge. Um controle que deveria funcionar não funciona. Isso é uma lacuna substantiva, não uma formalidade.
Parágrafo 2.2.3: moedas congeladas ou destruídas permanecem totalmente lastreadas e reconciliáveis
O que exige. Stablecoins congeladas ou destruídas por ação de fiscalização devem permanecer totalmente lastreadas, para que o supply e as reservas possam ser reconciliados.
O que o contrato faz. O fluxo usual de remediação para uma stablecoin regulamentada é queimar os fundos em um endereço ruim e, depois, reemitir um valor igual à vítima como um mint separado (é assim que funcionam destroyBlackFunds e issue da USDT). O destroy do HKDAP reduz _totalSupply, o que é um burn, mas nunca credita balances[address(this)], emite um Transfer para address(this) em vez de address(0) e deixa os contadores de emissão líquida usados pelo limite de mint intocados:
// 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 }
Onde diverge. A mudança de estado é um burn, mas o evento diz que os tokens foram movidos para o contrato, e nada é mantido lá (balances[address(this)] permanece zero). Um indexador creditaria address(this) com tokens que ele não detém, e o supply total reconstruído a partir dos eventos não corresponderia à chain. Isso também confunde a própria remediação: como os tokens são queimados em vez de estacionados, uma reemissão à vítima precisa ser um mint novo; no entanto, o enganoso Transfer(..., address(this), ...) sugere que o contrato agora os custodia e poderia repassá-los, o que não pode. Um burn limpo para address(0) mais uma reemissão separada seria correto e reconciliável. Como escrito, a contabilidade on-chain da qual uma reconciliação de reservas depende se desvia do estado real da chain. Isso é uma preocupação, não uma aprovação plena.
Limitamos essas constatações ao que a chain mostra. Se as reservas têm lastro total, se as chaves estão em um HSM ou em um ambiente com isolamento de rede, e se as transações são simuladas off-chain antes de assinar não são visíveis no contrato, e não fazemos afirmações sobre isso.
Conclusão
O quadro é consistente nos dois eixos da análise. Como software, o HKDAP contém defeitos funcionais, incluindo controles de conformidade que não executam como escritos, e mostra vários sinais de uma build de pré-produção. Como stablecoin regulamentada, várias de suas propriedades on-chain conflitam com cláusulas específicas da diretriz do HKMA. Com base nas evidências on-chain e deixando de lado as questões off-chain que não podemos ver, o contrato implantado ainda não atende ao padrão que uma stablecoin comercial deveria atender.
Duas observações se seguem e merecem ser ditas claramente.
Primeiro, emitir em uma chain pública muda onde a conformidade é decidida. Um requisito como "nenhuma parte única deve ser capaz de agir unilateralmente" é satisfeito ou não pelas verificações de papéis no código implantado, e esse código é público. A implementação, e não a licença ou a documentação, é onde tal requisito é realmente atendido ou deixado de ser atendido, e qualquer pessoa pode verificar qual é o caso.
Segundo, o rótulo "Beta Access" não muda o perfil de risco do código implantado. O contrato está ativo na mainnet da Ethereum, administrado por chaves reais e representa um direito sobre dólares de Hong Kong. Portanto, deve ser mantido nos padrões de produção, independentemente de como seja rotulado.
Um fio condutor percorre as constatações específicas. A arquitetura reinventa, de forma artesanal, primitivos que o ecossistema já fornece e audita em escala: um mecanismo de aprovação e uma camada de papéis feitos do zero, onde um multisig Safe com AccessManager e TimelockController da OpenZeppelin resolveria; uma coleção de lista encadeada escrita à mão no lugar de EnumerableSet; um proxy modificado no lugar do TransparentUpgradeableProxy padrão; e um ERC-20 escrito à mão no lugar do da OpenZeppelin. A maioria dos defeitos desta análise vive nessa maquinaria personalizada, não nas partes que reutilizam componentes padrão. Parece um sistema abstraído da mesma forma que software de propósito geral, em vez de composto pelos pequenos blocos de construção auditados que o desenvolvimento on-chain favorece, onde cada camada de abstração personalizada é também gás, superfície de ataque e risco de upgrade. Um design construído a partir desses componentes padrão seria menor, mais seguro e mais fácil de revisar, e viria com as coisas que este sistema atualmente não tem: uma janela de deliberação proveniente de um timelock e papéis nomeados e legíveis.
Os problemas descritos aqui são solucionáveis. Restaurar a multisignatura em operações de alto risco, separar execução de auditoria, corrigir a lógica de revogação de KYC, unificar as verificações de transferência, remover o código de depuração e exigir auditoria de terceiros antes de cada upgrade resolveria a maioria deles. Implantar na mainnet com código-fonte verificado também é o que tornou esta análise possível, e é o padrão correto. Revisão contínua de segurança e conformidade on-chain desse tipo é o que fazemos na BlockSec, e ficamos felizes em ajudar.



