Smock: The powerful mocking tool for Hardhat
Features of smock and how to use them with examples
We’ve covered mocking contracts before, but now there’s an additional great tool available: smock. It simplifies the mocking process greatly and also gives you more testing power. You’ll be able to change the return values for functions as well as changing internal contract storage directly!
How cool is that?
How to set up ?
Requirements: Currently only Hardhat with Waffle is supported. You can follow the setup here. If you use Truffle, you can take a look at mock-contract or wait for future support (see bottom).
The smock tool is available as plugin. You can install it via Npm using:
$ npm install @eth-optimism/smock --save-dev
Now you just have to add the storage plugin inside hardhat.config.js
:
require('@eth-optimism/smock/build/src/plugins/hardhat-storagelayout')
That's all, let's go ahead and use it.
smockit
The first functionality of changing the return values is available via smockit
. With this tool you can mock function calls:
- - asserting call count and call data
- - returning mocked data
- - reverting with data
Will the code in the function be executed?
- No, there will be no code of the mocked function executed.
Can I mock an internal function call?
- No, that's currently not supported. Only external function calls can be mocked. But this might change in the future (see bottom).
smockit is available via import:
const { smockit } = require("@eth-optimism/smock");
smoddit
The second functionality of changing internal storage is available via smoddit
. With this tool you can directly change the contract storage:
- modify primitive types directly
- change data in mappings
- modify stored structs
smoddit is available via import:
const { smoddit } = require("@eth-optimism/smock");
Let's see an example
MyERC20.sol
Imagine we have an ERC-20 contract with a mintUpTo
function. This function will mint whatever amount is required to mint to get the passed address to the requested balance amount.
This function as you can see is onlyOwner
. Now imagine a complex system where the onlyOwner
is really just another contract who's allowed to call this function. Let's see an example second contract for this...
pragma solidity ^0.7.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyERC20 is ERC20, Ownable {
constructor() ERC20("MyERC20", "MYE") {}
function mintUpTo(
address to, uint256 amount
) external onlyOwner returns (uint256) {
uint256 currentBalance = balanceOf(to);
if (currentBalance >= amount) return 0;
uint256 mintBalance = amount - currentBalance;
_mint(to, mintBalance);
return mintBalance;
}
}
pragma solidity ^0.7.0;
import "hardhat/console.sol";
import "./MyERC20.sol";
contract MyOtherContract {
MyERC20 private myERC20;
constructor(MyERC20 myERC20_) {
myERC20 = myERC20_;
}
function myOtherFunction(
address to, uint256 amount
) external returns (bool) {
// do stuff
uint256 mintAmount = myERC20.mintUpTo(to, amount);
console.log("The minted amount was", mintAmount);
// do more stuff
}
}
MyOtherContract.sol
In this contract we call the mintUpTo
function. Remember this will only work if the contract is actually the owner. This is the intended behavior we want in a mainnet setup. But wouldn't it be useful if we could avoid this just for our unit-tests?
Figuring out the system state in where MyOtherContract is actually the owner of myERC20 might require some complex initialization steps. Let's avoid this by mocking the mintUpTo
function...
Mocking mintUpTo via smockit
We can first create the smocked contract using:
smockit(myERC20)
This will create a copy of the contract with its own address. The address is actually just an empty account without any code. So don't call any actual functions here, only mocked ones.
So to mock our function we use:
MyMockContract.smocked.mintUpTo
.will.return.with(mockedMintAmount)
We now pass the mocked contract address to our MyOtherContract
. And as you can see, we can call myOtherFunction
even though it's not the owner of myERC20.
On top we can actually check and verify the calls to mintUpTo
. So in our unit test we can assert that the mintUpTo
does indeed happen with the expected call parameters available via:
MyMockContract.smocked.mintUpTo.calls
const { expect } = require("chai");
const { smoddit, smockit } = require("@eth-optimism/smock");
const { BigNumber } = require("ethers");
describe("My ERC20 and other contract", function () {
let myERC20;
beforeEach(async () => {
const MyERC20 = await ethers.getContractFactory("MyERC20");
myERC20 = await MyERC20.deploy();
await myERC20.deployed();
});
it("call mint up in my ERC20", async function () {
const MyMockContract = await smockit(myERC20);
const MyOtherContract = await ethers.getContractFactory(
"MyOtherContract"
);
const myOtherContract = await MyOtherContract.deploy(
MyMockContract.address
);
const mockedMintAmount = 30;
MyMockContract.smocked.mintUpTo
.will.return.with(mockedMintAmount);
const to = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const amount = 100;
await myOtherContract.myOtherFunction(to, amount);
expect(MyMockContract.smocked.mintUpTo.calls.length)
.to.be.equal(1);
expect(MyMockContract.smocked.mintUpTo.calls[0].to)
.to.be.equals(to);
expect(
MyMockContract.smocked.mintUpTo.calls[0].amount.toString()
).to.equal(amount.toString());
});
});
it("call mint up in my ERC20", async function () {
const MyModifiableERC20Factory = await smoddit("MyERC20");
const MyModifiableERC20 = await MyModifiableERC20Factory.deploy();
const MyOtherContract = await ethers.getContractFactory(
"MyOtherContract"
);
const myOtherContract = await MyOtherContract.deploy(
MyModifiableERC20.address
);
await MyModifiableERC20.transferOwnership(myOtherContract.address);
const to = "0xdAC17F958D2ee523a2206206994597C13D831ec7";
const transferAmount = 100;
const mockedBalance = BigNumber.from("10");
MyModifiableERC20.smodify.put({
_balances: {
[to]: mockedBalance,
},
});
expect((await MyModifiableERC20.balanceOf(to)).toString()).to.equal(
mockedBalance.toString()
);
await myOtherContract.myOtherFunction(to, transferAmount);
expect((await MyModifiableERC20.balanceOf(to)).toString()).to.equal(
transferAmount.toString()
);
});
Changing token balance via smoddit
We can also modify the internal storage of a contract via smoddit
. Create a contract factory and use it to deploy the contract:
const Factory = await smoddit("MyERC20");
const Contract = await Factory.deploy()
The resulting contract can be modified directly. In our case we can for example change the ERC-20 balance of the contract:
Contract.smodify.put({
_balances: {
[to]: mockedBalance,
},
});
An Optimistic Smock Future
I've reached out to Kelvin Fichter, the author of Smock and part of the Optimism team. As of right now you need Waffle and Buidler to use the tool. But in the next version it's planned to add support for Truffle.
And what I'm particularly excited about, we might get support for mocking internal functions. Instead of creating an empty account for the mocked address, the contract would have actual code behind the address. This would greatly improve the functionality. Stay tuned for further updates!
Solidity Developer