Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support solidity 0.8.4+ custom errors #323

Open
bogdan opened this issue Feb 17, 2025 · 8 comments · May be fixed by #325
Open

Support solidity 0.8.4+ custom errors #323

bogdan opened this issue Feb 17, 2025 · 8 comments · May be fixed by #325
Labels
enhancement New feature or request good first issue Good for newcomers

Comments

@bogdan
Copy link

bogdan commented Feb 17, 2025

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'
@q9f q9f added enhancement New feature or request good first issue Good for newcomers labels Feb 18, 2025
@q9f
Copy link
Owner

q9f commented Feb 18, 2025

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?

@bogdan
Copy link
Author

bogdan commented Feb 18, 2025

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

@q9f q9f linked a pull request Feb 18, 2025 that will close this issue
@q9f
Copy link
Owner

q9f commented Feb 18, 2025

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

@bogdan
Copy link
Author

bogdan commented Feb 18, 2025

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.

@q9f
Copy link
Owner

q9f commented Feb 18, 2025

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
Copy link
Owner

q9f commented Feb 18, 2025

Is there a specification on custom errors in ABI?

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

@bogdan
Copy link
Author

bogdan commented Feb 18, 2025

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
Copy link
Author

bogdan commented Feb 18, 2025

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"
  }
]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants