18

I run a geth/parity node for broadcasting user's transactions to Ethereum blockchain. Some of contract functions provide reasons for revert (see example below):

contract Example {
  function test (uint i) {
    require(i == 1, "ERROR_CODE")
  }
}

As I understand it, it is not possible to get revert reason string with eth_getTransactionReceipt. However, I still need to get the revert reason for failed transactions.

How do I get the revert reason ("ERROR_CODE" in the example above) for past failed transactions (at least for transactions in the last 20 blocks)?

0

6 Answers 6

12

In solidity 0.4.22 the require and revert reason were added. As can be seen here, they are abi-encoded as if it were a call to a function "Error(string)".

This blog post gives an example: an eth_call to a function

function myFunction(uint256 input) public view returns (uint256) {
    require(input >= 5, "myFunction only accepts arguments which are greather than or equal to 5");
    return input * input - 25;
}

with an invalid input argument (less than 5 in this example), will return

0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000476d7946756e6374696f6e206f6e6c79206163636570747320617267756d656e747320776869636820617265206772656174686572207468616e206f7220657175616c20746f203500000000000000000000000000000000000000000000000000

which is

0x08c379a0                                                       // Function selector
0000000000000000000000000000000000000000000000000000000000000020 // Offset of string return value
0000000000000000000000000000000000000000000000000000000000000047 // Length of string return value (the revert reason)
6d7946756e6374696f6e206f6e6c79206163636570747320617267756d656e74 // first 32 bytes of the revert reason
7320776869636820617265206772656174686572207468616e206f7220657175 // next 32 bytes of the revert reason
616c20746f203500000000000000000000000000000000000000000000000000 // last 7 bytes of the revert reason

So decoding the returned string will give you the revert reason.

With Web3j this could be done like:

public Optional<String> getRevertReason(EthCall ethCall) {
    String errorMethodId = "0x08c379a0"; // Numeric.toHexString(Hash.sha3("Error(string)".getBytes())).substring(0, 10)
    List<TypeReference<Type>> revertReasonTypes = Collections.singletonList(TypeReference.create((Class<Type>) AbiTypes.getType("string")));

    if (!ethCall.hasError() && ethCall.getValue() != null && ethCall.getValue().startsWith(errorMethodId)) {
        String encodedRevertReason = ethCall.getValue().substring(errorMethodId.length());
        List<Type> decoded = FunctionReturnDecoder.decode(encodedRevertReason, revertReasonTypes);
        Utf8String decodedRevertReason = (Utf8String) decoded.get(0);
        return Optional.of(decodedRevertReason.getValue());
    }
    return Optional.empty();
}
2
  • So the 0x08c379a0 is the signature of the require() function?
    – Ender
    Commented Jul 25, 2021 at 3:49
  • 3
    @Ender it's the signature of Error(string)keccak256("Error(string)") == 08c379a0afcc32b1a39302f7cb8073359698411ab5fd6e3edb2c02c0b5fba8aa first 4 bytes is method sig: 08c379a0
    – Justin
    Commented Aug 3, 2021 at 19:59
9

I created an NPM package to handle this: eth-revert-reason


It is difficult to decode the revert reason in a general manner. Many different factors, such as Geth vs. Parity, web3.js vs ethers, etc. will result in different results for all the answers posted here. Some issues are:

  1. For a Kovan transaction, you need a custom provider that exposes Parity trace methods.
  2. Transactions may result in different messages depending on the context of the block they are called from. Because of this, you may need to be running a full-archival node in order to retrieve the correct error message.

In a happy-path case, the code to retrieve the revert reason is:

const provider = customProvider || ethers.getDefaultProvider(network)
const tx = await provider.getTransaction(txHash)
const code = await provider.call(tx)

While the code to produce the revert reason in the normal case is simple, the non-standard cases are surprisingly difficult to handle, however this package attempts to appease this.

2
  • 2
    Not all heroes wear capes.
    – YulePale
    Commented Sep 20, 2021 at 11:34
  • 1
    Just saw that: "...this works consistently with the Infura and Alchemy providers." :(
    – YulePale
    Commented Sep 20, 2021 at 11:56
8

Using eth_Call to execute function:

Example.test(0x0000000000000000000000000000000000000000000000000000000000000002) 

of Example contract we will receive following reply from geth node:

0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000a4552524f525f434f444500000000000000000000000000000000000000000000

Which gives following output arguments:

arg0: 0x08c379a
arg1: 0x20
arg2: 0x0a
arg3: 0x4552524f525f434f444500000000000000000000000000000000000000000000

where arg3 == 'ERROR_CODE' (utf8 + hex encoded)

So, I suppose, for fully syched node we can use eth_Call with explicit indication of failed transaction block number:

{
  method: "eth_Call",
  params: [
    {
      from: '0x...address',
      to: '0x...contract',
      value: '0x0',
      data: "0x29e99f070000000000000000000000000000000000000000000000000000000000000002"
    }, 
    FALED_TRANSACTION_BLOCK_NR
  ]
}

However this is not convenient way of getting error codes.. Is there any other way?

1
  • 1
    I can't do the edit because it's too short, but notice that arg0 is actually 0x08c379a0 (notice the trailing 0), which represents the 8 first bytes of the hash of Error(string). Commented Feb 28, 2020 at 1:36
6

After being really annoyed for a long time I wrote a bash script to fetch the revert reason from geth by a tx hash:

https://gist.github.com/gluk64/fdea559472d957f1138ed93bcbc6f78a

1
  • 1
    this is really helpful, could you also help me do the same for metamask failed transactions Commented Oct 7, 2019 at 6:41
2

It is also possible with go-ethereum. Tested with a simulated blockchain backend.

import (
    "bytes"
    "context"
    "math/big"

    "github.com/ethereum/go-ethereum"
    "github.com/ethereum/go-ethereum/accounts"
    "github.com/ethereum/go-ethereum/accounts/abi"
    "github.com/ethereum/go-ethereum/accounts/abi/bind"
    "github.com/ethereum/go-ethereum/accounts/keystore"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/core/types"
    "github.com/pkg/errors"
)

// other code using the following...

func errorReason(ctx context.Context, b ethereum.ContractCaller, from common.Address, tx *types.Transaction, blockNum *big.Int) (string, error) {
    msg := ethereum.CallMsg{
        From:     from,
        To:       tx.To(),
        Gas:      tx.Gas(),
        GasPrice: tx.GasPrice(),
        Value:    tx.Value(),
        Data:     tx.Data(),
    }
    res, err := b.CallContract(ctx, msg, blockNum)
    if err != nil {
        return "", errors.Wrap(err, "CallContract")
    }
    return unpackError(res)
}

var (
    errorSig            = []byte{0x08, 0xc3, 0x79, 0xa0} // Keccak256("Error(string)")[:4]
    abiString, _        = abi.NewType("string", "", nil)
)

func unpackError(result []byte) (string, error) {
    if !bytes.Equal(result[:4], errorSig) {
        return "<tx result not Error(string)>", errors.New("TX result not of type Error(string)")
    }
    vs, err := abi.Arguments{{Type: abiString}}.UnpackValues(result[4:])
    if err != nil {
        return "<invalid tx result>", errors.Wrap(err, "unpacking revert reason")
    }
    return vs[0].(string), nil
}

After a transaction failed, you usually hold a *types.Receipt from bind.WaitMined, which has the field BlockNumber, so you know with which block number to call errorReason.

2

This is not a direct answer to the ops question, but for all others that stumble over this question while searching for how to get the error reason generated by a failed solidity require in javascript:

It depends on wether you are using web3.js or ethers.js to call the contract

web3:

  // invalid index, should trigger: require(_i < Donors.length, "index _i out of bounds");
  const donorIndex = 99;
  // The Contract object
  let faucet = new web3.eth.Contract(abi, contractAddr);
  try {
    let r = await faucet.methods.getDonorAddress(donorIndex).call().catch((e) => { throw (e); });
    console.log("Donor Address result:", r);
  } catch (e) {
    if (e && e.reason) {
      console.log("contract require failed with:", e.reason);
    }
  }

ethers:

  // invalid index, should trigger: require(_i < Donors.length, "index _i out of bounds");
  const donorIndex = 99;
  // Use a read-only signer as we call only view/pure functions
  const signer = new ethers.VoidSigner(fromAddress, provider);
  // The Contract object
  const faucet = new ethers.Contract(contractAddr, abi, signer);
  try {
    let r = await faucet.getDonorAddress(donorIndex).catch((e) => { throw (e); });
    console.log("Donor Address result:", r);
  } catch (e) {
    if (e && e.reason) {
      console.log("contract require failed with:", e.reason);
    }
  }

Results:

[kim@manjaro4 nodejs]$ ./getRequireErrorMessageWeb3.js 
contract require failed with: value out of range

[kim@manjaro4 nodejs]$ ./getRequireErrorMessageEthers.js 
contract require failed with: index _i out of bounds

[kim@manjaro4 nodejs]$ npm list web3
[email protected] /home/kim/source/Energi-Smart-Contract-Tutorial/nodejs
└── [email protected] 
[kim@manjaro4 nodejs]$ npm list ethers
[email protected] /home/kim/source/Energi-Smart-Contract-Tutorial/nodejs
└── [email protected]

The full source is at: Github - Energi3 smartcontract tutorial

TL;DR: Use ethers.js

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.