Quick hack for testing Hardhat projects with Foundry

lopotras
8 min readMay 18, 2023

--

Weather you’re an experienced smart contract auditor or just getting into the space you have probably heard about Foundry — it’s a fast and robust smart contract development toolchain.

While Foundry has been continuously gaining popularity, it is Hardhat that still holds the first place when it comes to most used frameworks for Solidity smart contract development.

What often makes people favour Foundry is that it allows testing and deployments using Solidity, unlike Hardhat which uses Javascript or Typescript. The tests in Foundry are often easier to write as you do not need to translate the operations to another language.

Photo by Pisit Heng on Unsplash

Before we dive in, note that this article assumes you have some basic knowledge of both Hardhat and Foundry.

Switching Frameworks

As an auditor you do not have the power over which framework a project you’re going to inspect uses. If you’re a Foundry wizard and have to tackle a project written in Hardhat, what you can always do is quickly rewrite the needed setup in Foundry, right…?

Well, there are a few problems with that:

  1. It takes time
    Weather it is a small or big project rewriting the setup takes time, which instead could be spent inspecting the original code. This is especially relevant when doing auditing competitions where, often given very limited time, every hour counts.
  2. It’s easy to make mistakes
    When you’re writing code bugs happen — that is why you’re doing the audit in the first place. The more complex the setup you’re recreating is, the more probable it is that you will make mistakes along the way.
  3. Projects can be complex
    A lot of projects build on top of existing DeFi building blocks, forking the mainnet and then deploying a multi-contract setup with complex relations on top of it. And with that we come back to points 1 and 2. It’s a bit like thinking: ‘Let me just redraw this crossroad real quick before I actually dive into what’s happening there.’
Photo by Timo Volz on Unsplash

Luckily, there’s a way around!

For the purpose of this article I created a sample Hardhat project using hardhat init and changing Lock.sol contract and its test a little to fit the tutorial. Full repository can be found here, the concept itself is applicable to any Hardhat project though.

Setting up Foundry

On your Hardhat repository run:

forge init --force

Note that --force flag is necessary as the repository you’re initiating forge in is not empty, otherwise you’re going to run into the following error:

$ forge init
Error:
Cannot run `init` on a non-empty directory.
Run with the `--force` flag to initialize regardless.

On taking effect this command will create a few new folders in your directory: lib, src, script, as well as a Counter.t.sol file in existing test folder and a foundry.toml file, which is responsible for configuring foundry. Take a look at the configuration file first:

[profile.default]
src = 'src'
out = 'artifacts'
libs = ['node_modules']
remappings = [
'@openzeppelin/=node_modules/@openzeppelin/',
'eth-gas-reporter/=node_modules/eth-gas-reporter/',
'hardhat/=node_modules/hardhat/',
]

src parameter for this scenario should be equal to contracts as that’s where all the contracts usually reside in Hardhat. For the out I like to change it to artifactsFoundry , to keep it separate from Hardhat’s compilation output, similar for script , but as scripts will not be needed for now, you can just delete that folder.

Next let’s add lib directory to libs and as it already contains one library used for testing, forge-std, let’s add it to remappings .

For the keeping the repo clean I prefer to put my forge tests separate from the Hardhat ones, by adding test='testFoundry' line.

So by now your foundry.toml file should look like this:

[profile.default]
src = 'contracts'
out = 'artifactsFoundry'
libs = ['node_modules', 'lib']
test = 'testFoundry'
remappings = [
'@openzeppelin/=node_modules/@openzeppelin/',
'eth-gas-reporter/=node_modules/eth-gas-reporter/',
'hardhat/=node_modules/hardhat/',
'forge-std/=lib/forge-std/src/'
]

Now create a testFoundry folder and move Counter.t.sol file there from test and rename it to Lock.t.sol. You will not use the Counter contract, but its test file will serve as a good base for our own tests.

Choosing what to test

Now it’s time to think of what do you actually want to test.

Usually a project will have an extensive test suite exploring various states of the system and in most of the cases one of these states will be a starting point for the tests you want to perform.

In this case original hardhat test file start as follows:

describe("Lock", function () {
// We define a fixture to reuse the same setup in every test.
// We use loadFixture to run this setup once, snapshot that state,
// and reset Hardhat Network to that snapshot in every test.
async function deployOneYearLockFixture() {
const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
const ONE_GWEI = 1_000_000_000;

const lockedAmount = ONE_GWEI;

const latestBlock = await ethers.provider.getBlock("latest");
const unlockTime = latestBlock.timestamp + ONE_YEAR_IN_SECS;

// Contracts are deployed using the first signer/account by default
const [owner, otherAccount] = await ethers.getSigners();

const Lock = await ethers.getContractFactory("Lock");
const lock = await Lock.deploy(unlockTime, { value: lockedAmount });

return { lock, unlockTime, lockedAmount, owner, otherAccount };
}

describe("Deployment", function () {
it("Should set the right unlockTime", async function () {
const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);

expect(await lock.unlockTime()).to.equal(unlockTime);
});

deployOneYearLockFixture() function deploys Lock contract and defines accounts used in tests and initial contract parameters. Its example use case is visible in first it block below:

const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);

As this particular it block checks the correct functioning of the system, we might as well use it as our starting point for Foundry tests. Let’s mark it with the only keyword:

it.only("Should set the right unlockTime", async function () {

This will ensure that this test will be the only one from the suite to run — we need only one deployment after all.

Migrating to Foundry

To be able to test the chosen state using forge we will need to:

  1. Deploy it on a local hardhat node
  2. Fork the local network with forge and test

Let’s roll!

Deploy chosen state on a local hardhat node

First run a local hardhat node by typing in your terminal:

npx hardhat node

This should result in the following output:

As stated on top, network’s rpc-url is: http://127.0.0.1:8545. This will be useful later on in order to fork this network. Accounts listed here will be the ones used in the tests by default.

After that you need to open a new terminal for other actions — this one will be used to run the node. Leave it as it is.

Now, before you run the chosen test, you need to extract some information from it. The type of information and its amount will differ from case to case. For example to redo the following test in forge:

it.only("Should set the right unlockTime", async function () {
const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);

expect(await lock.unlockTime()).to.equal(unlockTime);
});

you will need an address of deployed Lock contract and unlockTime . You can get it by adding some console.log statements at the end of the test:

it.only("Should set the right unlockTime", async function () {
const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);

expect(await lock.unlockTime()).to.equal(unlockTime);
console.log("lock:", lock.address);
console.log("unlockTime:", unlockTime.toString());
});

After that you can type the following command to run the test on your local node:

npx hardhat test --network localhost

Which will give you the following output:

The values you need show up under Deployment section. Now, as you’re all set up, it’s time to move to part 2.

Fork the local network with forge and test

Start with a test file. By default a test file generated by Foundry on initialization will look like this:

pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../src/Counter.sol";

contract CounterTest is Test {
Counter public counter;

function setUp() public {
counter = new Counter();
counter.setNumber(0);
}

function testIncrement() public {
counter.increment();
assertEq(counter.number(), 1);
}

function testSetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}

Clean it up to look as follows:

pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../contracts/Lock.sol";

contract LockTest is Test {
Lock public lock;

function test() public {

}
}

Normally a setUp() function is used to deploy initial state of the contracts, but as in this case the setup is done in Hardhat you won’t need it.

Next, introduce information extracted from the Hardhat test:

Lock public lock = Lock(0x5FbDB2315678afecb367f032d93F642f64180aa3);
uint256 unlockTime = 1715963342;

This will map a Lock contract to a given address and store unlockTime value. Then recreate chosen test, so that the file looks as follows:

pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "../contracts/Lock.sol";

contract LockTest is Test {
Lock public lock = Lock(0x5FbDB2315678afecb367f032d93F642f64180aa3);
uint256 unlockTime = 1715963342;

function test_Lock() public {
assertEq(lock.unlockTime(), unlockTime);
}
}

Afterwards come back to the terminal running you local node. It should look like this:

Note, that on deploying the Lock contract a new block was minted, so right now number of the latest block on your local network is 1. You will use it to specify from which block the network should be forked.

With that information you’re all set to run the test. Run:

forge test --fork-url http://localhost:8545 --fork-block-number 1

in your terminal. The result should be:

Done!

Finishing touches

As during audit or tests you might end up using this command a lot it can save you some time to add it to your package.json file.

"scripts": {
"forge-test": "forge test --fork-url http://localhost:8545 --fork-block-number 1"
}

Now to run the test you can type:

yarn forge-test

Thanks for sticking around!

I hope this trick brings you many good findings.

If you find the article useful make sure to follow me on Twitter:
https://twitter.com/lopotras

Full repo of this project can be found here:
https://github.com/lopotras/foundry-x-hardhat-setup

Have a beautiful day and until next time!

--

--

lopotras

Web3 Builder | Sharing my journey in On-chain Security