As explained in , Hash Timer Locked Contract(HTLC) has been used for Atomic Swap and cross payment channels between different blockchains. BEP3 defines native transactions to support HTLC on Binance Chain and also proposes the standard infrastructure and procedure to use HTLC for inter-chain atomic swap to easily create and use pegged token. During the swap process, the related fund will be locked to a purely-code-controlled escrow account. A purely-code-controlled escrow account is a kind of account which is derived from a hard-coded string in binance chain protocol. This kind of account doesn't have its own private key and it's only controlled by code of the protocol. The code for calculating escrow account is the same that is used in :
The account for mainnet is: bnb1wxeplyw7x8aahy93w96yhwm7xcq3ke4f8ge93u and the account for testnet is: tbnb1wxeplyw7x8aahy93w96yhwm7xcq3ke4ffasp3d. Once the swap is claimed or refunded, the fund will be transferred from the purely-code-controlled escrow account to client accounts.
Commands
Hash Timer Locked Transfer
Hash Timer Locked Transfer (HTLT) is a new transaction type on Binance Chain, to serve as HTLC in the first step of Atomic Swap,
After the deposit, you may observe that the balance of sender is decreased. The amount in deposit transaction must be positive. Besides, you can query the swap by swapID and the in_amount must equal to the amount that you balance decreased.
Claim HTLT
Claim Hash Timer Locked Transfer is to claim the locked asset by showing the random number value that matches the hash. Each HTLT locked asset is guaranteed to be release once.
const client = new BncClient("https://testnet-dex.binance.org") const privateKey = crypto.getPrivateKeyFromMnemonic(mnemonic) client.setPrivateKey(privateKey) const swapID = "61daf59e977c5f718f5aaedeaf69ccbea1c376db5274a84bca88848696164ffe" // the ID of an existing swap const randomNumber = "e8eae926261ab77d018202434791a335249b470246a7b02e28c3b2fb6ffad8f3" // the random number generated in htlt const res = client.swap.claimHTLT(from, swapID, randomNumber)
const client = new BncClient("https://testnet-dex.binance.org") const privateKey = crypto.getPrivateKeyFromMnemonic(mnemonic) client.setPrivateKey(privateKey) const swapID = "61daf59e977c5f718f5aaedeaf69ccbea1c376db5274a84bca88848696164ffe" // the ID of an existing swap const res = client.swap.refundHTLT(from, swapID, randomNumber)
Common error:
Already complete
ERROR: {"codespace":8,"code":12,"abci_code":524300,"message":"Expected swap status is Open, actually it is Completed"}
Not expired
ERROR: {"codespace":8,"code":8,"abci_code":524296,"message":"Current block height is 40003412, the expire height (40013236) is still not reached"}
Query Atomic Swap
Query atomic swap allows you to search swap information by swapID
Query atomic swap ID allows you to search swap history of an recipient. As this is a heavy query interface, some public nodes might close this query interface.
Query atomic swap ID allows you to search swap history of an initiator. As this is a heavy query interface, some public nodes might close this query interface.
Its corresponding address on testnet is: tbnb1pk45lc2k7lmf0pnfa59l0uhwrvpk8shsema7gron Binance Chain and 0xD93395B2771914E1679155F3EA58C41d89D96098 on Ethereum testnet
Swap Tokens from Ethereum to Binance Chain
image-20190918193751444
1. Approve Swap Transaction
Function: Approve
Parameters:
_spender: address of the smartcontract, which is 0x12DCBf79BE178479870A473A99d91f535ed960AD
_value: approved amount, should be bumped by e^10
Note: Please approve more than 1 token. In the following example, 100 PPC token was approved:
2. Call HTLT function From Ethereum
Function: htlt
Parameters:
_randomNumberHash: SHA256(randomNumber||timestamp), randomNumber is 32-length random byte array
_timestamp: it should be about 10 mins span around current timestamp
_heightSpan: it's a customized filed for deputy operator. it should be more than 200 for this deputy.
_recipientAddr: deputy address on Ethereum, it's 0x1C002969Fe201975eD8F054916b071672326858e for this one
_bep2SenderAddr: omit this field with 0x0
_bep2RecipientAddr: Decode your testnet address from bech32 encoded to hex, for example: 0xc41f2a85e1d3629637de1222017dce46c6c8e4b9
_outAmount: approved amount, should be bumped by e^10
_bep2Amount: _outAmount * exchange rate, the default rate is 1
random_number_hash must equal to the randomNumberHash in client HTLT transaction on ethereum
to must equals to client wallet address
timestamp must equal to the timestamp in client HTLT transaction on ethereum
out_amount should be reasonable. Please note that the decimals of bep2 tokens is 8, the out_amount should be something around 10000000000:PPC, deputy will deduct some fees.
expire_height must not be passed and should be enough for send claim transaction
You can use this swapID for refund if the deputy doesn't send htlt transaction on ethereum with proper parameters.
2. Deputy Approve Tokens
You should see that Deputy has approve enough amount of tokens for atomic swap.
3. Deputy Send HTLT on Ethereum
4. Claim ERC20 Tokens on Ethereum
You should see that Deputy has already approved enough tokens and
_randomNumberHash must equal to the randomNumberHash in client HTLT transaction on Binance Chain
_recipientAddr must equal to client ethereum wallet address
_timestamp must equal to the timestamp in client HTLT transaction on Binance Chain
_outAmount should be reasonable. Please note that the decimals erc20 contract and deputy will deduct some fees.
_expireHeight must not be passed and should be enough for send claim transaction
Then, you can call the claim function:
Function: claim
Parameters:
_swapID: this has been obtained from event, you can also calculate it from calSwapID function in the contract. calSwapID(randomNumberHash, {deputy ethereum address}, {hex encoding client binance address})
_randomNumber: reveal your randomNumber
5. Deputy Claim on Binance Chain
6. Demo for Client APP: swap bep2 to erc20
const erc20ContractAddr = "0xd93395b2771914e1679155f3ea58c41d89d96098" const swapContractAddr = "0x12DCBf79BE178479870A473A99d91f535ed960AD" const deputyEthWalletAddr = "0x1C002969Fe201975eD8F054916b071672326858e" const deputyBNBWalletAddr = "tbnb1pk45lc2k7lmf0pnfa59l0uhwrvpk8shsema7gr" const clientEthWalletAddr = "0xfA5E36a04EeF3152092099F352DDbe88953bB540" const clientEthWalletKey = new Buffer("89A0F0E0732ACAA7AD37C9E6D7A9798ECCE6940C63FF0290A58B1C1C1697486A", "hex") const clientBnbWalletAddr = "tbnb17vwyu8npjj5pywh3keq2lm7d4v76n434pwd8av" const clientBnbWalletMnemonic = "lawsuit margin siege phrase fabric matrix like picnic day thrive correct velvet stool type broom upon flee fee ten senior install wrestle soap sick" const web3 = new Web3(new Web3.providers.HttpProvider("https://ropsten.infura.io/v3/1c5b38a27f92410cb5feb13b6efb2e14")) const bnbClient = new BncClient("https://testnet-dex.binance.org") await bnbClient.initChain() bnbClient.setPrivateKey(crypto.getPrivateKeyFromMnemonic(clientBnbWalletMnemonic)) bnbClient.useDefaultSigningDelegate() bnbClient.useDefaultBroadcastDelegate() const bnbRPC = new rpcClient("https://seed-pre-s3.binance.org", "testnet") const erc20Contract = new web3.eth.Contract([{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_evilUser","type":"address"}],"name":"addBlackList","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"unpause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"account","type":"address"}],"name":"isPauser","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_maker","type":"address"}],"name":"getBlackListStatus","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"paused","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"renouncePauser","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"who","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"account","type":"address"}],"name":"addPauser","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"pause","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isOwner","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"issue","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"amount","type":"uint256"}],"name":"redeem","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"isBlackListed","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_clearedUser","type":"address"}],"name":"removeBlackList","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_blackListedUser","type":"address"}],"name":"destroyBlackFunds","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_initialSupply","type":"uint256"},{"name":"_name","type":"string"},{"name":"_symbol","type":"string"},{"name":"_decimals","type":"uint8"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Issue","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"amount","type":"uint256"}],"name":"Redeem","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_blackListedUser","type":"address"},{"indexed":false,"name":"_balance","type":"uint256"}],"name":"DestroyedBlackFunds","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_user","type":"address"}],"name":"AddedBlackList","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_user","type":"address"}],"name":"RemovedBlackList","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"account","type":"address"}],"name":"Paused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"account","type":"address"}],"name":"Unpaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"previousOwner","type":"address"},{"indexed":true,"name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"account","type":"address"}],"name":"PauserAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"account","type":"address"}],"name":"PauserRemoved","type":"event"}],erc20ContractAddr) const swapContract = new web3.eth.Contract([{"constant":true,"inputs":[],"name":"ERC20ContractAddr","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_swapID","type":"bytes32"}],"name":"isSwapExist","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_swapID","type":"bytes32"}],"name":"refund","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_randomNumberHash","type":"bytes32"},{"name":"_swapSender","type":"address"},{"name":"_bep2SenderAddr","type":"bytes20"}],"name":"calSwapID","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"name":"_swapID","type":"bytes32"},{"name":"_randomNumber","type":"bytes32"}],"name":"claim","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_randomNumberHash","type":"bytes32"},{"name":"_timestamp","type":"uint64"},{"name":"_heightSpan","type":"uint256"},{"name":"_recipientAddr","type":"address"},{"name":"_bep2SenderAddr","type":"bytes20"},{"name":"_bep2RecipientAddr","type":"bytes20"},{"name":"_outAmount","type":"uint256"},{"name":"_bep2Amount","type":"uint256"}],"name":"htlt","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_swapID","type":"bytes32"}],"name":"claimable","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_swapID","type":"bytes32"}],"name":"refundable","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_swapID","type":"bytes32"}],"name":"queryOpenSwap","outputs":[{"name":"_randomNumberHash","type":"bytes32"},{"name":"_timestamp","type":"uint64"},{"name":"_expireHeight","type":"uint256"},{"name":"_outAmount","type":"uint256"},{"name":"_sender","type":"address"},{"name":"_recipient","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_erc20Contract","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_msgSender","type":"address"},{"indexed":true,"name":"_recipientAddr","type":"address"},{"indexed":true,"name":"_swapID","type":"bytes32"},{"indexed":false,"name":"_randomNumberHash","type":"bytes32"},{"indexed":false,"name":"_timestamp","type":"uint64"},{"indexed":false,"name":"_bep2Addr","type":"bytes20"},{"indexed":false,"name":"_expireHeight","type":"uint256"},{"indexed":false,"name":"_outAmount","type":"uint256"},{"indexed":false,"name":"_bep2Amount","type":"uint256"}],"name":"HTLT","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_msgSender","type":"address"},{"indexed":true,"name":"_recipientAddr","type":"address"},{"indexed":true,"name":"_swapID","type":"bytes32"},{"indexed":false,"name":"_randomNumberHash","type":"bytes32"}],"name":"Refunded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_msgSender","type":"address"},{"indexed":true,"name":"_recipientAddr","type":"address"},{"indexed":true,"name":"_swapID","type":"bytes32"},{"indexed":false,"name":"_randomNumberHash","type":"bytes32"},{"indexed":false,"name":"_randomNumber","type":"bytes32"}],"name":"Claimed","type":"event"}], swapContractAddr) //-------------------------------------------- //Step1 send htlt on Binance Chain //-------------------------------------------- const randomNumber = "e8eae926261ab77d018202434791a335249b470246a7b02e28c3b2fb6ffad8f3" const timestamp = Math.floor(Date.now()/1000) const randomNumberHash = calculateRandomNumberHash(randomNumber, timestamp).toString("hex") const heightSpan = 10000 const amount = [{ denom: "PPC-00A", amount: 100000000 }] const expectedIncome = "9999990000:PPC" //"9999990000:PPC", decimal is 10, deputy will deduct swap fee, the swap fee is 10000:PPC bnbClient.swap.HTLT(clientBnbWalletAddr, deputyBNBWalletAddr, clientEthWalletAddr, "", randomNumberHash, timestamp, amount, expectedIncome, heightSpan, true) await wait(1000) //---------------------------------------------------------------------------- //Step2 query swap created by deputy on Ethereum and verify swap parameters //---------------------------------------------------------------------------- const hexEncodingClientBNBaddr = '0x'+crypto.decodeAddress(clientBnbWalletAddr).toString("hex") const swapID = await swapContract.methods.calSwapID("0x"+randomNumberHash, deputyEthWalletAddr, hexEncodingClientBNBaddr).call() console.log(swapID) let openSwap = await swapContract.methods.queryOpenSwap(swapID).call() while (openSwap._randomNumberHash == '0x0000000000000000000000000000000000000000000000000000000000000000') { console.log("Waiting for the atomic swap created by deputy") await wait(5000) openSwap = await swapContract.methods.queryOpenSwap(swapID).call() } let ethBlock = await web3.eth.getBlock('latest') let ethLatestHeight = ethBlock.number expect(openSwap._randomNumberHash).toBe("0x"+randomNumberHash) expect(Number(openSwap._timestamp)).toBe(timestamp) expect(Number(openSwap._outAmount)).toBe(9999990000) expect(openSwap._recipient).toBe(clientEthWalletAddr) expect(Number(openSwap._expireHeight)).toBeGreaterThan(Number(ethLatestHeight)+20) //---------------------------------------------------------------------------- //Step3 claim on Ethereum //---------------------------------------------------------------------------- const claimData = swapContract.methods.claim(swapID, "0x"+randomNumber).encodeABI() let nonce = await web3.eth.getTransactionCount(clientEthWalletAddr, 'pending') let gasPrice = await web3.eth.getGasPrice() let gasLimit = 3000000 let rawTx = { nonce: web3.utils.toHex(nonce), gasPrice: web3.utils.toHex(gasPrice), gasLimit: web3.utils.toHex(gasLimit), to: swapContractAddr, value: '0x00', data: claimData } var ethereumjs = require('ethereumjs-tx') var signTx = new ethereumjs(rawTx) signTx.sign(clientEthWalletKey) var serializedTx = signTx.serialize(); web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex')).on('receipt', console.log) await wait(20000) //---------------------------------------------------------------------------- //If step2 or step3 are failed and the expire height on Binance Chain is passed, try to send refundHTLT transaction on Binance Chain //----------------------------------------------------------------------------
Swap between Several BEP2 tokens
image-20190918193422062
Swap between Several BEP2 tokens fails
image-20190918193518929
Deploy smart-contract which supports Atomic Peg Swap (APS), there is already for Ethereum
Deploy deputy process for handling swap activities by token owners, there is an existing open-source solution here:
Go to and approve some amount of tokens.
Example of approve 100 PPC on
Go to and call HTLT function
Example of htlt
Then, Deputy will send HTLT transaction
You can also get swapID by . It requires three parameters:
Example of claim tx on
Deputy will claim ERC20 tokens afterwards with
This is a javascript implementation for client app to swap to with deputy.
Please read this to generate a valid HTLT transaction. Please write down the randomNumber and randomNumberHash.
Example is
You should see that Deputy has sent the htlt afterwards
To get the swapID on Ethereum, you can check this 0xd3bacf63906af5459ead39f27cae189e2f3e76fda34523714a4c61d76c79ee4e is the swapID on Ethereum.
In its , you should see the swapID. Before calling claim function on ethereum, clients should verify the parameters in the HTLT event.
Example is
Claim HTLT transaction from Deputy is afterwards:
This is a javascript implementation of client app to swap to with deputy.