How to Implement Hash Time-Locked Contract (HTLC) in XDC network?

Dolly Bhati
Yodaplus
Published in
7 min readNov 12, 2021

The blockchain world has been booming, paving the way for a new realm of innovations. There are many concepts that are becoming increasingly popular with the common masses. For instance, Smart Contracts, Asset Tokenization, Non-Fungible Tokens, etc. One of the concepts that we shall be discussing in the article ahead is — Hash Time-Locked Contract in XDC Network, a type of Smart Contract that has been gaining a lot of traction nowadays.

Let’s explore.

What is a Hash Time-Locked Contract?

A Hash Time-Locked Contract (HTLC) is a contract between two parties to exchange some crypto assets of value between two parties. As is suggested in the definition, these contracts are -

  • Hash locked — The sender creates a secret (preimage), and then creates a contract with the hash of the preimage. The receiver can only claim/withdraw the funds if she is aware of the preimage.
  • Time locked — The receiver has a pre-defined time limit to accept her claim. If not accepted the sender can get a refund of the exchanged crypto asset. The time lock can be set using blocks or seconds.
  • Hash Time-Locked

This facilitates the removal of any third parties for an exchange, and also facilitates exchange between two parties to swap two crypto assets using “atomic swap”.

We will explore how to implement a Hash Time locked contract for a particular type of crypto asset XRC20.
Following the same principle, similar HTLCs can be developed for an XRC721 token.

And finally, we will leave it as an exercise for you to develop an “atomic swap” by defining the steps required.

Implementation

An HTLC is an extension of crypto assets, which can be native XDC, an XRC20 token, or an XRC721 token. For our example, we will extend an XRC20 token, to see the additional functions required for an XRC20 token to support HTLC.

pragma solidity ^0.5.3;import "./XRC20.sol";/*** @title Hashed Timelock Contracts (HTLCs) on XDC XRC20 tokens.* This contract provides a way to create and keep HTLCs for XRC20 tokens.* Protocol:**  1) newContract(receiver, hashlock, timelock, tokenContract, amount) - a*      sender calls this to create a new HTLC on a given token (tokenContract)*       for a given amount. A 32-byte contract id is returned*  2) withdraw(contractId, preimage) - once the receiver knows the preimage of*      the hash lock hash they can claim the tokens with this function*  3) refund(contractId) - after timelock has expired and if the receiver did not*      withdraw the tokens the sender/creator of the HTLC can get their tokens*      back with this function.*/

As is explained early

Step 1 — Bob creates an HTLC using the newContract method, wherein he uses a hash of a preimage. A preimage is a SHA256 hash of saying a phrase “Alice grab your money in one hour”.

STEP 2 — Alice is informed of the newContract which has returned the contractID, and the preimage (it can be an offchain exercise in case of one-way transfer; in case of an “Atomic swap” it is managed by the Swap Smart Contract).

STEP 3- Alice uses the withdraw function using the contractID and the preimage, within an hour, to claim her XRC20s.

STEP 4 — If Alice fails to withdraw, Bob can get his XRC20 tokens by using the refund method.

Detailed code follows -

contract HashedTimelockXRC20 {event HTLCXRC20New(bytes32 indexed contractId,address indexed sender,address indexed receiver,address tokenContract,uint256 amount,bytes32 hashlock,uint256 timelock);event HTLCXRC20Withdraw(bytes32 indexed contractId);event HTLCXRC20Refund(bytes32 indexed contractId);struct LockContract {address sender;address receiver;address tokenContract;uint256 amount;bytes32 hashlock;// locked UNTIL this time in seconds.uint256 timelock;bool withdrawn;bool refunded;bytes32 preimage;}modifier tokensTransferable(address _token, address _sender, uint256 _amount) {require(_amount > 0, "token amount must be > 0");require(XRC20(_token).allowance(_sender, address(this)) >= _amount,"token allowance must be >= amount");_;}modifier futureTimelock(uint256 _time) {// only requirement is the timelock time is after the last blocktime (now).// probably want something a bit further in the future then this.// but this is still a useful sanity check:require(_time > now, "timelock time must be in the future");_;}modifier contractExists(bytes32 _contractId) {require(haveContract(_contractId), "contractId does not exist");_;}modifier hashlockMatches(bytes32 _contractId, bytes32 _x) {require(contracts[_contractId].hashlock == sha256(abi.encodePacked(_x)),"hashlock hash does not match");_;}modifier withdrawable(bytes32 _contractId) {require(contracts[_contractId].receiver == msg.sender, "withdrawable: not receiver");require(contracts[_contractId].withdrawn == false, "withdrawable: already withdrawn");// This check needs to be added if claims are allowed after timeout. That is, if the following timelock check is commented outrequire(contracts[_contractId].refunded == false, "withdrawable: already refunded");// if we want to disallow claim to be made after the timeout, uncomment the following line// require(contracts[_contractId].timelock > now, "withdrawable: timelock time must be in the future");_;}modifier refundable(bytes32 _contractId) {require(contracts[_contractId].sender == msg.sender, "refundable: not sender");require(contracts[_contractId].refunded == false, "refundable: already refunded");require(contracts[_contractId].withdrawn == false, "refundable: already withdrawn");require(contracts[_contractId].timelock <= now, "refundable: timelock not yet passed");_;}mapping (bytes32 => LockContract) contracts;/*** @dev Sender sets up a new hash time lock contract depositing the* funds and providing the receiver and terms.** NOTE: _sender must first call approve() on the token contract.*       See allowance check-in tokensTransferable modifier.* @param _receiver receiver of the tokens.* @param _hashlock A sha-2 sha256 hash hashlock.* @param _timelock UNIX epoch seconds time that the lock expires at.*                  Refunds can be made after this time.* @param _tokenContract XRC20 Token contract address.* @param _amount Amount of the token to lock up.* @return contractId Id of the new HTLC. This is needed for subsequent*                    calls.*/function newContract(address _receiver,bytes32 _hashlock,uint256 _timelock,address _tokenContract,uint256 _amount)externaltokensTransferable(_tokenContract, msg.sender, _amount)futureTimelock(_timelock)returns (bytes32 contractId){contractId = sha256(abi.encodePacked(msg.sender,_receiver,_tokenContract,_amount,_hashlock,_timelock));// Reject if a contract already exists with the same parameters. The// sender must change one of these parameters (ideally providing a// different _hashlock).if (haveContract(contractId))revert("Contract already exists");// This contract becomes the temporary owner of the tokensif (!XRC20(_tokenContract).transferFrom(msg.sender, address(this), _amount))revert("transferFrom sender to this failed");contracts[contractId] = LockContract(msg.sender,_receiver,_tokenContract,_amount,_hashlock,_timelock,false,false,0x0);emit HTLCXRC20New(contractId,msg.sender,_receiver,_tokenContract,_amount,_hashlock,_timelock);}/*** @dev Called by the receiver once they know the preimage of the hashlock.* This will transfer ownership of the locked tokens to their address.** @param _contractId Id of the HTLC.* @param _preimage sha256(_preimage) should equal the contract hashlock.* @return bool true on success*/function withdraw(bytes32 _contractId, bytes32 _preimage)externalcontractExists(_contractId)hashlockMatches(_contractId, _preimage)withdrawable(_contractId)returns (bool){LockContract storage c = contracts[_contractId];c.preimage = _preimage;c.withdrawn = true;XRC20(c.tokenContract).transfer(c.receiver, c.amount);emit HTLCXRC20Withdraw(_contractId);return true;}/*** @dev Called by the sender if there was no withdraw AND the time lock has* expired. This will restore ownership of the tokens to the sender.** @param _contractId Id of HTLC to refund from.* @return bool true on success*/function refund(bytes32 _contractId)externalcontractExists(_contractId)refundable(_contractId)returns (bool){LockContract storage c = contracts[_contractId];c.refunded = true;XRC20(c.tokenContract).transfer(c.sender, c.amount);emit HTLCXRC20Refund(_contractId);return true;}/*** @dev Get contract details.* @param _contractId HTLC contract id* @return All parameters in struct LockContract for _contractId HTLC*/function getContract(bytes32 _contractId)publicviewreturns (address sender,address receiver,address tokenContract,uint256 amount,bytes32 hashlock,uint256 timelock,bool withdrawn,bool refunded,bytes32 preimage){if (haveContract(_contractId) == false)return (address(0), address(0), address(0), 0, 0, 0, false, false, 0);LockContract storage c = contracts[_contractId];return (c.sender,c.receiver,c.tokenContract,c.amount,c.hashlock,c.timelock,c.withdrawn,c.refunded,c.preimage);}/*** @dev Is there a contract with id _contractId.* @param _contractId Id into contracts mapping.*/function haveContract(bytes32 _contractId)internalviewreturns (bool exists){exists = (contracts[_contractId].sender != address(0));}}

Try it in the remix — https://remix.xinfin.network/.

https://github.com/yodaplus/HashedTimeLockXRC20

Now, as we have seen the XRC20 implementation, you are encouraged to develop a similar HTLC for XRC721.

Once, both the HTLC contracts are ready, you can setup an “atomic swap” smart contract.
The flow should be as follows -

Contract: HashedTimelock swap between ERC721 token and ERC20 token (Delivery vs. Payment)

✓ Step 1: Alice sets up a swap with Bob to transfer the Commodity token

✓ Step 2: Bob sets up a swap with Alice in the payment contract

✓ Step 3: Alice as the initiator withdraws from the BobERC721 with the secret

✓ Step 4: Bob as the counterparty withdraws from the AliceERC721 with the secret learned from Alice’s withdrawal.

Test the refund scenario.

Conclusion

We have shown you an example of how to extend an XRC20 token to implement HTLC and defined a flow for “atomic swap” as it can be used for the Delivery vs. Payment scenario. And, in the end, we will leave you with a question –

Can Alice as the initiator has an upper hand and create a scenario where she gets Bob’s token, but Bob is not able to get his? And, how to avoid such a scenario in your next “Atomic swap” smart contract?

--

--

Dolly Bhati
Yodaplus

A technophile with a soul of travel yogi — writing experience in blockchain, cryptocurrency, dApps, software development, yoga, etc.