Interfacing With UniswapV2Router02: 4 ― Tests Using Mocked Smart Contracts

As a rule, smart contracts are probably more rigorously tested than code that is not deployed onto a blockchain. This is due to the higher 'degree of immutability' of smart contracts compared to off-chain code. Once deployed, a contract is simply immutable, without any gradation. The only way to effectuate changes to the code is to deploy a new instance of the contract, which will live at a new address, and update any references to the contract in other contracts that may be using it. (Other contracts may keep the contract address in their storage, which, unlike the code, is mutable.) Given that smart contract updates are more costly than updating off-chain code, special attention is paid to testing their functionality before they are deployed.

As with traditional testing frameworks, the need may arise to mock a smart contract in order to force a return value for one of its functions, or to verify a function was called as a result of a transaction. While Hardhat wraps Mocha to allow the developer to run tests that make use of the Hardhat context, it does not provide functionality for mocking smart contracts. To that end, smock was developed. Apart from smockit, which may be used to mock smart contract functions, smock includes smoddit for modifying a contract's storage variables. smoddit requires the Solidity compiler to emit the storage layout in the compilation artifacts so that it can map variable names to storage addresses. This is a setting that can be added to the solidity section in hardhat.config.ts:

const config: HardhatUserConfig = {
solidity: {
compilers: [
{
version: '0.8.6',
settings: {
outputSelection: {
'*': {
'*': ['storageLayout'],
},
},
},
},
],
},
// ...
}

The use cases for smockit in my tests were:

The way this works is that smockit creates a new instance of the contract where the mocked function is replaced with a stub that just returns the desired value. When testing against Uniswap contracts on a mainnet-forking node, this brings with it the limitation that it's a bit unwieldy to mock a token contract referenced by a pair: the pair references the token contract using its real address, while smockit creates a new contract the pair knows nothing about. So in order to achieve the desired setup, the pair itself must also be mocked.

This case doesn't arise in my tests, so the test suite happily just creates the mock contracts in isolation:

  beforeEach(async () => {
mockDAI = await smockit(DAI)
mockDAI_USDC = await smockit(DAI_USDC)
})

The function mocks are applied in the individual tests:

  it('gets ERC20 token balance of account', async () => {
const mockBalance = BigNumber.from(123)
mockDAI.smocked.balanceOf.will.return.with(mockBalance)
return expect(api.balanceOf(mockDAI.address, signerAddr)).to.eventually.eq(mockBalance)
})

(return here is needed with expect because the eventually property from chai-as-promised is used.)

Multiple return values may also be mocked:

    mockDAI_USDC.smocked.getReserves.will.return.with([
mockOutputReserve,
mockInputReserve,
0,
])

(Compare with the signature of UniswapV2Pair.getReserves.) ethers.BigNumber may be used for numeric values throughout.

One detail that may be a troll when running tests that are doing on-chain stuff is test timeouts, as tests involving transactions tend to be on the long-running side, especially when they're run on a test network (Rinkeby, …) or a forking node that needs to make RPC calls to an archive node in the background. Mocha's default timeout may be overridden in a section named mocha in hardhat.config.ts. (bail: true is just to abort the test suite on the first fail.)

  mocha: {
bail: true,
timeout: '600s',
},

Finally, when running tests on a forking node, it may be desirable to reset the fork at the beginning of the suite, or between individual tests. Hardhat's node provides a function for that, which can be called using hre.network.provider.request.

const resetFork = () =>
hre.network.provider.request({
method: 'hardhat_reset',
params: [
{
forking: {
jsonRpcUrl: JSON_RPC_URL,
blockNumber: FORKING_BLOCK_NUMBER,
},
},
],
})