Integrating existing contracts with Hamsa Privacy
The steps to operate with privacy are:
The steps to operate with privacy are:
- Deploy the L2 nodes.
- Deploy Hamsa L2 contracts on the nodes.
- Adapt your use case’s contracts to call Hamsa contracts for DvP operations.
- Deploy the adapted contracts.
- Execute DvP operations on each node involved.
In this section we will focus primarily on steps 3 and 5, going through a minimal example which demonstrates how to adapt regular contracts for Hamsa’s privacy layer.
We will go through the following examples:
- The simplest delivery versus payment (DvP), an ERC-20 token swap.
- A DvP for paying one installment of a collateralized loan and releasing a partial collateral.
Then, we’ll go into details on the objects and calls involved. Lastly, we’ll talk about internode communication.
1. A simple DvP contract TokenSwap without privacy
TokenSwap without privacy// 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);
}
}
The contract above allows one party to swap ERC20 tokens with another in the same chain. Approvals are assumed to be granted, for simplicity.
2. Adapted TokenSwap contract for privacy
TokenSwap contract for privacy// 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);
}
}
The token swap becomes a request for burning your assets and minting the counterparty’s assets.
The swap would happen between L2 nodes and settle on the L1 after both parties call swap in their nodes, with a matching bundleHash and corresponding chunkHashes. For example, consider that Alice has 100 ALICE in her node and Bob has 50 BOB in his node. They have agreed to swap.
- Alice calls TokenSwapHamsa::swap(), requesting a burn of 100 ALICE and a mint of 50 BOB.
- On the other chain, Bob calls TokenSwapHamsa::swap(), requesting a burn of 50 BOB and a mint of 100 ALICE.
- The matching DvP is captured and settled on the L1.
Notice that the function called on both sides is the exact same and thus should be called with mirrored values on both sides, e.g:
- In Alice’s node: tokenA is ALICE, tokenB is BOB, amountA is 100, amountB is 50.
- In Bob’s node: tokenA is BOB, tokenB is ALICE, amountA is 50, amountB is 100.
Different use cases might require completely different functions in either side, which is fine as long as the DvP is matched correctly.
3. Paying a loan installment and releasing collateral
As a secondary example, this contract showcases a 3-party DvP where Alice pays Bob one installment of a collateralized loan and a Custodian releases the collateral back to Alice. The calls below happen each in a separate node.
Alice calls LoanPayerHamsa::payInstallment, burning payment tokens and receiving the collateral back.
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 calls LoanPaymentReceiverHamsa::receiveInstallment to receive the payment..
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);
}
}
The Custodian calls LoanCustodianHamsa::releaseCollateral to release the collateral back to 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.
ScheduleRequest2 object.This object is passed to the scheduleBurnMintAndGenerate function in Hamsa’s Escrow contract and holds data related to scheduled burn and mint requests.
Let’s go through each property of its structure:
struct ScheduleRequest2 {
uint256 index;
uint256 chunkHash;
uint256 bundleHash;
uint256 expireTime;
BurnRequest[] burnRequests;
MintRequest[] mintRequests;
BurnSettleRequest[] burnSettleRequests;
}
- index: The index of the request in a DvP. In the context of the previous example, if Alice started this DvP, her index would be 0 and Bob’s index would be 1. They can be submitted in any order. If there’s a third party, its index is 2.
- chunkHash: The unique identifier of the request in a DvP. Uses Poseidon. Can be generated ahead of time with a Poseidon library of your choice or during transaction execution if you calculate Poseidon hashes in Solidity, e.g:
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])});
}
The snipped above uses a Poseidon library to generate unique chunk and bundle hashes to be used in DvPs.
- bundleHash: The identifier of the DvP itself, directly tied to the chunk hashes involved.
- expireTime: The epoch time past which an unmatched DvP is reverted.
- burnRequests: The array of tokens to be burnt in this part of the 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.
- mintRequests: The array of tokens to be minted in this part of the DvP.
struct MintRequest {
address tokenScAddress;
uint256 tokenType;
address account;
uint256 amount;
}
5. Node communication
In order for DvPs to be matched, nodes are responsible for broadcasting DvP data to the counterparties. Each counterparty of a DvP must know their corresponding index and chunkHash, as well as the shared bundleHash and the correct amount of tokens to be minted or burnt.
The internode communication is currently not handled by Hamsa’s privacy layer and must be orchestrated by the parties involved. As shown below, nodes can, for example, expose HTTP APIs to receive the data they require to complete a DvP started by another node.
The method used for communication between involved parties can be whatever they prefer.
Though the communication happens off-chain, the system operates in a trustless environment. Participants are still certain that the DvP will settle atomically if and only if all counterparties have sent their transactions.
Updated 4 months ago
