Back to Blog

Revisão de Segurança da Stablecoin HKDAP: Ativa, Licenciada, Não Pronta

Phalcon Security
14 de agosto de 2026
28 min read
Key Insights
  • A revogação de KYC do HKDAP é código morto e sua prova de KYC nunca é validada on-chain

  • Uma única chave pode emitir, queimar, pausar ou congelar; duas chaves controlam todas as atualizações e funções

  • Diversas propriedades on-chain divergem da diretriz de emissores de stablecoin da HKMA

Uma análise de segurança e conformidade da BlockSec sobre a HKDAP (Anchorpoint), em 13 de agosto de 2026.

TL;DR. Analisamos o contrato implantado da HKDAP, a primeira stablecoin regulamentada emitida em Hong Kong, ativa na mainnet da Ethereum, e ela não está pronta para produção. Seus controles de KYC e revogação não funcionam como especificado, sua governança é concentrada a ponto de uma única chave poder emitir, queimar ou congelar, e várias de suas propriedades on-chain conflitam com a própria diretriz da HKMA. Um fio condutor percorre tudo isso: o contrato reinventa do zero as primitivas que o ecossistema já oferece 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 engenharia customizada, e não nas partes que reutilizam componentes padrão. O rótulo de "Beta Access" não fecha essa lacuna.

Em 12 de agosto de 2026, a Anchorpoint lançou a primeira fase da HKDAP, uma stablecoin em dólar de Hong Kong. É um lançamento significativo. A Anchorpoint, uma joint venture liderada pelo Standard Chartered Bank (Hong Kong) com HKT e Animoca Brands, detém uma das apenas duas licenças de emissor de stablecoin concedidas pela HKMA, entre 36 candidatos, e a HKDAP está entre as primeiras stablecoins emitidas sob a Ordenança de Stablecoins de Hong Kong.

Diferentemente da maioria dos produtos de um banco regulamentado, a HKDAP pode ser diretamente inspecionada. Ela funciona na mainnet da Ethereum, e o código-fonte de seu contrato está verificado no Etherscan. Essa é uma propriedade útil: para uma stablecoin emitida dessa forma, as regras que regem emissão, transferência e congelamento não estão descritas em um documento, mas implementadas em código que qualquer pessoa pode ler e que executa exatamente como escrito. Uma licença é uma alegação; o contrato implantado é a implementação dessa alegação, e ele é público.

Isso torna possível uma análise concreta, e foi isso que fizemos. Examinamos o contrato implantado sob dois eixos. Primeiro, como software: ele é correto e possui qualidade de produção? Segundo, como uma stablecoin regulamentada: seu comportamento on-chain corresponde à Diretriz sobre Supervisão de Emissores Licenciados de Stablecoins da HKMA?

As descobertas são consistentes em ambos os eixos. O contrato contém múltiplos defeitos funcionais, incluindo controles de conformidade que não funcionam como especificado. Sua governança é altamente concentrada, com diversas 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 da HKMA. Nossa avaliação é que, mesmo como beta, o contrato não atende ao padrão de qualidade exigido por uma stablecoin comercial.

O restante deste artigo apresenta essa análise, atualizada até 13 de agosto de 2026. Nossa análise se baseia em código implantado publicamente e fatos observáveis on-chain. Não fazemos alegações sobre questões que a blockchain não pode estabelecer, como respaldo de reservas ou custódia de chaves fora da cadeia.

Como encontramos o contrato

Começamos pelo emissor, não por uma lista de tokens. A presença institucional da Anchorpoint remete ao seu site em anchorpoint.hk. A página de Beta Access nomeia a implantação: mainnet da Ethereum, proxy 0x87622385F960fcCB3121d6D0A9513bd1D9Bed6cA, cujo código-fonte está 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 de leitura na mainnet, e não inferida apenas a partir do código-fonte.

Como o contrato é estruturado

As três camadas da HKDAP: um plano de controle de governança administra o token, e o token consulta cinco módulos de conformidade em cada transferência. image.png

O token da HKDAP não usa a implementação de referência padrão do ERC-20 da OpenZeppelin; sua lógica de ERC-20 foi 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 desenvolvido internamente. Toda ação privilegiada (upgrade, mint, burn, pause, blacklist, congelamento, mudança de módulo de conformidade) passa por uma cerimônia de solicitação, aprovação e execução, em vez de um simples multisig.
  • Cinco módulos de conformidade. Blacklist, congelamento, um serviço de ativação de KYC, e listas de permissão para 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 de "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 no sistema converge, em última instância, para esse único registro, e quem pode reescrever os papéis dentro dele depende de um conjunto muito pequeno de chaves de signatários, como mostramos em detalhes a seguir.

Parte 1: Segurança e bugs

As descobertas desta parte são resumidas abaixo; cada linha é detalhada na seção indicada.

Área Descoberta Onde Efeito
1.1 Os controles de KYC e revogação falham Revogação de KYC é código morto TokenHolderActivationServerLibrary.sol:221-235 isActive ignora o cancelamento de registro do provedor (o loop nunca é executado e usa == em vez de =); falha aberta.
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 revert.
Prova de KYC nunca validada on-chain TokenHolderActivationServer.sol:575-578 A prova é passada, mas descartada; qualquer prova, incluindo uma string vazia, passa.
Isenção de transferência gratuita pode ser contornada ControllableAHKD.sol:337-535 freeTransferLimit é verificado por chamada, não cumulativamente; se usado como limite para atividades sem KYC, pode ser contornado dividindo em subtransferências abaixo do limite.
1.2 A governança é excessivamente concentrada Operações de alto risco são de assinatura única authorizationMatrix; tx de mint 0xa7e53c…b33d7 mint / burn / freeze / desativação de KYC (papel C), pause / destroy (papel D), blacklist / unfreeze (papel F) cada um executado 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 é mantido apenas pelo próprio 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 mais ADMIN_TOKEN_HOLDER e todos os quatro papéis de auditor; execução e auditoria se sobrepõem.
A revogação não é imediata; sem timelock HybridControlEngine.sol:158-227 Uma assinatura contabilizada não é revalidada após a revogação de um papel; a execução é atômica com a assinatura final, sem qualquer atraso.
O rastro de auditoria do mecanismo não é confiável HybridControlEngine.sol:36-129, 177-196 evtApprove emite address(0) como o signatário; a lista de solicitações ativas usa o nonce 0 tanto como um id real quanto como marcador vazio, de modo que 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 de permissão contorna o KYC apenas via transferFrom; checkingMode verifica partes diferentes; a mesma transferência é governada de forma diferente.
1.4 Sinais de uma build pré-produção logs de depuração, comentário incorreto, 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 de nome/hash de papel; 117 arquivos incl. testes enviados; optimizer runs = 0.
1.5 O único upgrade deixou defeitos no lugar rastreando o único upgrade tx 0x742372…85136, 0xa7a400…630c Deploy em 28 de abril → upgrade em 10 de julho; as duas transações da cerimônia A + B foram feitas com um bloco de diferença (~12s); todo defeito da Parte 1 está na implementação instalada.

1.1 Os controles de KYC e revogação não funcionam como especificado

Sob a regulamentação, os usuários do lado B (institucionais) que a HKDAP atende são obrigados a passar por KYC. No contrato, isso significa que ele precisa fazer pelo menos duas coisas: aplicar KYC nas transferências, de modo que uma carteira que não passou pelo KYC seja bloqueada, e revogar o acesso quando um provedor de identidade ou um titular é removido. Na HKDAP, esse fluxo 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. 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 desativada; depois, restringir esse valor para falso se algum provedor de identidade que a validou tiver, desde então, cancelado seu próprio registro. As duas cobrem dois tipos de revogação, revogar o KYC de um titular (o contador) e revogar um provedor inteiro, de modo que toda carteira que ele integrou caia junto (o loop). Apenas a primeira 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 entra em vigor: active se torna a comparação do contador (deactivationCount < activationCount). O loop (linhas 226-235) é o que deveria restringir esse valor, mas falha duas vezes. Sua condição na linha 226, entryCount > chainedItemList.entryCount, é 0 > N, então o corpo nunca é executado; e mesmo que fosse, a linha 231 usa ==, uma comparação cujo resultado é descartado, quando deveria fazer uma atribuição com =. Portanto, isActive retorna apenas a comparação do contador e ignora completamente a revogação do provedor. Isso é uma falha aberta: cancelar o registro de um provedor de KYC comprometido não impede que as carteiras que ele integrou continuem transacionando. E, como unregisterVerifier não altera esses contadores, as carteiras de um provedor revogado mantêm uma contagem positiva e permanecem ativas.

O cancelamento de registro do provedor também é falho do outro lado. unregisterVerifier apenas move um nó de 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 que deveria reativar um provedor é inalcançável, e uma segunda chamada a unregisterVerifier sofre underflow e revert.

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 alcança _checkKYCProof, que deveria validar a prova submetida contra o esquema do provedor. Segundo a interface, essa prova é "uma URI para um Oráculo, 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 transporta o kycProof submetido através de _registerOrRenew e o passa como segundo argumento. Mas, na linha 575, esse argumento cai em um parâmetro sem nenhum nome, 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 verdadeiro na linha 577, então a prova é descartada em vez de verificada. Isso não é um bypass por parte de terceiros, 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, então sua integridade repousa inteiramente sobre o verificador fora da cadeia. Um verificador comprometido ou negligente pode ativar qualquer carteira com qualquer prova, incluindo uma string vazia.

Juntos, esses três pontos significam que uma propriedade central de conformidade, a capacidade de restringir e revogar acesso, não funciona como especificado.

Além desses três, há uma fragilidade relacionada: a isenção de transferência gratuita pode ser contornada por divisão. O token tem um freeTransferLimit, e uma transferência abaixo desse limite ignora a verificação isActive (KYC). Mas o limite é comparado apenas com o valor da transferência atual; o contrato não mantém nenhum total cumulativo por endereço ou período. Assim, se o limite for usado como um teto para atividades sem KYC, um titular pode movimentar um total arbitrário dividindo-o em transferências repetidas, cada uma 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 cada papel e cada detentor de papel on-chain. Dois pontos se destacam antes das especificidades.

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 é, por si só, uma fragilidade: torna a governança mais difícil de revisar do que papéis nomeados permitiriam.

Segundo, o número de detentores é pequeno. A tabela abaixo é lida diretamente 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 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, registro/cancelamento de 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 dessa tabela.

Operações de alto risco com signatário único. 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 de módulos (assinatura única, exceto quando uma segunda assinatura + B é indicada):

Operação Governada 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
definidores de servidor de diretório e limite de fornecimento token D x1 + B x1
upgradeTo / changeAdmin / setControlAuthority (token e todo módulo), 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); pausa, destruição, alterações de diretório e registro de verificador são de assinatura única sob o papel D; blacklisting e unfreezing 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 precisam de uma assinatura, enquanto removeBlackList precisa de duas, então 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 redação, tx 0xa7e53c…b33d7, é uma única transação enviada pela única conta com papel C (0x2f7f00cc5334fe2861e485ff610f74890a0316ed) para o contrato de governança do token. Ela chama request, e essa mesma transação atinge o quórum e emite a Transfer de mint a partir de address(0), sem nenhuma transação de aprovação separada e sem segundo signatário. Como a emissão se conclui dentro da própria transação do solicitante, uma única chave solicitou e executou o mint.

Também não há nenhum timelock em nenhum lugar do mecanismo. No momento em que a última assinatura exigida chega, a ação é executada nessa mesma transação, sem nenhum atraso em que pudesse ser revisada, cancelada ou contestada; o mecanismo registra um timestamp executedAt, mas nunca o verifica. Assim, mesmo as operações de duas assinaturas se resolvem instantaneamente uma vez que 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 única 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), através 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 mudança 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. Esse portão de duas assinaturas é melhor do que as operações de assinatura única acima, mas ainda é um limite 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 (gerenciamento 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 representa a perda simultânea da administração de emissão, congelamento e KYC.

Os papéis de auditor têm dois problemas próprios. Primeiro, eles restringem os getters somente leitura das listas de conformidade, aparentemente para controlar quem pode lê-las, mas em uma cadeia pública isso é inútil: o armazenamento subjacente é legível por qualquer pessoa (foi lendo slots de armazenamento que mapeamos este sistema), então 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 cadeia 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, então as mesmas chaves que fazem mint, burn, congelam e colocam em blacklist também fazem parte do grupo destinado a revisar essas ações.

A revogação não é imediata. No mecanismo de cerimônia, o papel de um signatário é verificado uma única vez e a cota é decrementada; signatários anteriores nunca são revalidados, então revogar um papel posteriormente não retrai 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, o próprio rastro de auditoria do mecanismo não é confiável. Em cada aprovação, o evento evtApprove emite address(0) como o signatário, em vez do aprovador real; o signatário real sobrevive apenas no remetente da transação e em um registro interno, então os logs de eventos não conseguem atribuir quem aprovou uma solicitação. E a lista de solicitações ativas usa o nonce 0 tanto como um id real de solicitação quanto como seu marcador vazio, então ferramentas de monitoramento ou aprovação que percorrem a lista de trás para frente perdem a solicitação no nonce 0. Nenhum dos dois é crítico, mas para um sistema regulamentado que precisa de um rastro de auditoria limpo, ambos subtraem dessa qualidade.

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 em vez da origem dos fundos, então o código verifica a origem real from explicitamente em todos os modos; transfer não precisa fazer isso, porque ali msg.sender é a origem. Essa adaptação é razoável. Duas outras diferenças não são explicadas pelo iniciador, e elas deixam a mesma ação econômica governada por regras diferentes.

Primeiro, as listas de permissão de depósito e resgate são consultadas apenas no caminho de transferFrom, onde a pertença dispara um retorno antecipado que contorna a verificação de isActive (KYC). transfer nunca as consulta. Se um destinatário está na lista de permissão não tem nada a ver com quem iniciou a transferência, então o mesmo destinatário está sujeito a KYC via transfer, mas pode contorná-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 entre as 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 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 raciocinar e, dependendo da configuração, evitáveis.

1.4 Sinais de uma build pré-produção

Além dos bugs de lógica específicos, várias propriedades da base de código indicam que uma build pré-produção foi implantada na mainnet.

Registro de depuração em produção. Chamadas hardhat/console.log permanecem em todo o código, inclusive dentro do fallback do proxy, que é executado em cada transação de usuário. Como o próprio 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 de 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 cair no fallback para a implementação. Este contrato deliberadamente faz o contrário: a proteção na linha 117 está comentada, e o admin de fato cai no fallback. 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 evita problemas apenas porque cada módulo mantém sua própria cópia; um segundo nome de papel (AFL_TOKEN_HOLDER_AUDITOR_ROLE) é trocado da mesma forma.

Outros sinais. O pacote de verificação do proxy enviou 117 arquivos, incluindo o conjunto de testes do projeto, ao explorador público, o que entrega a um leitor os testes internos e casos extremos. O upgrade mais recente alterou apenas a limpeza de avisos do compilador, sem nenhuma auditoria de terceiros envolvida. E o optimizer está configurado com zero execuções, o que torna os caminhos mais usados de um token muito utilizado mais caros em vez de mais baratos.

Nenhum desses pontos é, isoladamente, 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. Ele 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 foi feito em duas transações em blocos adjacentes, com cerca de doze segundos de diferença:

  • a solicitação, papel A, a partir de 0xa9315aadc89ba681f1fe5df3375ab1a87d37eca2, no bloco 25500519 (tx 0x742372…85136);
  • a aprovação, papel B, a partir de 0x3795300b31429f9d37b0dc805528d9390ce87c50, no bloco 25500520 (tx 0xa7a400…630c), que atingiu o quórum e realizou o upgrade na mesma transação.

Dois pontos se destacam. Primeiro, toda a cerimônia de duas assinaturas foi concluída dentro de um único intervalo de bloco. Da solicitação até a execução houve apenas um bloco de distância; não houve nenhuma janela em que o segundo signatário pudesse revisar de forma independente antes que a mudança entrasse em vigor.

Segundo, nenhum dos dois signatários é identificável a partir do registro de papéis atual. Os papéis foram, desde então, rotacionados: 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 mais nenhum papel. Ler o registro em seu estado atual não diria quem autorizou o upgrade; apenas o histórico de transações mostra isso. Essa é a propriedade de "a revogação não é imediata" vista do outro lado: os papéis se movem, então uma fotografia de quem detém o que não é um registro de quem fez o que.

E o upgrade não corrigiu nenhum dos defeitos desta análise. A implementação que ele instalou, 0xe42d38b0…, é aquela que toda a Parte 1 descreve. Não podemos determinar a partir da cadeia 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: Ela está de acordo com o arcabouço de stablecoin de Hong Kong?

A Ordenança de Stablecoins de Hong Kong entrou em vigor em 1º de agosto de 2025, e os emissores licenciados são supervisionados sob a Diretriz da HKMA sobre Supervisão de Emissores Licenciados de Stablecoins. Comparamos apenas as cláusulas que um contrato inteligente pode satisfazer por conta própria; respaldo de reservas, custódia e cerimônias de chaves fora da cadeia estão fora do escopo de uma análise on-chain. Para cada cláusula abaixo, indicamos o que a diretriz exige, o que o contrato faz e onde os dois divergem. A comparação é resumida aqui e detalhada nas seções seguintes.

Cláusula da HKMA O que exige Onde o contrato diverge Veredito
6.5.3 Operações de alto risco não devem ser unilaterais (multiassinatura, e mitigantes como limites de velocidade ou timelocks) mint, burn, pause e freeze cada um executado com uma única chave; nenhum 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 mudança de código; correta, 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 indicam Diverge
Eficácia dos controles de conformidade os controles de blacklist, congelamento, lista de permissão e KYC devem ser eficazes a restriçã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 respaldadas e reconciliáveis a destruição emite Transfer para address(this) em vez de address(0) e deixa intocados os contadores de emissão líquida; a oferta reconstruída a partir de eventos diverge 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 para que nenhuma parte isolada possa realizá-las unilateralmente, por exemplo por meio de um protocolo de múltiplas assinaturas, e a diretriz lista mitigantes adicionais, como limites de velocidade e controles com atraso temporal (timelock).

O que o contrato faz. Lido a partir do authorizationMatrix do contrato de governança (a matriz completa está na tabela da seção 1.2), as operações de fornecimento 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. Mint, burn, pause e freeze podem, cada um, ser executados por uma única chave. Isso não atende ao requisito de "nenhuma parte unilateral". Também não há timelock: como mostrado na seção 1.2, uma vez atingido o quórum, a ação é executada na mesma transação, então um atraso temporal, um dos mitigantes citados pela diretriz, também está ausente. O contrato implementa, sim, um limite de velocidade de fornecimento (whenWithinRiskThresholds), então esse mitigante está presente, mas ele não substitui o controle de multiassinatura sobre as próprias operações.

Parágrafo 6.5.4: segregação de funções e revogação imediata

O que exige. Diferentes operações devem ser segregadas entre diferentes pessoas autorizadas, e a autoridade de uma pessoa autorizada deve ser revogável imediatamente.

O que o contrato faz. Uma única conta de propriedade externa detém seis papéis (emissão, congelamento, administração de KYC e todos os quatro papéis de auditor), então os papéis de execução e auditoria se sobrepõem. As próprias mudanças de papel também exigem apenas papel A mais papel B (ver 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 posteriormente revogado.

Onde diverge. As funções estão concentradas em vez de 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 cada mudança de código; correta, consistente, sem vulnerabilidades

O que exige. Um terceiro qualificado deve auditar os contratos inteligentes em cada mudança de código, e confirmar que eles sã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 é aquela instalada pelo upgrade de 10 de julho (rastreado na seção 1.5), e os defeitos da Parte 1 estão ativos nela.

Onde diverge. Não podemos ver fora da cadeia 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, congelamento, lista de permissão, KYC) presume que esses controles são eficazes.

O que o contrato faz. Como mostrado em 1.1, a restrição de KYC e a revogação de provedor não são funcionais no código implantado.

Onde diverge. Um controle que é exigido para funcionar não funciona. Essa é uma lacuna substantiva, não uma formalidade.

Parágrafo 2.2.3: moedas congeladas ou destruídas permanecem totalmente respaldadas e reconciliáveis

O que exige. Stablecoins congeladas ou destruídas por ação de fiscalização devem permanecer totalmente respaldadas, de modo que oferta e reservas possam ser reconciliadas.

O que o contrato faz. O fluxo de remediação usual para uma stablecoin regulamentada é queimar os fundos em um endereço malicioso e, mais tarde, reemitir um valor equivalente à vítima como um mint separado (é assim que funciona a combinação destroyBlackFunds mais issue da USDT). O destroy da HKDAP reduz _totalSupply, o que é um burn, mas nunca credita balances[address(this)], emite uma Transfer para address(this) em vez de address(0), e deixa intocados os contadores de emissão líquida usados pelo limite de mint:

// 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 é, de fato, mantido ali (balances[address(this)] permanece em zero). Um indexador creditaria a address(this) tokens que ele não possui, e a oferta total reconstruída a partir dos eventos não corresponderia à cadeia. Isso também confunde a própria remediação: como os tokens são queimados em vez de guardados, uma reemissão para a vítima deve ser um mint novo, mas a Transfer(..., address(this), ...) enganosa sugere que o contrato agora os custodia e poderia transferi-los, o que ele não pode fazer. Um burn limpo para address(0) mais uma reemissão separada seria tanto correto quanto reconciliável. Como está, a contabilidade on-chain de que depende uma reconciliação de reservas diverge do verdadeiro estado da cadeia. Isso é uma preocupação, e não uma aprovação limpa.

Limitamos essas descobertas ao que a cadeia mostra. Se as reservas estão totalmente respaldadas, se as chaves ficam em um HSM ou em um ambiente isolado (air-gapped), e se as transações são simuladas fora da cadeia antes de serem assinadas, não são coisas visíveis a partir do contrato, e não fazemos nenhuma alegação sobre elas.

Conclusão

O quadro é consistente nos dois eixos desta análise. Como software, a HKDAP contém defeitos funcionais, incluindo controles de conformidade que não executam como especificado, e apresenta vários sinais de uma build pré-produção. Como stablecoin regulamentada, várias de suas propriedades on-chain conflitam com cláusulas específicas da diretriz da HKMA. Com base na evidência on-chain, e deixando de lado as questões fora da cadeia que não conseguimos observar, o contrato implantado ainda não atende ao padrão que uma stablecoin comercial deveria atender.

Duas observações decorrem disso, e vale a pena declará-las claramente.

Primeiro, emitir em uma cadeia pública muda onde a conformidade é decidida. Um requisito como "nenhuma parte isolada deveria poder 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, que efetivamente determina se tal requisito é cumprido ou não, e qualquer pessoa pode verificar qual dos dois.

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 uma alegação sobre dólares de Hong Kong. Deveria, portanto, ser submetido a padrões de produção independentemente de como é rotulado.

Um fio condutor comum atravessa as descobertas específicas. A arquitetura reinventa, de forma customizada, primitivas que o ecossistema já oferece e já auditou em escala: um mecanismo de aprovação e uma camada de papéis feitos do zero, quando um multisig Safe com o AccessManager e o TimelockController da OpenZeppelin serviriam; 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 está nessa engenharia customizada, e não nas partes que reutilizam componentes padrão. Parece um sistema abstraído da forma como softwares de propósito geral costumam ser, em vez de composto a partir dos pequenos blocos de construção auditados que o desenvolvimento on-chain favorece, onde cada camada de abstração customizada 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 acompanhado das coisas que este sistema atualmente carece: uma janela de deliberação garantida por um timelock, e papéis nomeados e legíveis.

Os problemas descritos aqui são solucionáveis. Restaurar a multiassinatura 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 uma auditoria de terceiros antes de cada upgrade resolveriam a maioria deles. Implantar na mainnet com código-fonte verificado também é o que tornou esta análise possível, e é a postura padrão correta. Uma análise contínua de segurança e conformidade on-chain desse tipo é o que fazemos na BlockSec, e temos prazer em ajudar.

Get Real-Time Protection with Phalcon Security

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

phalcon security