EIP-712 V4: How to Run a Test with Hardhat

Aristoteles Joel Nici
carpediem-tech
Published in
5 min readMay 30, 2023

Hardhat has become an indispensable tool for many blockchain developers, thanks to its flexibility, power, and ease of use.

One of the most important aspects of smart contract development is the ability to effectively test them, and Hardhat offers some exceptional functionalities in this regard.

In this article, we will explore how to use Hardhat to test one of Ethereum’s security standards, the EIP-712 V4.

What is Hardhat?

Hardhat is an Ethereum development environment designed to facilitate the creation, compilation, deployment, and testing of smart contracts. It has the advantage of offering a highly customizable development experience, allowing developers to configure the environment to meet their specific needs.

Hardhat provides a number of key features, including:

  • Hardhat Network: A local Ethereum node for development that supports instant mining, automatic block creation, EVM transaction execution, and transaction debugging.
  • Advanced Solidity Compiler: Allows for incremental compilation for a fast development-feedback cycle.
  • Plugin System: Allows integration with other major blockchain technologies such as ethers.js, truffle, typechain, and many others.
  • Stack Traces: Hardhat is known for its detailed and accurate stack traces that simplify smart contract debugging.

Testing with Hardhat

One of the main advantages of using Hardhat is its powerful testing suite.

Hardhat uses the standard Node.js assertion library, making learning to test with Hardhat easier for those with JavaScript testing experience.

Hardhat supports both unit tests and integration tests. This means you can test individual functions of your contracts (unit tests), as well as the interaction between multiple contracts and/or functions (integration tests).

Before we start looking at the code, it’s recommended to read the documentation, which in my opinion is very clear and simple, on how to use Hardhat so you can write and test our smart contract.

The documentation can be found at this link:

https://hardhat.org/docs.

Smart contract

Let’s quickly take a look at the smart contract to be tested:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.9;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
import "hardhat/console.sol";

contract MyContract is EIP712 {

mapping(address => uint256) private _nonces;

struct MyMessage {
address sender;
address receiver;
uint256 amount;
uint256 nonce;
bytes signature;
}

bytes32 private constant _MESSAGE_TYPEHASH = keccak256("Message(address sender,address receiver,uint256 amount,uint256 chainId,uint256 nonce)");

constructor( ) EIP712("MyContract","1") { }

function transfer(MyMessage calldata message) public {

(bytes32 r, bytes32 s, uint8 v) = splitSignature(message.signature);
bytes32 structHash = keccak256(abi.encode(_MESSAGE_TYPEHASH,message.sender,message.receiver,message.amount,block.chainid,message.nonce));
address signer = ECDSA.recover(_hashTypedDataV4(structHash), v, r, s);
require(signer == message.sender, "invalid signature");

_nonces[msg.sender] += 1;

//Transfer logic
}

function getNonce() public view returns (uint256) {
return _nonces[msg.sender];
}

function splitSignature(bytes memory signature) internal virtual returns (bytes32 r, bytes32 s, uint8 v) {
require(signature.length == 65, "invalid signature length");

assembly {
// first 32 bytes, after the length prefix
r := mload(add(signature, 32))
// second 32 bytes
s := mload(add(signature, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(signature, 96)))
}
}
}

This smart contract is the same one used as an example in another article, where we analyze it in detail.

If interested, you can find all the information at this link:

https://medium.com/p/1e77de87eb09

Test

Now, if you’ve followed the Hardhat guide well, let’s create a new file “MyContract.ts” in the “test” folder.

Inside this file we can copy this code:

import { ethers } from "hardhat";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { BigNumberish } from 'ethers';
import { hexlify } from 'ethers/lib/utils';

describe("MyContract", function () {
async function getSignaturer(
sender: SignerWithAddress,
receiver: string,
amount: BigNumberish,
nonce: BigNumberish,
myContractAddress: string,
manualChainId: BigNumberish,
permitConfig?: { name?: string; chainId?: number; version?: string })
{
const [name, version, chainId] = await Promise.all([
permitConfig?.name ?? "MyContract",
permitConfig?.version ?? '1',
permitConfig?.chainId ?? sender.getChainId(),
])

var result = await sender._signTypedData(
{
name,
version,
chainId,
verifyingContract: myContractAddress,
},
{
Message: [
{ name: 'sender', type: 'address' },
{ name: 'receiver', type: 'address' },
{ name: 'amount', type: 'uint256' },
{ name: 'chainId', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
]
},
{
sender: sender.address,
receiver: receiver,
amount: amount,
chainId: manualChainId,
nonce,
}
)

return hexlify(result);
}

describe("Transfer", function () {
it("Test", async function () {
var wallet: SignerWithAddress[] = await ethers.getSigners();

const MyContract = await ethers.getContractFactory("MyContract");
const myContract = await MyContract.deploy();
await myContract.deployed();

const Token = await ethers.getContractFactory("GenericERC20");
var token = await Token.deploy("Token", "TKN");
await token.deployed();

var nonce = await myContract.connect(wallet[0]).getNonce();

var sign = await getSignaturer(wallet[0], wallet[1].address, 100, nonce, myContract.address, 31337);

const amount = ethers.utils.parseUnits("100.0", 6);

var message = {
sender: wallet[0].address,
receiver: wallet[1].address,
amount: 100,
nonce: 0,
signature: sign,
}

var tx = await myContract.connect(wallet[0]).transfer(message);

console.log(tx);
})
})
});

The test code includes a snippet of JavaScript code that imports the necessary libraries. We use ethers to interact with the Ethereum blockchain and hardhat-ethers for handling signers (SignerWithAddress).

The first significant piece of code is a getSignaturer function. This function is used to generate a digital signature for a message. The message includes a sender, a recipient, an amount, a nonce, the contract address, and a chain ID. Moreover, this function supports customized permit configuration through permitConfig.

The actual test is the describe(“Transfer”) block. In this block, we define a series of actions.

Firstly, we retrieve the signers from the blockchain using ethers.getSigners() and save the result in wallet.

Then, we get the “MyContract” and deploy it on the blockchain. We also get a generic ERC20 token called “Token” and deploy this as well.

Next, we retrieve the nonce for our contract and generate a signature for a message with getSignaturer. This message represents a token transfer from wallet[0] to wallet[1].

Finally, we run the contract’s transfer function with the message as the argument. This will effectively transfer the tokens.

Conclusions

This example shows how to run a complex test using Hardhat. We have demonstrated how Ethereum smart contracts’ digital signature capabilities can be used, along with ethers.js, to test the contract’s functionality.

Tests are crucial to ensure that smart contracts work as expected. With Hardhat and ethers.js, we have powerful tools at our disposal to carry out these tests.

Bear in mind that coding and deploying smart contracts is a complex and delicate activity. Errors or bugs can lead to serious problems, including the loss of funds. Therefore, it’s always important to carry out thorough testing and, if possible, to seek a third-party contract audit before using a contract in a production environment.

--

--