Solidity 0.8.x Foundry – Catching Custom Errors in Forge

custom-errorsforgefoundrysolidity-0.8.x

I want to catch a custom defined error in my Foundry tests. Before I was using require and the following code worked:

    try pathRegistry.swap(WETH, LUSD, amountIn, type(uint256).max) {
        assertTrue(false, "swap(..) should revert when amountOut <  amountIn.");
    } catch Error(string memory reason) {
        assertEq(reason, "INSUFFICIENT_AMOUNT_OUT");
    }

After replacing this require statement

require(amountOutMin <= amountOut, "INSUFFICIENT_AMOUNT_OUT");

with

if (amountOut < amountOutMin) revert InsufficientAmountOut();

the catch execution path no longer gets executed.

I managed to get the catch part executed using the following code:

    try pathRegistry.swap(WETH, LUSD, amountIn, type(uint256).max) {
        assertTrue(false, "swap(..) should revert when amountOut <  amountIn.");
    } catch (bytes memory reason) {
        emit log_string(string(reason));
    }

However an issue with this code is that string(reason) is equal to �)p� and I would like to test that specifically the InsufficientAmountOut error got thrown.

How would I solve this?
Thank you

EDIT:
According to this post errors are ABI encode. For this reason I assume that I should catch the error with catch (bytes memory reason). The issue I am now facing is that I don't know how to verify that bytes memory reason can actually be decoded as error InsufficientAmountOut(). I found a blog post where the author explains how to decode custom errors in javascript. However I don't know how to do it in Solidity. Thanks

Best Answer

SOLUTION 1:

Best solution is using expectRevert cheat code from forge-std:

vm.expectRevert(ExampleBridgeContract.InvalidCaller.selector);
exampleBridge.convert(empty, empty, empty, empty, 0, 0, 0, address(0));

If you are using an error with parameters pass calldata instead of selector to the vm.expectRevert(...) method:

vm.expectRevert(abi.encodeWithSignature("IncorrectStatus(uint8,uint8)", 1, 0));

SOLUTION 2:

    try pathRegistry.swap(WETH, LUSD, amountIn, type(uint256).max) {
        assertTrue(false, "swap(..) should revert when amountOut <  amountIn.");
    } catch (bytes memory reason) {
        bytes4 desiredSelector = bytes4(keccak256(bytes("InsufficientAmountOut(uint256,uint256)")));
        bytes4 receivedSelector = bytes4(reason);
        assertEq(desiredSelector, receivedSelector);
    }

Since custom errors are encoded the same way functions are, bytes memory reason contains function/error selector and the encoded values. I don't want to work with the values so I simply verify the selector. To do that I convert bytes memory reason to bytes4 type which removes all the bytes related to the encoded values and leaves only the selector in place. Then I simply compare with assertEq whether the selector really corresponds to the custom error selector which is supposed to be thrown in this test.

EDIT: As @Ismael pointed out it's possible to receive the selector directly from the error definition. The code then would look like this:

    try pathRegistry.swap(WETH, LUSD, amountIn, type(uint256).max) {
        assertTrue(false, "swap(..) should revert when amountOut <  amountIn.");
    } catch (bytes memory reason) {
        bytes4 expectedSelector = PathRegistry.InsufficientAmountOut.selector;
        bytes4 receivedSelector = bytes4(reason);
        assertEq(expectedSelector, receivedSelector);
    }