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?
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.
The first functionality of changing the return values is available via smockit
. With this tool you can mock function calls:
Will the code in the function be executed?
Can I mock an internal function call?
smockit is available via import:
const { smockit } = require("@eth-optimism/smock");
The second functionality of changing internal storage is available via smoddit
. With this tool you can directly change the contract storage:
smoddit is available via import:
const { smoddit } = require("@eth-optimism/smock");
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
}
}
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...
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()
);
});
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,
},
});
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
The Openzeppelin v4 contracts are now available in Beta and most notably come with Solidity 0.8 support. For older compiler versions, you'll need to stick with the older contract versions. The beta tag means there still might be small breaking changes coming for the final v4 version, but you can...
As we've discussed last week, flash loans are a commonly used pattern for hacks. But what exactly are they and how are they implemented in the contracts? As of right now each protocol has its own way of implementing flash loans. With EIP-3156 we will get a standardized interface. The standard was...
With the recent Yearn vault v1 hack from just a few days ago, we can see a new pattern of hacks emerging: Get anonymous ETH via tornado.cash . Use the ETH to pay for the hack transaction(s). Use a flash loan to decrease capital requirements. Create some imbalances given the large capital and...