2

In Solidity code, there are some statements like revert("some reason") or require(a < b, "a should be less than b"). I want to get the error reason string using ethers.js.

I know there's an NPM package called ethers-decode-error. In the past I was using ethers.js v6, and this package works fine. But recently for some reason, I've started using ethers.js v5, and the ethers-decode-error can't decode the actual reverted reason anymore.

I'm wondering how to get the transaction reverted data? I've console.loged both the transaction and the receipt, and I can't find a field that contains the error message or hexadecimal representation of the error message.


My code is like:

let receipt: TransactionReceipt;
try {
  const response = await myContract.myMethod();
  receipt = await response.wait();
} catch (e) {
  console.log(e);
  // Here `e.data` is undefined
}
Yan
  • 145
  • 4

1 Answers1

1

(Updated solution)

First of all, you can get the txHash this way:

const response = await myContract.myMethod();
receipt = await response.wait();

txHash = receipt.transactionHash;

// ...


This should work:

const ethers = require("ethers");

const rpcProviderUrl = 'https://rpc.ankr.com/polygon_mumbai'; const provider = new ethers.providers.JsonRpcProvider(rpcProviderUrl);

const txHash = '0x074e91fe5c1535d9bdc2bbc8cbadc0378c93eb06318401b1f341d844185430fb';

(async function () { const txData = await provider.getTransaction(txHash);

// IMPORTANT! Otherwise the mock call will fail because these fields cannot co-exist...
if (txData.gasPrice) {
    txData.maxFeePerGas = undefined;
    txData.maxPriorityFeePerGas = undefined;
}

try {
    const res = await provider.call(txData);

    console.log({res});

    const errorReasonMessage = ethers.utils.toUtf8String('0x' + res.substring(138)).replaceAll('\x00', ''); // clear the empty bytes

    console.log({errorReasonMessage});
}
catch(err) {
    console.log({err});
}

})();

(index.js)

With the following package.json:

{
  "name": "ethers-js-v5-js-retrieve-revert-message",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "ethers": "^5.7.2"
  }
}

Explanation. Re But when simulating, the EVM states have already changed and the result of simulation may be different from the actual run. For example, myContract.myMethod may depend on timestamp, blockhash, the address of an NFT owner, etc...

Yes, I am mocking a transaction, essentially, but the transaction is retrieved from the blockchain immutable history via the txHash, and the txHash has the following structure (example real Polygon testnet txData):

{
  hash: '0x074e91fe5c1535d9bdc2bbc8cbadc0378c93eb06318401b1f341d844185430fb',
  type: 2,
  accessList: [],
  blockHash: '0x11a099885b00b5fab210ee50bf27a64772380ffef550f581db7bf87f9ec0e638',
  blockNumber: 46538654,
  transactionIndex: 0,
  confirmations: 654532,
  from: '0x5F497A8Db6B9138eEEd4D456EBfA7114Ac9A3b87',
  gasPrice: BigNumber { _hex: '0x9502f90f', _isBigNumber: true },
  maxPriorityFeePerGas: BigNumber { _hex: '0x9502f900', _isBigNumber: true },
  maxFeePerGas: BigNumber { _hex: '0x9502f91e', _isBigNumber: true },
  gasLimit: BigNumber { _hex: '0x012ea32c', _isBigNumber: true },
  to: '0xc58f3C8108a6feeb9aB9B6fd47D31822cA3aC2eD',
  value: BigNumber { _hex: '0x00', _isBigNumber: true },
  nonce: 361,
  data: '0x975428450000000000000000000000000000000000000000000000000000000000000000',
  r: '0x3758b8676b4a5667bb5d2d22bdae19516ed48e680c476c3372f00992500824ad',
  s: '0x05e2cc00be1740f11d978975a6b383e128dba65cd9490d596c84540c2898ba74',
  v: 0,
  creates: null,
  chainId: 80001,
  wait: [Function (anonymous)] // injected by Typescript
}

So you do have the block.number specified here.

The provider will make a mock call based on the block.number and the transactionIndex.

Here's a great explanation of the structure of the transaction object, by @eth (https://ethereum.stackexchange.com/a/16541/120999):

blockHash: String, 32 Bytes - hash of the block where this transaction was in.
blockNumber: Number - block number where this transaction was in.
transactionHash: String, 32 Bytes - hash of the transaction.
transactionIndex: Number - integer of the transactions index position in the block.
from: String, 20 Bytes - address of the sender.
to: String, 20 Bytes - address of the receiver. null when its a contract creation transaction.
cumulativeGasUsed: Number - The total amount of gas used when this transaction was executed in the block.
gasUsed: Number - The amount of gas used by this specific transaction alone.
status: String - '0x0' indicates transaction failure , '0x1' indicates transaction succeeded.
contractAddress: String - 20 Bytes - The contract address created, if the transaction was a contract creation, otherwise null.
logs: Array - Array of log objects, which this transaction generated.

So if you're concerned about reproducing the transaction in the exact same state and the same block position and order, transactionIndex comes to the rescue!!

transactionIndex will be set by miner. each block has many transactions which are ordered to mine. your transaction's index is 0 when the block goes to mine the transaction was first tx in that block. maybe you are using local dev node because in real networks a user cant get his/her tx all the time at first place in a block (https://ethereum.stackexchange.com/a/114279/120999)


(Old, first revision of my answer:)

Indeed with ethers.js v5, the way you should access the error property of the call is slightly different from ethers.js v6.

I suggest you to try something like this:

try {
  await myContract.someMethod()
} catch (error) {
  // @ts-ignore
  const revertData = error.data.data
  const decodedError = myContract.interface.parseError(revertData)
  console.log(`Transaction failed with a revert reason: ${decodedError.name}`)
}

Does that work?


P.S. That should work for runtime transaction errors, and the method is slightly different when you need to get the revert reason from an old txHash.

Ideally, you should share your ethers.js integration code, so it's possible reproduce it and check whether a particular approach works in each case.

Mila A
  • 1,154
  • 2
  • 15
  • 1
    Thanks for answering! I've tried but got no luck. my error.data is undefined. I've also updated my code in the question – Yan Mar 18 '24 at 07:27
  • I see. What provider are you running this on? Geth? Ganache? – Mila A Mar 18 '24 at 07:40
  • 1
    I'm playing with the new Blast L2 chain. Since Blast is a fork of Optimism, I use Optimism SDK to create provider. i.e., const provider = optimism.asL2Provider(new ethers.providers.StaticJsonRpcProvider(rpcUrl));. The rpcUrl is provided by QuickNode through a private API key. – Yan Mar 18 '24 at 07:45
  • @Yan, then check out my updated answer ;) – Mila A Mar 18 '24 at 08:50
  • Really thanks for such a detailed answer!! Seems that you are trying to simulate the transaction again. But when simulating, the EVM states have already changed and the result of simulation may be different from the actual run. For example, myContract.myMethod may depend on timestamp, blockhash, the address of an NFT owner, etc... – Yan Mar 18 '24 at 09:31
  • @Yan, please see my updated answer above! In short, blockHash, blockNumber and transactionIndex come to the rescue, and allow for mocking the transaction in the exactly same matching state, as it was specified within the blockchain based on the retrieved txData and the provider. Hopefully, that answers your concerns :) – Mila A Mar 18 '24 at 12:22
  • 1
    Wow this answer is amazing!!! Really really thanks!! – Yan Mar 18 '24 at 13:36
  • 1
    @Yan, you're very welcome! :) – Mila A Mar 18 '24 at 14:37