eth.rb icon indicating copy to clipboard operation
eth.rb copied to clipboard

ERC-6093: Support solidity 0.8.4+ custom errors

Open bogdan opened this issue 11 months ago • 9 comments

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

Contract errors are not parsed from ABI by Contract class and the revert data is not part of IOError to be parsed manually.

Example script:

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'eth'
require 'active_support'
require 'active_support/core_ext/hash/indifferent_access'
require 'digest'

# Connect to Ethereum RPC (Infura example)
rpc_url = "https://base-rpc.publicnode.com"
client = Eth::Client.create(rpc_url)

# Load ERC-721 contract details
contract_address = "0x6f813e6430a223e3ac285144fa9857cb38a642a6"
token_id = 1

# ERC-721 ABI method signature for transferFrom(address, address, uint256)
abi = [
  {
    "constant": true,
    "inputs": [
      {
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ownerOf",
    "outputs": [
      {
        "name": "",
        "type": "address"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ERC721NonexistentToken",
    "type": "error"
  },
].map(&:with_indifferent_access)

contract = Eth::Contract.from_abi(name: "ERC721", address: contract_address, abi: abi)

result = client.call(contract, "ownerOf", token_id)
pp result

RPC Response:

{"jsonrpc"=>"2.0", "id"=>1, "error"=>{
"code"=>3, 
"message"=>"execution reverted",
"data"=>"0x7e2732890000000000000000000000000000000000000000000000000000000000000001"}}

Thrown error:

/Users/bogdan/.rvm/gems/ruby-3.3.4/gems/eth-0.5.13/lib/eth/client.rb:485:in `send_command': execution reverted (IOError)
	from /Users/bogdan/.rvm/gems/ruby-3.3.4/gems/eth-0.5.13/lib/eth/client.rb:394:in `block (2 levels) in <class:Client>'
	from /Users/bogdan/.rvm/gems/ruby-3.3.4/gems/eth-0.5.13/lib/eth/client.rb:454:in `call_raw'
	from /Users/bogdan/.rvm/gems/ruby-3.3.4/gems/eth-0.5.13/lib/eth/client.rb:263:in `call'

bogdan avatar Feb 17 '25 10:02 bogdan

Thanks for bringing this up. Just to clarify - would you prefer that the gem returns the full response instead of raising an error so that you can handle the error on your end?

q9f avatar Feb 18 '25 10:02 q9f

Throwing an error with all parsed data is better. I think current IOError is too basic. JS libs work this way.

bogdan avatar Feb 18 '25 10:02 bogdan

Ok got it. Can you take a look? I implemented custom solidity errors (if data is present), like the following:

/home/user/.src/q9f/eth.rb/lib/eth/client.rb:493:in `send_command': {"jsonrpc"=>"2.0", "id"=>1, "error"=>{"code"=>3, "message"=>"execution reverted", "data"=>"0x7e2732890000000000000000000000000000000000000000000000000000000000000001"}} (Eth::Client::CustomSolidityError)
        from /home/user/.src/q9f/eth.rb/lib/eth/client.rb:400:in `block (2 levels) in <class:Client>'
        from /home/user/.src/q9f/eth.rb/lib/eth/client.rb:460:in `call_raw'
        from /home/user/.src/q9f/eth.rb/lib/eth/client.rb:269:in `call'
        from err.rb:56:in `<main>'

You can do something like

rescue Client::CustomSolidityError => e
    pp JSON.parse(e.message.gsub("=>", ":"))
end

q9f avatar Feb 18 '25 15:02 q9f

This is good step forward, but the error could be parsed better. The custom solidity errors are parsed the same way as events. They have parameters and those parameters have solidity types. Here is how it is parsed using JS viem library:

Details: VM Exception while processing transaction: reverted with custom error 
'ERC721NonexistentToken(79106826802597624183325779265297774265409121481891170477833790770152883893012)'

Ideally, we should do the same.

bogdan avatar Feb 18 '25 16:02 bogdan

Ok, got it. I just read the documentation.

The compiler includes all errors that a contract can emit in the contract's ABI-JSON. Note that this will not include errors forwarded through external calls. Similarly, developers can provide NatSpec documentation for errors which would then be part of the user and developer documentation and can explain the error in much more detail at no cost.

I'll spend some time thinking how this would be best implemented. We already have the logic now to raise a custom solidity error.

Probably next step is to implement a rescue somewhere where we are calling the contact (and thus having access to the ABI) which can pretty-print the error using the contract's ABI.

q9f avatar Feb 18 '25 16:02 q9f

Is there a specification on custom errors in ABI?

contract.abi.last["name"] does not seem to be a safe accessor for that.

q9f avatar Feb 18 '25 16:02 q9f

See, when I saw this library I was surprised on how contract functions are called:

client.call(contract, "ownerOf", token_id)

This is weird. As I would expect a function to be called on a contract because only the contract is aware of a function, but not the client.

All js libraries I used do it this way. After using web3.js, ethers and viem over years, I think the best way to implement function calling is like this:

func = contract.function("safeTransferFrom", from, to, tokenId)
# eth_call to make sure the function doesn't revert
# experienced developers know that eth_estimateGas returns nonsense when function reverts
func.call()
# eth_estimateGas to make sure it is under the adequate limit
func.estimate_gas 
tx = func.sign(key)
# eth_sendTransaction
client.eth_send_raw_transaction(tx.hex)
# store tx somewhere to ensure retries are possible
Tx.create!(hex: tx.hex)

If you want some KISS for v1, just have:

contract.call("safeTransferFrom", from, to, tokenId)
contract.estimate_gas(....)
contract.sign(...)

bogdan avatar Feb 18 '25 17:02 bogdan

Is there a specification on custom errors in ABI?

I didn't find official link.

Here is what I've got for https://eips.ethereum.org/EIPS/eip-6093#erc-721:

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "ERC721IncorrectOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ERC721InsufficientApproval",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approver",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidApprover",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidOperator",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidSender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ERC721NonexistentToken",
    "type": "error"
  }
]

bogdan avatar Feb 18 '25 18:02 bogdan

Ok, so this library is almost 10 years old. While most of the code has been rewritten over time, some of the weird logic remains. You correctly pointed out that calling the client instead of the contract is not intuitive. This mechanic requires some time and as this is only touching the developer experience, this would be for me very low priority given all the other open issues. I'd be happy to accept PRs though, if anyone wants to give this a go.

W.r.t. ERC-6093 custom errors, this is a worthwhile endeavor. I'll take a look at it but cannot promise any fast progress in the coming weeks. I'm currently focusing, if anything on finally fixing the ABI coders.

q9f avatar Apr 17 '25 15:04 q9f

Closed by #344

q9f avatar Aug 04 '25 06:08 q9f