DEV Community

George K
George K

Posted on

Smart contracts integration testing with hardhat mainnet fork

In the Ethereum blockchain, the data and code of smart contracts are stored on-chain. Smart contracts can interact with each other in a permissionless manner, unlike Web 2.0 APIs that usually require authorization or are just inaccessible from a public network. Thereby composability is achieved. New decentralized applications can be created based on other applications and can be integrated with them.

When creating such an application, it is important to be able to test integration with other applications, the interaction of smart contracts. For example, let's say you want to write a Swap Aggregator service that routes specific “swap” to one of the exchanges: Sushiswap, Uniswap, Curve, etc. The contract may look like this:

// SwapAggregator.sol

contract SwapAggregator {
  public uint256 SUSHISWAP = 0;
  public uint256 sushiswapRouter = 0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f;

  // other variables…

  function swap(uint256 dexId, address tokenIn, address tokenOut, uint256 amountIn) external {
    if (dexId == SUSHISWAP) {
      // here goes some interaction with Sushiswap router
      ISushiswapRouter(sushiswapRouter).swap(tokenIn, tokenOut, amountIn, msg.sender);
    } // else if ...
  }

  // other methods…
}
Enter fullscreen mode Exit fullscreen mode

You need to make sure that your smart contract interacts correctly with the smart contracts of other exchanges. How can you do that?

Smart contract mocks

First, hardhat provides convenient contract testing tools. You can write simplified versions of exchange contracts that have methods you need. For example, you can create a SushiswapRouter smart contract that has a swap method with the desired functionality (and its interface matches the SushiswapRouter smart contract interface on the mainnet). Let’s have a look:

// test.js

it("Should swap through Sushiswap", async () => {
  const SushiswapRouter = await ethers.getContractFactory("SushiswapRouter");
  const sushiRouter = await SushiswapRouter.deploy();

  const SwapAggregator = await ethers.getContractFactory("SwapAggregator");
  const swapAggregator = await SwapAggregator.deploy(sushiRouter.address);

  const tx = await swapAggregator.swap(sushiswapId, USDC, 1000000)
  // validate tx receipt
})
Enter fullscreen mode Exit fullscreen mode

This approach has some disadvantages:

  • You can implement functionality of a smart contract incorrectly, the test will pass locally, but your smart contract won’t work on the mainnet.
  • It is difficult as you need to spend time implementing a test smart contract. And the more complex the integration, the more difficult it is to do.

Testing in testnet

The second option is to deploy your contract on testnet and properly test the integration there. This is a good option and should be done in most cases. But sometimes it’s impossible:

  • Not all projects are deployed to testnet, thus, the smart contracts you need may simply not be available.
  • There may not be all the necessary data (for example, any specific tokens that you want to support the exchange of)

Forking mainnet

The third option is to check the integration on the local blockchain with state from the mainnet. Hardhat can fork mainnet.

From documentation:

You can start an instance of Hardhat Network that forks mainnet. This means that it will simulate having the same state as mainnet, but it will work as a local development network. That way you can interact with deployed protocols and test complex interactions locally

To launch local node with state forked from the mainnet:

npx hardhat node --fork https://rpc.flashbots.net/ 
Enter fullscreen mode Exit fullscreen mode

Now we can run tests

npx hardhat --network localhost test
Enter fullscreen mode Exit fullscreen mode

And you can interact with contracts from mainnet

// test:

it("Swap should work", async () => {
  // Sushiswap router address in mainnet
  const sushiswapRouterAddress = "0xd9e1ce17f2641f24ae83637ab66a2cca9c378b9f"

  const SwapAggregator = await ethers.getContractFactory("SwapAggregator");
  const swapAggregator = await SwapAggregator.deploy(sushiswapRouterAddress);

  const tx = await swapAggregator.swap(sushiswapId, USDC, 1000000)
  // validate tx receipt
})
Enter fullscreen mode Exit fullscreen mode

By default, the fork will use the most recent mainnet block, so the tests will be run with different states. This is usually undesirable and hardhat allows you to create a fork from a specific block:

npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/<key> --fork-block-number 14390000
Enter fullscreen mode Exit fullscreen mode

It requires an archive node, for example, Alchemy. Just don't forget to replace with your own key.

Funds for testing

You may need funds to test some features of the smart contract (obviously, you need to have some tokens to exchange them). To get ETH, you can use hardhat_setBalance.

await network.provider.send("hardhat_setBalance", [
  "0x0d2026b3EE6eC71FC6746ADb6311F6d3Ba1C000B",
  ethers.utils.parseUnits(1),
]);
// now account 0x0d2026b3EE6eC71FC6746ADb6311F6d3Ba1C000B has 1 ETH after this
Enter fullscreen mode Exit fullscreen mode

To get some token, for example, USDC, you can exchange ETH for it using any exchange (in our example, we could use SwapAggregator for this).

Access to contracts

In some cases, it may be necessary to change the configuration of other smart contracts, and for this, you need to have access to an account with such authority. hardhat_impersonateAccount will help here:

await hre.network.provider.request({
  method: "hardhat_impersonateAccount",
  params: ["0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6"],
});
//  after that, all transactions will be sent on behalf of 0x364d6D0333432C3Ac016Ca832fb8594A8cE43Ca6
Enter fullscreen mode Exit fullscreen mode

Other useful features

For more complex cases, hardhat allows you to rewrite data in the storage of a smart contract or change the code of the entire contract. List of utility methods:

  • hardhat_impersonateAccount
  • hardhat_stopImpersonatingAccount
  • hardhat_setNonce
  • hardhat_setBalance
  • hardhat_setCode
  • hardhat_setStorageAt

You can learn more about them in the hardhat documentation.

To sum up, using mainnet forking, you can test complex interactions with other apps. And deploy the contract being sure that everything will work on the mainnet.

Top comments (0)