How to Develop a Simple Solidity Escrow Smart Contracts Using Hardhat | by Agustinus Theodorus | Jun, 2022

In this tutorial, we’ll create a simple escrow smart contract, test it, and deploy it on a Testnet using Hardhat.

watercolor wallpaper vector created by Freepik

Suppose you’re relatively new to the blockchain, no worries. First, we’ll review some of the fundamentals of Solidity and Hardhat before programming our smart contract step-by-step. At the end of this tutorial, you should be able to recreate an escrow smart contract with Solidity and Hardhat. Let’s get started!

A smart contract is a simple program that executes transactions on a blockchain by following predefined rules set by the author. Ethereum’s smart contracts use a specified programming language, Solidity. Solidity is an object-oriented programming language built for running smart contracts on the Ethereum Virtual Machine (EVM), with syntax similar to other programming languages ​​C++, Python, and JavaScript.

Solidity compiles your smart contract into a sequence of bytecodes before deploying it in the Ethereum Virtual Machine. Each smart contract has its address. To call a specific function, you need an Application Binary Interface (ABI) to specify the function you want to execute and return a format you expect.

Creating smart contracts requires a development environment for testing and deploying the contract on the Testnet. There are a lot of alternatives to choose from, like Truffle and its Ganache suite or Remix, the Solidity IDE. But there is a third alternative, Hardhat.

Hardhat is a Solidity development environment built using Node.js. It first released its Beta version in 2019 and has grown ever since. With Hardhat, developers don’t need to leave the JavaScript and Node.js environment to develop smart contracts, like Truffle.

Testing smart contracts built with Hardhat is also easy since Hardhat has a plug-and-play environment and doesn’t require you to set up a personal Ethereum network to test your smart contracts. To connect to the smart contract, you use Ethers.js, and to test them, you can use well-known JavaScript testing libraries like Chai.

Installing Hardhat is simple. You’ll need to install npm and Node.js v12. Assuming you use Linux, you need to run the following commands:

sudo apt update curl -sL | sudo bash - sudo apt install nodejs

Then, to install npm, run the code below:

sudo apt install npm

After installing npm, you can install Hardhat. Hardhat is based on Node.js and can only be installed using npm. Create a new directory and initiate your Node.js project:

mkdir hardhat-example cd hardhat-example npm init -y

Then, install Hardhat as a dev dependency:

npm i --save-dev hardhat

To initiate a Hardhat project, you’ll need a hardhat.config.js file. You can autogenerate it using the command below:

npx hardhat

Create an empty hardhat.config.js. Your Hardhat environment is almost ready. You only need to install the other dependencies:

npm i --save-dev @nomiclabs/hardhat-ethers ethers chai

Hardhat uses Ethers.js to connect to the smart contract and Chai as the assertion library. Open your hardhat.config.js and add the code below:

And voila! You’ve created your Solidity development environment. The smart contract in this tutorial will use Solidity version 0.8.4.

In this example, you’ll make a simple escrow smart contract, similar to Tornado Cash. Each user who executes the smart contract will deposit several tokens to the smart contract, and the smart contract will return a hash. You can use the hash to withdraw the tokens into a different account.

Bootstrapping Your Smart Contract for Development

The tutorial will use Open Zeppelin smart contracts. Open Zeppelin provides a library of secure smart contracts vetted by the community. To use Open Zeppelin smart contracts, install their library in your project with npm:

npm i -S @openzeppelin/contracts

Open Zeppelin has implementation standards for both the ERC20 and ERC721 tokens. In this example, you’ll use the ERC20 standard.

Your smart contract will use the DAI cryptocurrency, but you must create a mocked DAI token to test your local node. We’ll create the smart contract template for the token and escrow smart contract.

First, make a new contracts directory and create a file named MockDaiToken.sol:

Then, create another file named Escrow.sol:

You can only use the MockDaiToken in local environments and testing environments. When testing on the Testnet, use the actual DAI token. The pragma Solidity version will be for Solidity versions 0.8.0 and up.

Escrow Deposit Function

Depositing your tokens into an escrow smart contract is simple. You’ll transfer your funds from your wallet to the smart contract’s wallet.

Every smart contract has a wallet where you can store your funds. But, you also need to map every deposit with a unique hash. You can store the map using the mapping type and add a deposit_count to count how many deposits you’ve entered using the uint type:

The deposit function will have two parameters, a transaction hash and transaction amount. The transaction hash will be generated from outside the function and inserted into the mapping along with the deposit amount. Because you will receive two parameters, you’ll have to validate them to ensure users don’t insert malicious inputs.

You can use the require method to validate these three conditions:

  1. Validate that the transaction hash is not empty
  2. Validate if the escrow amount is not equal to zero
  3. Validate if the transaction hash is not conflicting and isn’t already used

After the inputs are successfully validated, insert them into the mapping and increment the deposit count. Next, create a view function that generates a unique hash based on the sender’s address, deposit amount, and the existing number of deposits:

Creating a view function and calling it externally rather than internally within the deposit function will reduce the number of gas fees Your function will need to consume. Unlike the deposit function, view functions essentially just read the blockchain in its current state without changing it.

Withdrawing From The Escrow

To withdraw your funds from the escrow, you need to create a separate function that accepts the transaction hash parameter. The function assumes that you will withdraw the entirety of the deposited escrow and cannot be used for a partial withdrawal. You’ll need to validate two conditions:

  1. Validate if the transaction hash is not empty
  2. Validate if the mapping for the transaction hash exists

After which, you can transfer the funds to the sender’s address and set the mapped balance to zero:

Final Escrow Smart Contract File

If you’ve followed the tutorial correctly, your smart contract will look like the following:

Next, you’ll need to test your smart contract using Chai.

One of the biggest advantages of using Hardhat is how easy the testing suite is. If you’re already familiar with JavaScript tests, you can quickly adapt to Hardhat’s testing, especially if you use Chai regularly.

Configuring Your Tests

Before starting the tests and deploying the escrow smart contract, you need to initiate the MockDaiToken smart contract. The escrow smart contract has a dependency on the ERC20 token address:

Planning the Happy and Unhappy Tests

Software testing has something called a happy path and an unhappy path. The happy path is when you test the successful scenarios of the software, while the unhappy path is when you test each exception that can arise from the software.

There will be two functions that need to be tested, withdraw escrow and deposit escrow. Each function will have one happy path when the transaction succeeds. However, a good rule of thumb to determine the number of unhappy paths is to count the number of validations your parameter has to pass.

There will be two validations in the case of the withdrawal escrow function. Thus, it has two unhappy paths:

  1. Validating if transaction hash is empty.
  2. Validating if transaction hash exists in the mapping.

For the deposit escrow function, there will be three validations. But, depositing requires you to use a number of your tokens, and there is a possibility that you input more tokens in the amount parameter than you have. Therefore, you have to add one more validation test, so the function has four unhappy paths:

  1. Validating if transaction hash is empty.
  2. Validating if the deposit amount submitted is not zero.
  3. Validating if the transaction hash does not exist in the mapping.
  4. Validating if the sender has enough funds to deposit.

Testing the Deposit Function

You can write your unit tests after defining the happy and unhappy paths. First, write the happy path, which will be the easiest.

Happy Path

In the configuration stage, you have already defined an account for happy path and unhappy path tests, and you can use them accordingly:

The tests will use Ethers.js to interface with the smart contract and use Chai as an assertion library.

Unhappy Path

Next, increase your tests’ coverage by implementing the unhappy path. You need to test four unhappy paths:

  1. Validating if transaction hash is empty
  2. Validating if the deposit amount submitted is not zero
  3. Validating if the transaction hash does not exist in the mapping
  4. Validating if the sender has enough funds to deposit

To simulate an empty hash, you can use ethers.constants.HashZero:

Simulating a zero amount equates to ethers.utils.parseUnits("0"):

In the next test, you can use them happyPathAccount because you will be simulating if a transaction hash already exists inside the mapping:

Finally, even if everything passes, you still need to have a sufficient amount of allowance.

Testing the Withdrawal Function

Now, we’ll repeat it with the withdrawal function.

Happy Path

Before you can test the happy path of the withdrawal function, you need to call the deposit function too:

Unhappy Path

You need to test two unhappy paths for the withdrawal function:

  1. Validating if the transaction hash is empty
  2. Validating if the transaction hash exists in the mapping

Hardhat gives you a straightforward interface that you can use to deploy your smart contracts.

Configuring Your Hardhat Deployments

You can deploy your smart contract to any Ethereum Testnet, including the Ropsten, Kovan, Goerli, and Rinkeby testnets. Each Testnet has a different RPC connection, and you wouldn’t want to hardcode them one by one.

You can add the connection details inside the hardhat.config.js:

Ideally, you want to contain the RPC URL and the deployer private keys inside your environment variable. You can create a new Ethereum wallet with private keys.

In this example, you’ll deploy your smart contract in your local Testnet and the Rinkeby Testnet.
To access the environment variables in JavaScript, you can use the dotenv npm package to use a .env file instead of hardcoding them. Install dotenv with the command below:

npm i -S dotenv

dotenv is installed as a dependency and not as a dev dependency because you will use it outside the dev environments.

Creating Your Deployment Script

First, you need to define your deployment stage. Create a new .maintain directory and make a new deployment.js file:

You need to get the Hardhat parameters before deployment:

After retrieving your targeted network name and RPC URL, continue to deploy your smart contracts.

The MockDaiToken will only be if you are deploying to a local Testnet. Otherwise, you want to use the actual DAI token:

Next, deploy your escrow smart contract. The escrow smart contract accepts an ERC20 token address in its constructor. You’ll need to supply the DAITokenAddress for the target network:

Your deployment script is finished! You can deploy your escrow smart contract.

You can easily start a local Ethereum network by running the following code:

npx hardhat node

The command above will start a new Ethereum RPC server locally on port 8545. You can create a frontend app and connect to your local RPC server using Metamask.

Deploying your smart contracts locally or on a Testnet like Rinkeby is similar. You can define which network you want to deploy your smart contract to use the --network flag. If you want to deploy to the local network, the command is below:

npx hardhat run .maintain/deployment.js --network localhost

Otherwise, if you want to deploy on the Rinkeby Testnet:

npx hardhat run .maintain/deployment.js --network rinkeby

If everything is successful, it will return something like this:

> hardhat-article@1.0.0 deploy:rinkeby /home/user/hardhat
> npx hardhat run --network rinkeby .maintain/deployment.js
Deploying to network rinkeby
Contracts deployed!
Deployed Escrow Contract address 0xF6C2123ba061eE544A6363836155FD6af86D7C08

Congratulations, you have your escrow smart contract!

In this article, you learned how to use Hardhat to develop, test, and deploy an Ethereum smart contract. Next, you can go even deeper by learning to connect your frontend applications to the smart contract from the browser.

I hope you enjoyed this article! Be sure to leave a comment if you have any questions. Happy coding!

Leave a Comment