Integrando contratos com Hamsa Privacy
Este documento orienta os desenvolvedores sobre como integrar seus contratos inteligentes existentes com a camada de privacidade da Hamsa para executar operações de Entrega contra Pagamento (DvP) de forma privada. Abordaremos o processo de adaptação de seus contratos para interagir com os contratos da Hamsa, garantindo que as transações permaneçam confidenciais e seguras. Ao longo deste guia, você encontrará exemplos de código detalhados, desde uma simples troca de tokens até cenários mais complexos de pagamento de empréstimos, além de uma análise aprofundada dos objetos e da comunicação entre nós necessários para a liquidação de DvPs.
Passos para operar com privacidade
Os passos para operar com privacidade são:
- Implantar os nós L2.
- Implantar os contratos Hamsa L2 nos nós.
- Adaptar os contratos do seu caso de uso para chamar os contratos Hamsa em operações DvP.
- Implantar os contratos adaptados.
- Executar as operações DvP em cada nó envolvido.
Nesta seção, focaremos principalmente nos passos 3 e 5, apresentando um exemplo mínimo que demonstra como adaptar contratos regulares para a camada de privacidade do Hamsa.
Vamos passar pelos seguintes exemplos:
- O exemplo mais simples de entrega contra pagamento (DvP), uma troca de tokens ERC-20.
- Um DvP para o pagamento de uma parcela de um empréstimo com garantia e a liberação de uma parte da garantia.
Em seguida, entraremos em detalhes sobre os objetos e chamadas envolvidas. Por fim, falaremos sobre a comunicação entre nós.
1. Um contrato simples de DvP TokenSwap sem privacidade.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IERC20 {
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}
contract TokenSwap {
function swap(
address tokenA,
address tokenB,
address recipient,
uint256 amountA,
uint256 amountB
) external {
IERC20(tokenA).transferFrom(msg.sender, recipient, amountA);
IERC20(tokenB).transferFrom(recipient, msg.sender, amountB);
}
}
O contrato acima permite que uma parte troque tokens ERC20 com outra na mesma rede. Para simplificar, assume-se que as aprovações já foram concedidas.
2. TokenSwap adaptado para privacidade.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../ucl/IDvpEscrow.sol";
contract TokenSwapHamsa {
// Use the real address of the escrow in your L2 node.
address constant HAMSA_ESCROW_ADDRESS = 0x5Af0D9827E0c53E4799BB226655A1de152A425a5;
function swap(
address tokenA,
address tokenB,
address recipient,
uint256 amountA,
uint256 amountB,
DvpInfo calldata dvpInfo
) external {
BurnSettleRequest[] memory burnSettleRequests = new BurnSettleRequest[](1) ;
burnSettleRequests[0] = BurnSettleRequest({
tokenScAddress : tokenA,
tokenType : 0,
account : msg.sender,
amount : amountA,
toBankAddress : recipient
});
MintRequest[] memory mintRequests = new MintRequest[](1) ;
mintRequests[0] = MintRequest({
tokenScAddress : tokenB,
tokenType : 0,
account : msg.sender,
amount : amountB
});
ScheduleRequest2 memory request = ScheduleRequest2({
index : dvpInfo.index,
chunkHash : dvpInfo.chunkHash,
bundleHash : dvpInfo.bundleHash,
expireTime : dvpInfo.expireTime,
burnRequests : new BurnRequest[](0) ,
mintRequests : mintRequests,
burnSettleRequests : burnSettleRequests
});
IDvpEscrow(HAMSA_ESCROW_ADDRESS).scheduleBurnMintAndGenerate(request);
}
}
A troca de tokens torna-se uma solicitação de queima dos seus ativos e cunhagem dos ativos da contraparte.
A transação acontece entre nós L2 e será liquidada na L1 depois que ambas as partes chamam TokenSwapHamsa::swap() em seus respectivos nós, usando o mesmo bundleHash e os chunkHashes correspondentes. Por exemplo, suponha que Alice tenha 100 ALICE em seu nó e Bob tenha 50 BOB em seu nó. Eles concordaram em trocar.
- Alice chama
TokenSwapHamsa::swap(), solicitando a queima de 100 ALICE e a cunhagem de 50 BOB. - Bob, na outra cadeia, chama
TokenSwapHamsa::swap(), solicitando a queima de 50 BOB e a cunhagem de 100 ALICE.
O DvP correspondente é capturado e liquidado na L1.
Observe que a função chamada em ambos os lados é exatamente a mesma, devendo portanto ser invocada com valores “espelhados” em cada nó, por exemplo:
- No nó da Alice:
tokenA= ALICE,tokenB= BOB,amountA= 100,amountB= 50. - No nó do Bob:
tokenA= BOB,tokenB= ALICE,amountA= 50,amountB= 100.
Casos de uso diferentes podem exigir funções distintas em cada lado, o que é perfeitamente aceitável, desde que o DvP seja correspondido corretamente.
3. Pagamento de uma parcela de empréstimo e liberação de garantia
Como exemplo secundário, este contrato demonstra um DvP com 3 partes, no qual Alice paga a Bob uma parcela de um empréstimo com garantia, e um Custodiante libera a garantia de volta para Alice. As chamadas abaixo ocorrem em nós distintos.
Alice chama LoanPayerHamsa::payInstallment, queimando os tokens de pagamento e recebendo a garantia de volta.
contract LoanPayerHamsa {
address constant HAMSA_ESCROW_ADDRESS = 0x5Af0D9827E0c53E4799BB226655A1de152A425a5;
function payInstallment(
DvpInfo calldata dvpInfo
) external {
BurnSettleRequest[] memory burnSettleRequests = new BurnSettleRequest[](1) ;
burnSettleRequests[0] = BurnSettleRequest({
tokenScAddress : dvpInfo.paymentToken,
tokenType : 0,
account : msg.sender,
amount : dvpInfo.paymentAmount,
toBankAddress : dvpInfo.settleToAddress
});
MintRequest[] memory mintRequests = new MintRequest[](1) ;
mintRequests[0] = MintRequest({
tokenScAddress : dvpInfo.collateralToken,
tokenType : dvpInfo.collateralId,
account : msg.sender,
amount : dvpInfo.collateralToReceive
});
ScheduleRequest2 memory request = ScheduleRequest2({
index : dvpInfo.index,
chunkHash : dvpInfo.chunkHash,
bundleHash : dvpInfo.bundleHash,
expireTime : dvpInfo.expireTime,
burnRequests : new BurnRequest[](0) ,
mintRequests : mintRequests,
burnSettleRequests : burnSettleRequests
});
IDvpEscrow(HAMSA_ESCROW_ADDRESS).scheduleBurnMintAndGenerate(request);
}
}
Bob chama LoanPaymentReceiverHamsa::receiveInstallment para receber o pagamento.
contract LoanPaymentReceiverHamsa {
address constant HAMSA_ESCROW_ADDRESS = 0x5Af0D9827E0c53E4799BB226655A1de152A425a5;
function receiveInstallment(
DvpInfo calldata dvpInfo
) external {
MintRequest[] memory mintRequests = new MintRequest[](1) ;
mintRequests[0] = MintRequest({
tokenScAddress : dvpInfo.paymentToken,
tokenType : 0,
account : msg.sender,
amount : dvpInfo.paymentAmount
});
ScheduleRequest2 memory request = ScheduleRequest2({
index : dvpInfo.index,
chunkHash : dvpInfo.chunkHash,
bundleHash : dvpInfo.bundleHash,
expireTime : dvpInfo.expireTime,
burnRequests : new BurnRequest[](0) ,
mintRequests : mintRequests,
burnSettleRequests : new BurnSettleRequest[](0)
});
IDvpEscrow(HAMSA_ESCROW_ADDRESS).scheduleBurnMintAndGenerate(request);
}
}
O Custodiante chama LoanCustodianHamsa::releaseCollateral para liberar a garantia de volta para Alice.
contract LoanCustodianHamsa {
address constant HAMSA_ESCROW_ADDRESS = 0x5Af0D9827E0c53E4799BB226655A1de152A425a5;
function releaseCollateral(
DvpInfo calldata dvpInfo
) external {
BurnSettleRequest[] memory burnSettleRequests = new BurnSettleRequest[](1);
burnSettleRequests[0] = BurnSettleRequest({
tokenScAddress : dvpInfo.collateralToken,
tokenType : dvpInfo.collateralId,
account : dvpInfo.custodian,
amount : dvpInfo.collateralToReceive,
toBankAddress : dvpInfo.settleToAddress
});
ScheduleRequest2 memory request = ScheduleRequest2({
index : dvpInfo.index,
chunkHash : dvpInfo.chunkHash,
bundleHash : dvpInfo.bundleHash,
expireTime : dvpInfo.expireTime,
burnRequests : new BurnRequest[](0),
mintRequests : new MintRequest[](0),
burnSettleRequests : burnSettleRequests
});
IDvpEscrow(HAMSA_ESCROW_ADDRESS).scheduleBurnMintAndGenerate(request);
}
}
4. The ScheduleRequest2 object.
Este objeto é passado para a função scheduleBurnMintAndGenerate no contrato Escrow da Hamsa e contém os dados relacionados às solicitações agendadas de queima e cunhagem.
Vamos analisar cada propriedade da sua estrutura:
struct ScheduleRequest2 {
uint256 index;
uint256 chunkHash;
uint256 bundleHash;
uint256 expireTime;
BurnRequest[] burnRequests;
MintRequest[] mintRequests;
BurnSettleRequest[] burnSettleRequests;
}
index: O índice da solicitação em um DvP. No contexto do exemplo anterior, se Alice iniciou esse DvP, o índice dela seria 0 e o do Bob seria 1. As solicitações podem ser enviadas em qualquer ordem. Se houver uma terceira parte, o índice dela será 2.
chunkHash: O identificador único da solicitação em um DvP. Utiliza Poseidon. Pode ser gerado antecipadamente com uma biblioteca Poseidon de sua escolha ou durante a execução da transação, caso você calcule os hashes Poseidon em Solidity, por exemplo:
function generateHash2() public returns (Hashes2 memory){
uint256 c1 = vm.randomUint();
uint256 c2 = vm.randomUint();
return Hashes2({chunkHash1: c1, chunkHash2: c2, bundleHash: PoseidonT3.hash([c1, c2])});
}
O trecho acima utiliza uma biblioteca Poseidon para gerar hashes únicos de chunk e bundle a serem usados em DvPs.
- bundleHash: O identificador do próprio DvP, diretamente vinculado aos hashes de chunk envolvidos.
- expireTime: O timestamp em epoch após o qual um DvP não correspondido é revertido.
- burnRequests: O array de tokens a serem queimados nesta parte do DvP.
struct BurnRequest {
address tokenScAddress;
uint256 tokenType;
address account;
uint256 amount;
}
- tokenScAddress: O endereço do token.
- tokenType: O índice do token se for ERC-1155; 0 caso contrário.
- account: O endereço da conta de onde os tokens serão queimados.
- amount: A quantidade de tokens a queimar.
burnSettleRequests: Igual à estrutura anterior, mas será liquidada na L1 quando correspondida.
struct BurnSettleRequest {
address tokenScAddress;
uint256 tokenType;
address account;
uint256 amount;
address toBankAddress;
}
- O mesmo de burnRequest.
5. Comunicação entre nós
Para que os DvPs sejam finalizados, os nós são responsáveis por transmitir os dados do DvP às contrapartes. Cada contraparte de um DvP deve conhecer seu índice e chunkHash correspondente, assim como o bundleHash compartilhado e a quantidade correta de tokens a serem cunhados ou queimados.
A comunicação entre nós atualmente não é gerenciada pela camada de privacidade do Hamsa e deve ser orquestrada pelas partes envolvidas. Como mostrado abaixo, os nós podem, por exemplo, expor APIs HTTP para receber os dados necessários para completar um DvP iniciado por outro nó.
O método utilizado para a comunicação entre as partes envolvidas pode ser o que elas preferirem.
Embora a comunicação ocorra fora da blockchain, o sistema opera em um ambiente trustless. Os participantes têm a certeza de que o DvP será liquidado, se somente se todas as contrapartes tiverem enviado suas transações.
Updated 5 months ago
