Added convenience functions for creating CALL_EXCEPTION Errors.

This commit is contained in:
Richard Moore 2022-10-20 04:55:25 -04:00
parent a667dcbebe
commit 9c45482fad
4 changed files with 123 additions and 56 deletions

@ -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<number, string> = 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<any> } = 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();

@ -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

@ -4,7 +4,8 @@
//////
export {
AbiCoder,
defaultAbiCoder
defaultAbiCoder,
getBuiltinCallException
} from "./abi-coder.js";
export { decodeBytes32String, encodeBytes32String } from "./bytes32.js";

@ -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 {
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";
const ef = this.getError(selector);
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)`
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;
}
/**