4

Since Solidity v0.8.4, custom errors are supported, also in ethers.js.

How can I use them in a test? Here is an example:

pragma solidity 0.8.10;

contract Contract {
    uint8 value;

    error ComparisionFailed(uint8 expected, uint8 actual);

    function setValue(uint8 _value) external {
        value = _value;
    }

    function compare(uint8 _value) external view {
        if (_value != value)
            revert ComparisionFailed({expected : value, actual : _value});
    }
}

The test below works, but this is a bad solution.

import {expect} from 'chai';
import {ethers} from 'hardhat';

describe('Contract', function () {
  let contract: any;

  beforeEach(async function () {
    let Contract = await ethers.getContractFactory('Contract');
    contract = await Contract.deploy();
  });

  it('Should revert if value is wrong', async function () {
    const expected = 123;

    await contract.setValue(expected);

    let actual = 123;
    await expect(
        contract.compare(actual)
    ).to.be.ok;

    actual = 1
    await expect(
        contract.compare(actual)
    ).to.be.revertedWith("custom error 'ComparisionFailed(123, 1)'"); // bad solution
  });
});

What's the proper way to handle this and check for the error ComparisionFailed from the abi and the values 123 and 1?

3 Answers 3

2

You can handle custom errors from your Solidity smart contract in your application using ethers-decode-error quite easily. This utility also allows you to extract and use the parameters from custom errors if needed.

Consider a scenario where you have a contract method called swap, and you want to catch a specific custom error called InvalidSwapToken. You can do this using the following code:

import { ErrorDecoder } from 'ethers-decode-error'
const errorDecoder = ErrorDecoder.create([abi])

const MyCustomErrorContract = new ethers.Contract('0x12345678', abi, provider)
try {
  const tx = await MyCustomErrorContract.swap('0xabcd', 123)
  await tx.wait()
} catch (err) {
  const decodedError = await errorDecoder.decode(err)
  const reason = customReasonMapper(decodedError)
  // Prints "Invalid swap with token contract address 0xabcd."
  console.log('Custom error reason:', reason)
}

const customReasonMapper = ({ name, args }: DecodedError): string => {
  switch (name) {
    case 'InvalidSwapToken':
      // You can access the error parameters using their index:
      return `Invalid swap with token contract address ${args[0]}.`
      // Or, you could also access the error parameters using their names:
      return `Invalid swap with token contract address ${args['token']}.`
    default:
      return 'The transaction has reverted.'
  }
}

The example above threw an InvalidSwapToken custom error which has a parameter of the token address. The value of this parameter can be accessed from the args variable when handling the error. If your customer error doesn't have any parameters, you can ignore the args variable.

You can refer to the docs for more examples.

Here's how the ABI of the InvalidSwapToken custom error with an input parameter might have looked like:

const abi = [
  {
    inputs: [
      {
        internalType: 'address',
        name: 'token',
        type: 'address',
      },
    ],
    name: 'InvalidSwapToken',
    type: 'error',
  },
]

It will be much easier if you're using Typechain as you can pass the contract interface as ABI:

const errorDecoder = ErrorDecoder.create([MyCustomErrorContract.interface])
1

I wrote a function to check for custom errors:

export function customError(errorName: string, ...args: any[]) {

  let argumentString = '';

  if (Array.isArray(args) && args.length) {

    // add quotation marks to first argument if it is of string type
    if (typeof args[0] === 'string') {
      args[0] = `"${args[0]}"`
    }

    // add joining comma and quotation marks to all subsequent arguments, if they are of string type
    argumentString = args.reduce(function (acc: string, cur: any) {
      if (typeof cur === 'string')
        return `${acc}, "${cur}"`
      else
        return `${acc}, ${cur.toString()}`;
    })
  }

  return `'${errorName}(${argumentString})'`
}

The function can also handle string type arguments properly. Here is the solution to the example in the original post:

await expect(
  contract.compare(actual)
).to.be.revertedWith(customError('ComparisionFailed', expected, actual);
0

According to the document, you can use revertedWithCustomError

await expect(
 contract.compare(actual)
).to.be.revertedWithCustomError(contract, 'ComparisionFailed').withArgs(123, 1);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.