Skip to content

Commit

Permalink
Support for EIP-838 custom contract errors (#1498).
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo committed Apr 23, 2021
1 parent 8e22e02 commit 6519609
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 17 deletions.
87 changes: 84 additions & 3 deletions packages/abi/src.ts/fragments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ export abstract class Fragment {
case "constructor":
return ConstructorFragment.fromObject(value);
case "error":
return ErrorFragment.fromObject(value);
case "fallback":
case "receive":
// @TODO: Something? Maybe return a FunctionFragment? A custom DefaultFunctionFragment?
Expand All @@ -467,6 +468,8 @@ export abstract class Fragment {
return FunctionFragment.fromString(value.substring(8).trim());
} else if (value.split("(")[0].trim() === "constructor") {
return ConstructorFragment.fromString(value.trim());
} else if (value.split(" ")[0] === "error") {
return ErrorFragment.fromString(value.substring(5).trim());
}

return logger.throwArgumentError("unsupported fragment", "value", value);
Expand Down Expand Up @@ -928,12 +931,90 @@ export class FunctionFragment extends ConstructorFragment {
}
}

//export class ErrorFragment extends Fragment {
//}

//export class StructFragment extends Fragment {
//}

function checkForbidden(fragment: ErrorFragment): ErrorFragment {
const sig = fragment.format();
if (sig === "Error(string)" || sig === "Panic(uint256)") {
logger.throwArgumentError(`cannot specify user defined ${ sig } error`, "fragment", fragment);
}
return fragment;
}

export class ErrorFragment extends Fragment {

format(format?: string): string {
if (!format) { format = FormatTypes.sighash; }
if (!FormatTypes[format]) {
logger.throwArgumentError("invalid format type", "format", format);
}

if (format === FormatTypes.json) {
return JSON.stringify({
type: "error",
name: this.name,
inputs: this.inputs.map((input) => JSON.parse(input.format(format))),
});
}

let result = "";

if (format !== FormatTypes.sighash) {
result += "error ";
}

result += this.name + "(" + this.inputs.map(
(input) => input.format(format)
).join((format === FormatTypes.full) ? ", ": ",") + ") ";

return result.trim();
}

static from(value: ErrorFragment | JsonFragment | string): ErrorFragment {
if (typeof(value) === "string") {
return ErrorFragment.fromString(value);
}
return ErrorFragment.fromObject(value);
}

static fromObject(value: ErrorFragment | JsonFragment): ErrorFragment {
if (ErrorFragment.isErrorFragment(value)) { return value; }

if (value.type !== "error") {
logger.throwArgumentError("invalid error object", "value", value);
}

const params: TypeCheck<_Fragment> = {
type: value.type,
name: verifyIdentifier(value.name),
inputs: (value.inputs ? value.inputs.map(ParamType.fromObject): [])
};

return checkForbidden(new ErrorFragment(_constructorGuard, params));
}

static fromString(value: string): ErrorFragment {
let params: any = { type: "error" };

let parens = value.match(regexParen);
if (!parens) {
logger.throwArgumentError("invalid error signature", "value", value);
}

params.name = parens[1].trim();
if (params.name) { verifyIdentifier(params.name); }

params.inputs = parseParams(parens[2], false);

return checkForbidden(ErrorFragment.fromObject(params));
}

static isErrorFragment(value: any): value is ErrorFragment {
return (value && value._isFragment && value.type === "error");
}
}

function verifyType(type: string): string {

// These need to be transformed to their full description
Expand Down
3 changes: 2 additions & 1 deletion packages/abi/src.ts/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"use strict";

import { ConstructorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, JsonFragment, JsonFragmentType, ParamType } from "./fragments";
import { ConstructorFragment, ErrorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, JsonFragment, JsonFragmentType, ParamType } from "./fragments";
import { AbiCoder, CoerceFunc, defaultAbiCoder } from "./abi-coder";
import { checkResultErrors, Indexed, Interface, LogDescription, Result, TransactionDescription } from "./interface";

export {
ConstructorFragment,
ErrorFragment,
EventFragment,
Fragment,
FunctionFragment,
Expand Down
80 changes: 68 additions & 12 deletions packages/abi/src.ts/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { defineReadOnly, Description, getStatic } from "@ethersproject/propertie

import { AbiCoder, defaultAbiCoder } from "./abi-coder";
import { checkResultErrors, Result } from "./coders/abstract-coder";
import { ConstructorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, JsonFragment, ParamType } from "./fragments";
import { ConstructorFragment, ErrorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, JsonFragment, ParamType } from "./fragments";

import { Logger } from "@ethersproject/logger";
import { version } from "./_version";
Expand Down Expand Up @@ -43,6 +43,11 @@ export class Indexed extends Description<Indexed> {
}
}

const BuiltinErrors: Record<string, { signature: string, inputs: Array<string>, reason?: boolean }> = {
"0x08c379a0": { signature: "Error(string)", inputs: [ "string" ], reason: true },
"0x4e487b71": { signature: "Panic(uint256)", inputs: [ "uint256" ] }
}

function wrapAccessError(property: string, error: Error): Error {
const wrap = new Error(`deferred error during ABI decoding triggered accessing ${ property }`);
(<any>wrap).error = error;
Expand All @@ -65,7 +70,7 @@ function checkNames(fragment: Fragment, type: "input" | "output", params: Array<
export class Interface {
readonly fragments: ReadonlyArray<Fragment>;

readonly errors: { [ name: string ]: any };
readonly errors: { [ name: string ]: ErrorFragment };
readonly events: { [ name: string ]: EventFragment };
readonly functions: { [ name: string ]: FunctionFragment };
readonly structs: { [ name: string ]: any };
Expand Down Expand Up @@ -118,6 +123,9 @@ export class Interface {
//checkNames(fragment, "input", fragment.inputs);
bucket = this.events;
break;
case "error":
bucket = this.errors;
break;
default:
return;
}
Expand Down Expand Up @@ -167,8 +175,8 @@ export class Interface {
return getAddress(address);
}

static getSighash(functionFragment: FunctionFragment): string {
return hexDataSlice(id(functionFragment.format()), 0, 4);
static getSighash(fragment: ErrorFragment | FunctionFragment): string {
return hexDataSlice(id(fragment.format()), 0, 4);
}

static getEventTopic(eventFragment: EventFragment): string {
Expand Down Expand Up @@ -240,6 +248,40 @@ export class Interface {
return result;
}

// Find a function definition by any means necessary (unless it is ambiguous)
getError(nameOrSignatureOrSighash: string): ErrorFragment {
if (isHexString(nameOrSignatureOrSighash)) {
const getSighash = getStatic<(f: ErrorFragment | FunctionFragment) => string>(this.constructor, "getSighash");
for (const name in this.errors) {
const error = this.errors[name];
if (nameOrSignatureOrSighash === getSighash(error)) {
return this.errors[name];
}
}
logger.throwArgumentError("no matching error", "sighash", nameOrSignatureOrSighash);
}

// It is a bare name, look up the function (will return null if ambiguous)
if (nameOrSignatureOrSighash.indexOf("(") === -1) {
const name = nameOrSignatureOrSighash.trim();
const matching = Object.keys(this.errors).filter((f) => (f.split("("/* fix:) */)[0] === name));
if (matching.length === 0) {
logger.throwArgumentError("no matching error", "name", name);
} else if (matching.length > 1) {
logger.throwArgumentError("multiple matching errors", "name", name);
}

return this.errors[matching[0]];
}

// Normlize the signature and lookup the function
const result = this.errors[FunctionFragment.fromString(nameOrSignatureOrSighash).format()];
if (!result) {
logger.throwArgumentError("no matching error", "signature", nameOrSignatureOrSighash);
}
return result;
}

// Get the sighash (the bytes4 selector) used by Solidity to identify a function
getSighash(functionFragment: FunctionFragment | string): string {
if (typeof(functionFragment) === "string") {
Expand Down Expand Up @@ -304,9 +346,10 @@ export class Interface {
functionFragment = this.getFunction(functionFragment);
}

let bytes = arrayify(data);
let bytes = arrayify(data);

let reason: string = null;
let errorArgs: Result = null;
let errorSignature: string = null;
switch (bytes.length % this._abiCoder._getWordSize()) {
case 0:
Expand All @@ -315,19 +358,29 @@ export class Interface {
} catch (error) { }
break;

case 4:
if (hexlify(bytes.slice(0, 4)) === "0x08c379a0") {
errorSignature = "Error(string)";
reason = this._abiCoder.decode([ "string" ], bytes.slice(4))[0];
case 4: {
const selector = hexlify(bytes.slice(0, 4));
const builtin = BuiltinErrors[selector];
if (builtin) {
errorSignature = builtin.signature;
errorArgs = this._abiCoder.decode(builtin.inputs, bytes.slice(4));
if (builtin.reason) { reason = errorArgs[0]; }
} else {
try {
const error = this.getError(selector);
errorSignature = error.format();
errorArgs = this._abiCoder.decode(error.inputs, bytes.slice(4));
} catch (error) {
console.log(error);
}
}
break;
}
}

return logger.throwError("call revert exception", Logger.errors.CALL_EXCEPTION, {
method: functionFragment.format(),
errorSignature: errorSignature,
errorArgs: [ reason ],
reason: reason
errorSignature, errorArgs, reason
});
}

Expand Down Expand Up @@ -547,6 +600,9 @@ export class Interface {
});
}

// @TODO
//parseCallResult(data: BytesLike): ??

// Given an event log, find the matching event fragment (if any) and
// determine all its properties and values
parseLog(log: { topics: Array<string>, data: string}): LogDescription {
Expand Down
3 changes: 2 additions & 1 deletion packages/ethers/src.ts/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";

import { AbiCoder, checkResultErrors, defaultAbiCoder, EventFragment, FormatTypes, Fragment, FunctionFragment, Indexed, Interface, LogDescription, ParamType, Result, TransactionDescription }from "@ethersproject/abi";
import { AbiCoder, checkResultErrors, defaultAbiCoder, ErrorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, Indexed, Interface, LogDescription, ParamType, Result, TransactionDescription }from "@ethersproject/abi";
import { getAddress, getCreate2Address, getContractAddress, getIcapAddress, isAddress } from "@ethersproject/address";
import * as base64 from "@ethersproject/base64";
import { Base58 as base58 } from "@ethersproject/basex";
Expand Down Expand Up @@ -49,6 +49,7 @@ export {
defaultAbiCoder,

Fragment,
ErrorFragment,
EventFragment,
FunctionFragment,
ParamType,
Expand Down
26 changes: 26 additions & 0 deletions packages/tests/src.ts/test-contract-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,29 @@ describe("Test ParamType Parser", function() {
});
});
});

describe('Test EIP-838 Error Codes', function() {
const addr = "0xbd0B4B009a76CA97766360F04f75e05A3E449f1E";
it("testError1", async function () {
const provider = new ethers.providers.InfuraProvider("ropsten", "49a0efa3aaee4fd99797bfa94d8ce2f1");
const contract = new ethers.Contract(addr, [
"function testError1(bool pass, address addr, uint256 value) pure returns (bool)",
"function testError2(bool pass, bytes data) pure returns (bool)",
"error TestError1(address addr, uint256 value)",
"error TestError2(bytes data)",
], provider);

try {
const result = await contract.testError1(false, addr, 42);
console.log(result);
assert.ok(false, "did not throw ");
} catch (error) {
assert.equal(error.code, ethers.utils.Logger.errors.CALL_EXCEPTION, "error.code");
assert.equal(error.errorSignature, "TestError1(address,uint256)", "error.errorSignature");
assert.equal(error.errorArgs[0], addr, "error.errorArgs[0]");
assert.equal(error.errorArgs.addr, addr, "error.errorArgs.addr");
assert.equal(error.errorArgs[1], 42, "error.errorArgs[1]");
assert.equal(error.errorArgs.value, 42, "error.errorArgs.value");
}
});
});

0 comments on commit 6519609

Please sign in to comment.