perimetersec / fuzzlib

Solidity Fuzzing Library
MIT License
32 stars 12 forks source link

Support for text-based errors in `assertErrorsAllowed` #23

Open rappie opened 7 months ago

rappie commented 7 months ago

Code example for foundry (thanks to DevDacian)

[invariant]
fail_on_revert = true
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/utils/Strings.sol";

import "forge-std/Test.sol";

// run from base project directory with:
// forge test --match-contract ErrorTest -vvv
contract ErrorRaiser {
    error SecondError();
    error ThirdError();

    function generateError(uint8 input) external pure {
        if(input == 0) revert("First error");

        revert SecondError();
    }
}

contract ErrorTest is Test {
    using Strings for string;
    ErrorRaiser errorRaiser;

    function setUp() public {
        errorRaiser = new ErrorRaiser();

        targetContract(address(this));

        bytes4[] memory selectors = new bytes4[](1);
        selectors[0] = this.fake_handler.selector;

        targetSelector(FuzzSelector({addr: address(this), selectors: selectors}));
    }

    function invariant_always_true() public pure {
        assert(true);
    }

    function fake_handler(uint8 input) public {
        try errorRaiser.generateError(input) {}
        // handle `require` text-based errors
        catch Error(string memory err) {
            string[] memory allowedErrors = new string[](1);
            allowedErrors[0] = "First error";

            _assertTextErrorsAllowed(err, allowedErrors);
        }
        // handle custom errors
        catch(bytes memory err) {
            bytes4[] memory allowedErrors = new bytes4[](1);
            allowedErrors[0] = ErrorRaiser.SecondError.selector;

            _assertCustomErrorsAllowed(err, allowedErrors);
        }

    }

    // used to filter for allowed text errors during functions
    // if a function fails with an error that is not allowed,
    // this can indicate a potential DoS attack vector
    event UnexpectedTextError(string);
    function _assertTextErrorsAllowed(string memory err, string[] memory allowedErrors) private {
        bool allowed;
        uint256 allowedErrorsLength = allowedErrors.length;

        for (uint256 i; i < allowedErrorsLength;) {
            if (err.equal(allowedErrors[i])) {
                allowed = true;
                break;
            }
            unchecked {++i;}
        }

        if(!allowed) {
            emit UnexpectedTextError(err);
            assert(false);
        }
    }

    // used to filter for allowed custom errors during functions
    // if a function fails with an error that is not allowed,
    // this can indicate a potential DoS attack vector
    event UnexpectedCustomError(bytes);
    function _assertCustomErrorsAllowed(bytes memory err, bytes4[] memory allowedErrors) private {
        bool allowed;
        bytes4 errorSelector = bytes4(err);
        uint256 allowedErrorsLength = allowedErrors.length;

        for (uint256 i; i < allowedErrorsLength;) {
            if (errorSelector == allowedErrors[i]) {
                allowed = true;
                break;
            }
            unchecked {++i;}
        }

        if(!allowed) {
            emit UnexpectedCustomError(err);
            assert(false);
        }
    }
}
aviggiano commented 7 months ago

btw, I don't think you need unchecked {++i;} after Solidity 0.8.22