brownie icon indicating copy to clipboard operation
brownie copied to clipboard

Handle new error types for solidity 0.8.4

Open BlinkyStitt opened this issue 3 years ago • 6 comments

https://blog.soliditylang.org/2021/04/21/custom-errors/

This post has some example ethers js code that should be a decent guide.

BlinkyStitt avatar Jun 06 '21 21:06 BlinkyStitt

Was this fixed in #1110 ?

iamdefinitelyahuman avatar Jul 26 '21 20:07 iamdefinitelyahuman

I think that #1110 made Brownie not crash on them but did not add any way to parse the info out of them and display it to the user.

cameel avatar Aug 14 '21 20:08 cameel

HI, im having trouble parsing custom errors in my tests. Im using solc 0.8.4 and I get the following error when trying to parse the revert reason. Is there a plan to support this in the short term? Should I be using the brownie.reverts in a different way? Thanks!

  with brownie.reverts("CustomErrorMessage()"):
        token.issue(mintAmount, {'from': owner})

I get this error:

 - AssertionError: Unexpected revert string 'typed error: 0xd9a6bff9'

julianmrodri avatar Sep 21 '21 17:09 julianmrodri

+1 for this feature. It's been almost 2 years that solidity announced Catch for custom error types: https://blog.ethereum.org/2020/01/29/solidity-0.6-try-catch/ so who knows when this will actually land. Might as well bite the bullet and do the parsing on brownie itself. The existing helper with // dev: in brownie has been one of the decisive factors to choose brownie, so extending this for having support for custom errors is highly valuable IMO.

tfalencar avatar Oct 07 '21 08:10 tfalencar

Worth to mention I found this workaround to check for custom errors in tests. Replace YourCustomError() with the actual error you expect to throw.

  errMsg = web3.keccak(text='YourCustomError()')[:4].hex()  
  
  with brownie.reverts('typed error: ' + errMsg):
        ... your contract call....
        

julianmrodri avatar Oct 07 '21 12:10 julianmrodri

In case it is useful, I created some sample contracts and tests surrounding this: https://github.com/EdNoepel/brownie-test-issues/blob/master/tests/test_errors.py

Note that when a custom error is raised by an imported/library contract, the exception and callstack does not reveal details about the error.

EdNoepel avatar Apr 19 '22 17:04 EdNoepel

Hey guys @julianmrodri workaround works perfect but does anybody knows how to do same thing when errors have parameters? If I have an error for example like this error AddressNotAdmin(address addr);, the revert message is like typed error: 0x0888870300000000000000000000000033a4622b82d4c04a53e170c638b944ce27cffce3. Do you how that hash is built? Thanks

damn1 avatar Jan 11 '23 10:01 damn1

The selector is built the same way as for functions, i.e. includes parameter types. I.e. for your AddressNotAdmin error the selector is created by hashing AddressNotAdmin(address).

cameel avatar Jan 11 '23 18:01 cameel

This is not exactly a solution for this issue but you should give a try to the new Python-based testing framework that was inspired by Brownie - Woke testing framework (spoiler alert - I am the lead developer). It handles user-defined errors in a form of dataclass exceptions. To do this, pytypes , Python equivalents of Solidity types with type hints, are generated which also provide autocompletions while writing tests in Python.

michprev avatar Feb 10 '23 12:02 michprev

To anybody still struggling with this, I submitted a pull request. In the meanwhile (or if it is not approved) you can use this function in your testing:

from eth_utils.abi import function_abi_to_4byte_selector, collapse_if_tuple
def encode_custom_error(contract_, err_name, params):

    contract_abi = contract_.abi

    for error in [abi for abi in contract_abi if abi["type"] == "error"]:
        # Get error signature components
        name = error["name"]
        data_types = [collapse_if_tuple(abi_input) for abi_input in error.get("inputs", [])]
        error_signature_hex = function_abi_to_4byte_selector(error).hex()
 
        if err_name == name:
            encoded_params = ''
            for param in params:
                if(type(param)==str):
                    return('typed error: 0x'+error_signature_hex+param.zfill(66)[2:])
                elif(type(param)==int):
                    val = "{0:#0{1}x}".format(param,66)
                    val = val[2:]
                else:
                    return 'Unsupported type'
                encoded_params = encoded_params + val
            return('typed error: 0x'+error_signature_hex+encoded_params)
        
    return 'error not found'

For example if you want to test if a function checkIfPar(uint256 num) in your contract NumberGetter throws the error numberNotPa(num), for num = 10, you would do:

with brownie.reverts(encode_custom_error('NumberGetter', 'numberNotPar ', [10])):
        NumberGetterdeploy.checkIfPar(10
                                        {"from":accounts[0]})

JoaoMorais96 avatar Mar 21 '23 20:03 JoaoMorais96