Testing Interactions with other Smart Contracts
Tags: blockchain, tutorial, javascript, testing
Published: 2022-01-04 00:34:51

Developing on the blockchain is an incredible experience. The ecosystem is open and permissionless; each project becoming a lego brick in whatever idea a developer has in mind. Because of the open nature of the blockchain, it's not uncommon to have your smart contracts interact with another project’s contracts. It may be a [Chainlink Oracle](https://chain.link/), a Dex like [Uniswap](https://uniswap.org/), or a Lending platform like the [QiDAO](https://mai.finance/), or maybe you interact with all three in a single contract?

But how do you test your contract based on responses and interactions with these external contracts?

There are two ways: you can deploy "mock contracts" or you can use a mocking library. There are tradeoffs, but for this post I am going to focus on using a [Smock's](https://smock.readthedocs.io/en/latest/index.html) mocking library to put external contracts into a place for testing.

Smock depends on [Hardhat](https://hardhat.org/) so you need to have a Hardhat project. For the sake of this post let's write and test a smart contract that can liquidate a loan on the [QiDAO](https://mai.finance/).

The [QiDAO](https://mai.finance/) contracts can be found in their [docs](https://docs.mai.finance/functions/smart-contract-addresses) and the source can be found on their github.

Specifically we will be using the [erc20Stablecoin](https://github.com/0xlaozi/qidao/blob/main/contracts/erc20Stablecoin/erc20Stablecoin.sol) contract deployed for [LINK - 0x61167073E31b1DAd85a3E531211c7B8F1E5cAE72](https://polygonscan.com/address/0x61167073E31b1DAd85a3E531211c7B8F1E5cAE72)

Our simple liquidation contract looks like this.

```solidity
contract LoanLiquidator {
  address const vaultAddress = 0x61167073E31b1DAd85a3E531211c7B8F1E5cAE72
  function liquidate(uint256 vaultId) external {
  	erc20Stablecoin vault = erc20Stablecoin(vaultAddress);
    require(vault.checkLiquidation(vaultId), "Vault not below liquidation threshold");

    vault.liquidateVault(vaultId);
  }
}
```

For simplicity let's test the two cases of `checkLiquidation` as `liquidateVault` doesn't return anything. First we will test  but there is a gotcha. We can get into that later!

```tsx
describe("LoanLiquidator", () => {
  describe("#liquidate", () => {
    it("should revert if the vault cannot be liquidated")
    it("call the vaults liquidateVault if the loan can be liquidated")
  })
})
```

If we aren't using Smock then this is pretty difficult. I would either need to inject a contract address into the `LoanLiquidator` and then have that address implement `erc20Stablecoin`'s interface. That's for another blog post.

In this post it's a lot simpler because we will use Smock, but there are limitations. First let's focus on `it("should revert if the vault cannot be liquidated")`

```tsx
#...
it("should revert if the vault cannot be liquidated", async () => {
  const VAULT_ADDRESS = "0x61167073E31b1DAd85a3E531211c7B8F1E5cAE72"
  # I am using Typechain to generate types for the erc20Stablecoin ABI
  const fake = await smock.fake<Erc20QiStablecoin>(
    Erc20QiStablecoin.abi,
    {address: VAULT_ADDRESS}
  );

  fake.checkLiquidation.returns(false);

  const LoanLiquidatorFactory = await ethers.getContractFactory("LoanLiquidatator") as LoanLiquidator__factory;
  const loanLiquidator = await LoanLiquidatatorFactory.deploy();
  await loanLiquidator.deployed();

  await expect(loanLiquidator.liquidate(1)).to
    .be
    .revertedWith("Vault not below liquidation threshold")

  expect(fake.liquidateVault).not.to.have.been.called
})
#...
```

The magic here lies in the `opts` for Smock's `#fake()` method. You can pass an existing contract address to `#fake()` and Smock will use Hardhat's `[hardhat_setCode](https://hardhat.org/hardhat-network/reference/#hardhat-setcode)` rpc call to replace the contract at the address given with Smock's fake implementation of the contract.

Next lets test `it("call the vaults liquidateVault if the loan can be liquidated")`.

```tsx
it("call the vaults liquidateVault if the loan can be liquidated", async () => {
  const VAULT_ADDRESS = "0x61167073E31b1DAd85a3E531211c7B8F1E5cAE72"
  # I am using Typechain to generate types for the erc20Stablecoin ABI
  const fake = await smock.fake<Erc20QiStablecoin>(
    Erc20QiStablecoin.abi,
    {address: VAULT_ADDRESS}
  );

  fake.checkLiquidation.returns(true);

  const LoanLiquidatorFactory = await ethers.getContractFactory("LoanLiquidatator") as LoanLiquidator__factory;
  const loanLiquidator = await LoanLiquidatorFactory.deploy();
  await loanLiquidator.deployed();

  await expect(loanLiquidator.liquidate(1)).not.to
    .be
    .reverted

  expect(fake.liquidateVault).to.have.been.called
})
```

In this case you'll get green lights and you can keep on coding. In the real world there's a gotcha. When you fake a contract you fake *all* of it. By default, functions will now return their Zero value. If you have calls to later in your implementation that require non-zero values.

A clear example of this is if we add the method `#getVaultAddress()` to our `LoanLiquidator` contract:

```solidity
function getVaultAddress() public view returns (address) {
  return vaultAddress;
}
```

Now in test, after faking, if you call `#getVaultAddress()` you will get the zero address `0x0000000000000000000000000000000000000000` If you had code that used the returned address you may see an error like:

`Error: Transaction reverted: function call to a non-contract account`

This just scratches the surface of what's possible with Smock and Solidity. The Web3 space is one of the most test driven development friendly and open ecosystems I have ever encountered.

If you're interested in TDD, writing great software, and developing cutting edge technology, don't hesitate to check out our [careers page](notion://www.notion.so/careers). Or if you’re looking for a partner to help build your next dApp, backend, or frontend and up-skill your  team please reach out to us at work@withfocus.com.