diff --git a/src.ts/abi/abi-coder.ts b/src.ts/abi/abi-coder.ts index 752414212..89213139b 100644 --- a/src.ts/abi/abi-coder.ts +++ b/src.ts/abi/abi-coder.ts @@ -14,7 +14,13 @@ import { StringCoder } from "./coders/string.js"; import { TupleCoder } from "./coders/tuple.js"; import { ParamType } from "./fragments.js"; -import type { BytesLike } from "../utils/index.js"; +import { getAddress } from "../address/index.js"; +import { getBytes, hexlify, makeError } from "../utils/index.js"; + +import type { + BytesLike, + CallExceptionAction, CallExceptionError, CallExceptionTransaction +} from "../utils/index.js"; const paramTypeBytes = new RegExp(/^bytes([0-9]*)$/); @@ -91,5 +97,80 @@ export class AbiCoder { } } +// https://docs.soliditylang.org/en/v0.8.17/control-structures.html +const PanicReasons: Map = new Map(); +PanicReasons.set(0x00, "GENERIC_PANIC"); +PanicReasons.set(0x01, "ASSERT_FALSE"); +PanicReasons.set(0x11, "OVERFLOW"); +PanicReasons.set(0x12, "DIVIDE_BY_ZERO"); +PanicReasons.set(0x21, "ENUM_RANGE_ERROR"); +PanicReasons.set(0x22, "BAD_STORAGE_DATA"); +PanicReasons.set(0x31, "STACK_UNDERFLOW"); +PanicReasons.set(0x32, "ARRAY_RANGE_ERROR"); +PanicReasons.set(0x41, "OUT_OF_MEMORY"); +PanicReasons.set(0x51, "UNINITIALIZED_FUNCTION_CALL"); + +export function getBuiltinCallException(action: CallExceptionAction, tx: { to?: null | string, from?: null | string, data?: string }, data: null | BytesLike): CallExceptionError { + let message = "missing revert data"; + + let reason: null | string = null; + const invocation = null; + let revert: null | { signature: string, name: string, args: Array } = null; + + if (data) { + message = "execution reverted"; + + const bytes = getBytes(data); + data = hexlify(data); + + if (bytes.length % 32 !== 4) { + message += " (could not decode reason; invalid data length)"; + + } else if (hexlify(bytes.slice(0, 4)) === "0x08c379a0") { + // Error(string) + try { + reason = defaultAbiCoder.decode([ "string" ], bytes.slice(4))[0] + revert = { + signature: "Error(string)", + name: "Error", + args: [ reason ] + }; + message += `: ${ JSON.stringify(reason) }`; + + } catch (error) { + console.log(error); + message += " (could not decode reason; invalid data)"; + } + + } else if (hexlify(bytes.slice(0, 4)) === "0x4e487b71") { + // Panic(uint256) + try { + const code = Number(defaultAbiCoder.decode([ "uint256" ], bytes.slice(4))[0]); + revert = { + signature: "Panic(uint256)", + name: "Panic", + args: [ code ] + }; + reason = `Panic due to ${ PanicReasons.get(code) || "UNKNOWN" }(${ code })`; + message += `: ${ reason }`; + } catch (error) { + console.log(error); + message += " (could not decode panic reason)"; + } + } else { + message += " (unknown custom error)"; + } + } + + const transaction: CallExceptionTransaction = { + to: (tx.to ? getAddress(tx.to): null), + data: (tx.data || "0x") + }; + if (tx.from) { transaction.from = getAddress(tx.from); } + + return makeError(message, "CALL_EXCEPTION", { + action, data, reason, transaction, invocation, revert + }); +} export const defaultAbiCoder: AbiCoder = new AbiCoder(); diff --git a/src.ts/abi/fragments.ts b/src.ts/abi/fragments.ts index 09d60b470..164962e76 100644 --- a/src.ts/abi/fragments.ts +++ b/src.ts/abi/fragments.ts @@ -613,7 +613,8 @@ export class ParamType { return value[param.name]; }); } - if (value.length !== this.components.length) { + + if (result.length !== this.components.length) { throw new Error("array is wrong length"); } @@ -718,7 +719,7 @@ export class ParamType { return new ParamType(_guard, name, type, "array", indexed, null, arrayLength, arrayChildren); } - if (type.substring(0, 5) === "tuple(" || type[0] === "(") { + if (type === "tuple" || type.substring(0, 5) === "tuple(" || type[0] === "(") { const comps = (obj.components != null) ? obj.components.map((c: any) => ParamType.from(c)): null; const tuple = new ParamType(_guard, name, type, "tuple", indexed, comps, null, null); // @TODO: use lexer to validate and normalize type diff --git a/src.ts/abi/index.ts b/src.ts/abi/index.ts index 91b59d262..a366b69c5 100644 --- a/src.ts/abi/index.ts +++ b/src.ts/abi/index.ts @@ -4,7 +4,8 @@ ////// export { AbiCoder, - defaultAbiCoder + defaultAbiCoder, + getBuiltinCallException } from "./abi-coder.js"; export { decodeBytes32String, encodeBytes32String } from "./bytes32.js"; diff --git a/src.ts/abi/interface.ts b/src.ts/abi/interface.ts index d2f9055e0..4e3aa9787 100644 --- a/src.ts/abi/interface.ts +++ b/src.ts/abi/interface.ts @@ -1,17 +1,17 @@ import { keccak256 } from "../crypto/index.js" import { id } from "../hash/index.js" import { - concat, dataSlice, getBigInt, getBytes, getBytesCopy, hexlify, - zeroPadValue, isHexString, defineProperties, throwArgumentError, toHex, - throwError, makeError + concat, dataSlice, getBigInt, getBytes, getBytesCopy, + hexlify, zeroPadValue, isHexString, defineProperties, throwArgumentError, toHex, + throwError } from "../utils/index.js"; -import { AbiCoder, defaultAbiCoder } from "./abi-coder.js"; +import { AbiCoder, defaultAbiCoder, getBuiltinCallException } from "./abi-coder.js"; import { checkResultErrors, Result } from "./coders/abstract-coder.js"; import { ConstructorFragment, ErrorFragment, EventFragment, Fragment, FunctionFragment, ParamType } from "./fragments.js"; import { Typed } from "./typed.js"; -import type { BigNumberish, BytesLike } from "../utils/index.js"; +import type { BigNumberish, BytesLike, CallExceptionError, CallExceptionTransaction } from "../utils/index.js"; import type { JsonFragment } from "./fragments.js"; @@ -662,61 +662,45 @@ export class Interface { }); } - makeError(fragment: FunctionFragment | string, _data: BytesLike, tx?: { data: string }): Error { - if (typeof(fragment) === "string") { fragment = this.getFunction(fragment); } + makeError(_data: BytesLike, tx: CallExceptionTransaction): CallExceptionError { + const data = getBytes(_data, "data"); - const data = getBytes(_data); + const error = getBuiltinCallException("call", tx, data); - let args: undefined | Result = undefined; - if (tx) { - try { - args = this.#abiCoder.decode(fragment.inputs, tx.data || "0x"); - } catch (error) { console.log(error); } - } - - let errorArgs: undefined | Result = undefined; - let errorName: undefined | string = undefined; - let errorSignature: undefined | string = undefined; - let reason: string = "unknown reason"; - - if (data.length === 0) { - reason = "missing error reason"; - - } else if ((data.length % 32) === 4) { + // Not a built-in error; try finding a custom error + if (!error.message.match(/could not decode/)) { const selector = hexlify(data.slice(0, 4)); - const builtin = BuiltinErrors[selector]; - if (builtin) { + + error.message = "execution reverted (unknown custom error)"; + try { + const ef = this.getError(selector); try { - errorName = builtin.name; - errorSignature = builtin.signature; - errorArgs = this.#abiCoder.decode(builtin.inputs, data.slice(4)); - reason = builtin.reason(...errorArgs); - } catch (error) { - console.log(error); // @TODO: remove - } - } else { - reason = "unknown custom error"; - try { - const error = this.getError(selector); - errorName = error.name; - errorSignature = error.format(); - reason = `custom error: ${ errorSignature }`; - try { - errorArgs = this.#abiCoder.decode(error.inputs, data.slice(4)); - } catch (error) { - reason = `custom error: ${ errorSignature } (coult not decode error data)` - } - } catch (error) { - console.log(error); // @TODO: remove + error.revert = { + name: ef.name, + signature: ef.format(), + args: this.#abiCoder.decode(ef.inputs, data.slice(4)) + }; + error.reason = error.revert.signature; + error.message = `execution reverted: ${ error.reason }` + } catch (e) { + error.message = `execution reverted (coult not decode custom error)` } + } catch (error) { + console.log(error); // @TODO: remove } } - return makeError("call revert exception", "CALL_EXCEPTION", { - data: hexlify(data), transaction: null, - method: fragment.name, signature: fragment.format(), args, - errorArgs, errorName, errorSignature, reason - }); + // Add the invocation, if available + const parsed = this.parseTransaction(tx); + if (parsed) { + error.invocation = { + method: parsed.name, + signature: parsed.signature, + args: parsed.args + }; + } + + return error; } /**