Um Hashed Timelock Contract (HTLC) é um tipo de contrato inteligente que permite que um valor (como um token) seja bloqueado por um período fixo (ou um número de blocos). Enquanto o valor estiver bloqueado, ele só pode ser transferido para um destinatário pretendido - desde que o segredo correto (pré-imagem de hash) seja fornecido. Isso permite que a transferência de valor ocorra apenas condicionalmente - ou seja, somente se o proprietário do token decidir revelar o segredo. Este artigo mostrará uma possível implementação de um HTLC no Solidity.
Atomic Swaps entre cadeias
Um caso de uso típico de HTLC é a entrega (atômica) de um ativo em uma blockchain, em troca do pagamento de outro ativo em uma blockchain diferente. Vamos supor que Alice deseja enviar algum token (token 1) para Bob na cadeia A, e Bob deseja pagar a Alice por isso usando algum token (token 2) na cadeia B. Os termos da troca já foram acordados off-chain. O “caminho feliz” – ou seja, quando nenhuma das partes desiste da negociação antes que a janela de tempo termine – é o seguinte:
- Alice gera uma pré-imagem aleatória (secreta), faz um hash e usa o hash para bloquear seu token na cadeia A. O token só pode ser reivindicado por Bob, desde que ele use a pré-imagem correta do hash e o faça antes que o período de bloqueio tenha decorrido.
- O hash agora está visível para Bob por meio do HTLC na cadeia A, então ele o usa para bloquear seu token em um HTLC na cadeia B.
- Alice é então capaz de reivindicar o token bloqueado na cadeia B usando sua pré-imagem.
- Como a pré-imagem acabou de ser revelada na cadeia por Alice, Bob a usa para reivindicar o token na cadeia A.
Se os tokens não forem reclamados em um HTLC após o término da janela de tempo, o remetente terá direito a um reembolso e o destinatário não poderá mais reivindicar o token.
Escrevendo em Solidity
Para escrever uma implementação do Solidity, precisamos escolher que tipo de valor pode ser bloqueado no HTLC. Para fins gerais e sem complicar demais o(s) contrato(s), este exemplo dará suporte a qualquer token compatível com ERC-20. A opção mais simples seria usar apenas o ether propriamente dito, mas isso torna suas aplicações potenciais bastante limitadas. O suporte a qualquer token ERC-20 é direto devido à simplicidade da interface e isso se generaliza para uma ampla gama de tokens possíveis, incluindo (wrapped) ether. Outras opções podem incluir tokens ERC-721 ou ERC-1155. Seria bem simples adaptar este exemplo para também suportar esses outros padrões de token.
Seria um desperdício e inviável incluir uma implementação HTLC em cada contrato ERC-20. Em vez disso, devemos criar um único HTLC, separado dos contratos ERC-20, que pode interagir com qualquer número de ERC-20s. Isso faz uso da capacidade de composição dos contratos Solidity.
Aqui está um diagrama de máquina de estado do HTLC e sua interação com um token ERC-20. As pré-condições necessárias de uma transição são indicadas entre colchetes e t
indica o tempo atual fornecido pela blockchain. O estado inicial é apontado por uma seta sem origem. Cada transição de estado é executada chamando um método (externo) no HTLC. O diagrama refere-se apenas aos estados pertinentes à transação Alice-Bob.
Inicialmente, Alice é a proprietária do token (desbloqueado) — conforme armazenado no contrato ERC-20. O bloqueio é obtido transferindo a propriedade do token de Alice para o próprio HTLC, evitando assim a transferência, exceto sob condições especificadas pelo HTLC. Bob pode reivindicar o token desde que forneça uma pré-imagem p
, de modo que h(p)
seja igual ao hash fornecido por Alice, onde h
é a função de hash (kekkack256
neste exemplo).
Aqui está uma visualização do armazenamento do HTLC:
pragma solidity >=0.8.0 <0.9.0;
contract HTLC {
struct Lock {
uint unlockTime;
uint amount;
address tokenAddress;
address senderAddress;
address receiverAddress;
}
mapping(bytes32 => Lock) public locks;
...
}
A estrutura Lock
(bloqueio) armazena todas as informações necessárias para bloquear um token específico, com exceção do hash. Podemos armazenar um conjunto arbitrário de locks
no mapeamento de locks
, cada um indexado pelo valor de hash. Para simplificar, optei por indexar pelo hash, mas para torná-lo mais genérico, um índice diferente poderia ser usado para que vários tokens pudessem ser bloqueados usando o mesmo hash.
As possíveis transições de estado são expostas através dos seguintes métodos:
contract HTLC {
...
function claim(bytes calldata preImage) external { ... }
function lock(
bytes32 hashValue,
uint unlockTime,
uint amount,
address tokenAddress,
address receiverAddress
) external { ... }
function retake(bytes32 hashValue) external { ... }
}
Novas entradas são inseridas nos locks
por meio do método de lock
. Este método reverte a transação se o valor do hash já tiver sido usado. Observe que o método lock
transfere os tokens do msg.sender
para o HTLC, portanto, o msg.sender
deve pré-aprovar o HTLC para realizar essa transferência em seu nome usando o método de aprovação ERC-20.
O método de claim
(reivindicação) permite que o msg.sender
receba um token bloqueado por hash do preImage
fornecido e verificando se ele existe em locks
. Se isso acontecer e o tempo de desbloqueio não tiver sido alcançado, o msg.sender
será verificado para ver se é igual ao the receiverAddress
, após o qual o token poderá ser transferido. Observe que a entrada em locks
deve ser excluída antes de transferir o token para evitar ataques de reentrada.
O método de retake
(retomada) funciona de forma semelhante ao claim
, mas não requer a pré-imagem.
Por favor, consulte https://github.com/hgrano/eth-htlc para mais detalhes sobre a implementação.
Este artigo foi escrito por Huw Grano e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.
Top comments (0)