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 custom errors from Solidity 0.8.4 (new fragment type) #1493

Closed
frangio opened this issue Apr 21, 2021 · 12 comments
Closed

Support custom errors from Solidity 0.8.4 (new fragment type) #1493

frangio opened this issue Apr 21, 2021 · 12 comments
Labels
enhancement New feature or improvement. fixed/complete This Bug is fixed or Enhancement is complete and published.

Comments

@frangio
Copy link

frangio commented Apr 21, 2021

Today Solidity 0.8.4 was released and it includes a new feature called "custom errors" that currently breaks Ethers.js.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

contract TestToken {
    error InsufficientBalance(uint256 available, uint256 required);

    function transfer(address /*to*/, uint amount) public pure {
        revert InsufficientBalance(0, amount);
    }
}

The resulting ABI contains a new type of fragment for errors:

{
  "type": "error",
  "name": "InsufficientBalance",
  "inputs": [
    {
      "internalType": "uint256",
      "name": "available",
      "type": "uint256"
    },
    {
      "internalType": "uint256",
      "name": "required",
      "type": "uint256"
    }
  ],
},

Loading the contract ABI on Ethers results in:

Error: invalid fragment object (argument="value", value={"inputs":[{"internalType":"uint256","name":"available","type":"uint256"},{"internalType":"uint256","name":"required","type":"uint256"}],"name":"InsufficientBalance","type":"error"}, code=INVALID_ARGUMENT, version=abi/5.1.1)

Eventually Ethers should parse the revert data into one of these error objects but in the meantime it should still support loading these ABIs.

@frangio frangio added the investigate Under investigation and may be a bug. label Apr 21, 2021
@ricmoo
Copy link
Member

ricmoo commented Apr 22, 2021

Sorry, just saw this. I’ve opened up enhancement issues to track this and will be adding support soon. :)

@ricmoo
Copy link
Member

ricmoo commented Apr 22, 2021

Being tracking in #1497 and #1498.

@ricmoo ricmoo added enhancement New feature or improvement. on-deck This Enhancement or Bug is currently being worked on. and removed investigate Under investigation and may be a bug. labels Apr 22, 2021
@ricmoo
Copy link
Member

ricmoo commented Apr 26, 2021

Not erroring should be fixed in [5.1.4])https://github.com/ethers-io/ethers.js/releases/tag/v5.1.4).

Try it out and let me know. :)

@frangio
Copy link
Author

frangio commented Apr 26, 2021

Seems to work!

@ricmoo
Copy link
Member

ricmoo commented Apr 27, 2021

Awesome. I'll close this issue, and use #1498 to track the v5.2 release with full Custom Error support.

Thanks! :)

@sebastiendan
Copy link

Hi,

I supposed custom errors should work thanks to this issue, yet I cannot get them.

I have a simple contract:

pragma solidity ^0.8.4;

error Invalid (uint256 a, uint256 b);

contract Test {
    function compare(uint256 a, uint256 b) public view {
        if (a < b) {
            revert Invalid({a: a, b: b});
        }
    }
}

And I have a simple script:

const ethers = require("ethers");

const ABI = require("./abi.json");

const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545");

const contract = new ethers.Contract(
  <CONTRACT_ADDRESS>,
  ABI,
  provider
);

(async () => {
  await contract.compare(0, 1);
})();
ABI
[
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "a",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "b",
        "type": "uint256"
      }
    ],
    "name": "Invalid",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "a",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "b",
        "type": "uint256"
      }
    ],
    "name": "compare",
    "outputs": [],
    "stateMutability": "view",
    "type": "function"
  }
]

No matter which version of ethers I'm using (5.1, 5.2, 6.*), I get errors like below, with a null or undefined error data field:

{"jsonrpc":"2.0","id":44,"error":{"code":-32600,"message":"execution was reverted"}}

@ricmoo
Copy link
Member

ricmoo commented Mar 30, 2023

@sebastiendan In v6, it should definitely work well. What backend are you using? Geth? Can you add provider.on("debug", console.log) and see what is coming over the wire?

@sebastiendan
Copy link

@ricmoo I'm using a polygon-edge chain.

With [email protected], the latest debug log:

{
  action: 'request',
  request: {
    method: 'eth_call',
    params: [ [Object], 'latest' ],
    id: 44,
    jsonrpc: '2.0'
  },
  provider: JsonRpcProvider {
    _isProvider: true,
    _events: [ [Event] ],
    _emitted: { block: -2 },
    formatter: Formatter { formats: [Object] },
    anyNetwork: false,
    _networkPromise: Promise { [Object] },
    _maxInternalBlockNumber: -1024,
    _lastBlockNumber: -2,
    _pollingInterval: 4000,
    _fastQueryDate: 0,
    connection: { url: 'http://localhost:30002' },
    _nextId: 45,
    _eventLoopCache: { detectNetwork: null, eth_chainId: null },
    _network: { chainId: 2359, name: 'unknown' }
  }
}

With [email protected], the two debug logs:

{
  action: 'sendRpcPayload',
  payload: [
    { method: 'eth_chainId', params: [], id: 1, jsonrpc: '2.0' },
    { method: 'eth_call', params: [Array], id: 2, jsonrpc: '2.0' },
    { method: 'eth_chainId', params: [], id: 3, jsonrpc: '2.0' }
  ]
} EventPayload { filter: 'debug', emitter: JsonRpcProvider {} }
{
  action: 'receiveRpcResult',
  result: [
    { jsonrpc: '2.0', id: 1, result: '0x937' },
    { jsonrpc: '2.0', id: 2, error: [Object] },
    { jsonrpc: '2.0', id: 3, result: '0x937' }
  ]
} EventPayload { filter: 'debug', emitter: JsonRpcProvider {} }

@ricmoo
Copy link
Member

ricmoo commented Mar 30, 2023

Ethers v5.1 definitely doesn’t support it, as custom errors came out afterwards, but the most recent v5 version should handle them fine.

I’m most concerned with v6; the part I need is masked (the [ Object ]). Can you use provider.on("debug", (e) => console.dir(e, { depth: null })) to suss it out?

@sebastiendan
Copy link

With [email protected]:

{
  action: 'sendRpcPayload',
  payload: [
    { method: 'eth_chainId', params: [], id: 1, jsonrpc: '2.0' },
    {
      method: 'eth_call',
      params: [
        {
          to: '0x6478de14702aa616bf5548fa97a7e8eb22ae2dab',
          data: '0xf360234c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001'
        },
        'latest'
      ],
      id: 2,
      jsonrpc: '2.0'
    },
    { method: 'eth_chainId', params: [], id: 3, jsonrpc: '2.0' }
  ]
}
{
  action: 'receiveRpcResult',
  result: [
    { jsonrpc: '2.0', id: 1, result: '0x937' },
    {
      jsonrpc: '2.0',
      id: 2,
      error: { code: -32600, message: 'execution was reverted' }
    },
    { jsonrpc: '2.0', id: 3, result: '0x937' }
  ]
}

With [email protected]:

Open here
{
  action: 'request',
  request: {
    method: 'eth_call',
    params: [
      {
        to: '0x6478de14702aa616bf5548fa97a7e8eb22ae2dab',
        data: '0xf360234c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001'
      },
      'latest'
    ],
    id: 44,
    jsonrpc: '2.0'
  },
  provider: JsonRpcProvider {
    _isProvider: true,
    _events: [
      Event {
        tag: 'debug',
        listener: [Function (anonymous)],
        once: false,
        _lastBlockNumber: -2,
        _inflight: false
      }
    ],
    _emitted: { block: -2 },
    disableCcipRead: false,
    formatter: Formatter {
      formats: {
        transaction: {
          hash: [Function: bound ],
          type: [Function: bound ],
          accessList: [Function (anonymous)],
          blockHash: [Function (anonymous)],
          blockNumber: [Function (anonymous)],
          transactionIndex: [Function (anonymous)],
          confirmations: [Function (anonymous)],
          from: [Function: bound ],
          gasPrice: [Function (anonymous)],
          maxPriorityFeePerGas: [Function (anonymous)],
          maxFeePerGas: [Function (anonymous)],
          gasLimit: [Function: bound ],
          to: [Function (anonymous)],
          value: [Function: bound ],
          nonce: [Function: bound ],
          data: [Function: bound ],
          r: [Function (anonymous)],
          s: [Function (anonymous)],
          v: [Function (anonymous)],
          creates: [Function (anonymous)],
          raw: [Function (anonymous)]
        },
        transactionRequest: {
          from: [Function (anonymous)],
          nonce: [Function (anonymous)],
          gasLimit: [Function (anonymous)],
          gasPrice: [Function (anonymous)],
          maxPriorityFeePerGas: [Function (anonymous)],
          maxFeePerGas: [Function (anonymous)],
          to: [Function (anonymous)],
          value: [Function (anonymous)],
          data: [Function (anonymous)],
          type: [Function (anonymous)],
          accessList: [Function (anonymous)]
        },
        receiptLog: {
          transactionIndex: [Function: bound ],
          blockNumber: [Function: bound ],
          transactionHash: [Function: bound ],
          address: [Function: bound ],
          topics: [Function (anonymous)],
          data: [Function: bound ],
          logIndex: [Function: bound ],
          blockHash: [Function: bound ]
        },
        receipt: {
          to: [Function (anonymous)],
          from: [Function (anonymous)],
          contractAddress: [Function (anonymous)],
          transactionIndex: [Function: bound ],
          root: [Function (anonymous)],
          gasUsed: [Function: bound ],
          logsBloom: [Function (anonymous)],
          blockHash: [Function: bound ],
          transactionHash: [Function: bound ],
          logs: [Function (anonymous)],
          blockNumber: [Function: bound ],
          confirmations: [Function (anonymous)],
          cumulativeGasUsed: [Function: bound ],
          effectiveGasPrice: [Function (anonymous)],
          status: [Function (anonymous)],
          type: [Function: bound ]
        },
        block: {
          hash: [Function (anonymous)],
          parentHash: [Function: bound ],
          number: [Function: bound ],
          timestamp: [Function: bound ],
          nonce: [Function (anonymous)],
          difficulty: [Function: bound ],
          gasLimit: [Function: bound ],
          gasUsed: [Function: bound ],
          miner: [Function (anonymous)],
          extraData: [Function: bound ],
          transactions: [Function (anonymous)],
          baseFeePerGas: [Function (anonymous)]
        },
        blockWithTransactions: {
          hash: [Function (anonymous)],
          parentHash: [Function: bound ],
          number: [Function: bound ],
          timestamp: [Function: bound ],
          nonce: [Function (anonymous)],
          difficulty: [Function: bound ],
          gasLimit: [Function: bound ],
          gasUsed: [Function: bound ],
          miner: [Function (anonymous)],
          extraData: [Function: bound ],
          transactions: [Function (anonymous)],
          baseFeePerGas: [Function (anonymous)]
        },
        filter: {
          fromBlock: [Function (anonymous)],
          toBlock: [Function (anonymous)],
          blockHash: [Function (anonymous)],
          address: [Function (anonymous)],
          topics: [Function (anonymous)]
        },
        filterLog: {
          blockNumber: [Function (anonymous)],
          blockHash: [Function (anonymous)],
          transactionIndex: [Function: bound ],
          removed: [Function (anonymous)],
          address: [Function: bound ],
          data: [Function (anonymous)],
          topics: [Function (anonymous)],
          transactionHash: [Function: bound ],
          logIndex: [Function: bound ]
        }
      }
    },
    anyNetwork: false,
    _networkPromise: Promise { { chainId: 2359, name: 'unknown' } },
    _maxInternalBlockNumber: -1024,
    _lastBlockNumber: -2,
    _maxFilterBlockRange: 10,
    _pollingInterval: 4000,
    _fastQueryDate: 0,
    connection: { url: 'http://localhost:30002' },
    _nextId: 45,
    _eventLoopCache: { detectNetwork: null, eth_chainId: null },
    _network: { chainId: 2359, name: 'unknown' }
  }
}
/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/logger/lib/index.js:238
        var error = new Error(message);
                    ^

Error: missing revert data in call exception; Transaction reverted without a reason string [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (data="0x", transaction={"to":"0x6478dE14702AA616BF5548FA97A7e8eb22Ae2dAB","data":"0xf360234c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001","accessList":null}, error={"reason":"processing response error","code":"SERVER_ERROR","body":"{\"jsonrpc\":\"2.0\",\"id\":44,\"error\":{\"code\":-32600,\"message\":\"execution was reverted\"}}","error":{"code":-32600},"requestBody":"{\"method\":\"eth_call\",\"params\":[{\"to\":\"0x6478de14702aa616bf5548fa97a7e8eb22ae2dab\",\"data\":\"0xf360234c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001\"},\"latest\"],\"id\":44,\"jsonrpc\":\"2.0\"}","requestMethod":"POST","url":"http://localhost:30002"}, code=CALL_EXCEPTION, version=providers/5.7.2)
    at Logger.makeError (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/logger/lib/index.js:238:21)
    at Logger.throwError (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/logger/lib/index.js:247:20)
    at checkError (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:108:16)
    at JsonRpcProvider.<anonymous> (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:751:47)
    at step (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:48:23)
    at Object.throw (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:29:53)
    at rejected (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:21:65)
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  reason: 'missing revert data in call exception; Transaction reverted without a reason string',
  code: 'CALL_EXCEPTION',
  data: '0x',
  transaction: {
    to: '0x6478dE14702AA616BF5548FA97A7e8eb22Ae2dAB',
    data: '0xf360234c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',
    accessList: null
  },
  error: Error: processing response error (body="{\"jsonrpc\":\"2.0\",\"id\":44,\"error\":{\"code\":-32600,\"message\":\"execution was reverted\"}}", error={"code":-32600}, requestBody="{\"method\":\"eth_call\",\"params\":[{\"to\":\"0x6478de14702aa616bf5548fa97a7e8eb22ae2dab\",\"data\":\"0xf360234c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001\"},\"latest\"],\"id\":44,\"jsonrpc\":\"2.0\"}", requestMethod="POST", url="http://localhost:30002", code=SERVER_ERROR, version=web/5.7.1)
      at Logger.makeError (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/logger/lib/index.js:238:21)
      at Logger.throwError (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/logger/lib/index.js:247:20)
      at /Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/web/lib/index.js:313:32
      at step (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/web/lib/index.js:33:23)
      at Object.next (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/web/lib/index.js:14:53)
      at fulfilled (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/web/lib/index.js:5:58)
      at processTicksAndRejections (node:internal/process/task_queues:96:5) {
    reason: 'processing response error',
    code: 'SERVER_ERROR',
    body: '{"jsonrpc":"2.0","id":44,"error":{"code":-32600,"message":"execution was reverted"}}',
    error: Error: execution was reverted
        at getResult (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/providers/lib/json-rpc-provider.js:191:21)
        at processJsonFunc (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/web/lib/index.js:356:22)
        at /Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/web/lib/index.js:288:46
        at step (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/web/lib/index.js:33:23)
        at Object.next (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/web/lib/index.js:14:53)
        at fulfilled (/Users/sebastiendan/workspace/test-ethers-revert/node_modules/@ethersproject/web/lib/index.js:5:58)
        at processTicksAndRejections (node:internal/process/task_queues:96:5) {
      code: -32600,
      data: undefined
    },
    requestBody: '{"method":"eth_call","params":[{"to":"0x6478de14702aa616bf5548fa97a7e8eb22ae2dab","data":"0xf360234c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"},"latest"],"id":44,"jsonrpc":"2.0"}',
    requestMethod: 'POST',
    url: 'http://localhost:30002'
  }
}

@ricmoo
Copy link
Member

ricmoo commented Mar 31, 2023

@sebastiendan It looks like your node (polygon-edge) isn't returning the error data at all, so there is nothing for ethers to process.

All it returned was:

{
  jsonrpc: '2.0',
  id: 2,
  error: { code: -32600, message: 'execution was reverted' }
}

It needs to return the encoded error information to be decoded. Maybe you can open a bug or feature request with them?

@sebastiendan
Copy link

@ricmoo I'll do that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or improvement. fixed/complete This Bug is fixed or Enhancement is complete and published.
Projects
None yet
Development

No branches or pull requests

3 participants