Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

fix: gethify eth call errors (#2133) #2186

Merged
merged 12 commits into from
Jan 25, 2022
4 changes: 4 additions & 0 deletions src/chains/ethereum/ethereum/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ type TypedData = Exclude<
//#endregion

//#region helpers
/**
* Combines RuntimeErrors for a list of rejected or reverted transactions.
* @param transactions Array of transactions with errors to assert.
*/
function assertExceptionalTransactions(transactions: TypedTransaction[]) {
let baseError: string = null;
let errors: string[];
Expand Down
6 changes: 2 additions & 4 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
TraceDataFactory,
TraceStorageMap,
RuntimeError,
RETURN_TYPES,
CallError,
StorageKeys,
StorageRangeResult,
StorageRecords,
Expand Down Expand Up @@ -1086,9 +1086,7 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
context: transactionContext
});
if (result.execResult.exceptionError) {
// eth_call transactions don't really have a transaction hash
const hash = RPCQUANTITY_EMPTY;
throw new RuntimeError(hash, result, RETURN_TYPES.RETURN_VALUE);
throw new CallError(result);
} else {
return Data.from(result.execResult.returnValue || "0x");
}
Expand Down
25 changes: 18 additions & 7 deletions src/chains/ethereum/ethereum/tests/api/eth/call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import getProvider from "../../helpers/getProvider";
import compile, { CompileOutput } from "../../helpers/compile";
import { join } from "path";
import { BUFFER_EMPTY, Quantity, RPCQUANTITY_EMPTY } from "@ganache/utils";
import { RETURN_TYPES, RuntimeError } from "@ganache/ethereum-utils";
import { CallError } from "@ganache/ethereum-utils";

describe("api", () => {
describe("eth", () => {
Expand Down Expand Up @@ -99,7 +99,7 @@ describe("api", () => {
assert.strictEqual(Quantity.from(result).toNumber(), 5);
});

it("rejects transactions that specify legacy and eip-1559 transaction fields", async () => {
it("rejects transactions that specify both legacy and eip-1559 transaction fields", async () => {
const tx = {
from,
to: contractAddress,
Expand Down Expand Up @@ -150,11 +150,7 @@ describe("api", () => {
// the vm error should propagate through to here
await assert.rejects(
ethCallProm,
new RuntimeError(
RPCQUANTITY_EMPTY,
result,
RETURN_TYPES.RETURN_VALUE
),
new CallError(result),
"didn't reject transaction with insufficient gas"
);
});
Expand All @@ -169,6 +165,21 @@ describe("api", () => {
const result = await provider.send("eth_call", [tx, "latest"]);
assert.strictEqual(BigInt(result), BigInt(block.baseFeePerGas));
});

it("returns string data property on revert error", async () => {
const tx = {
from,
to: contractAddress,
data: `0x${contract.contract.evm.methodIdentifiers["doARevert()"]}`
};
const revertString =
"0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000011796f75206172652061206661696c757265000000000000000000000000000000";
await assert.rejects(provider.send("eth_call", [tx, "latest"]), {
message:
"VM Exception while processing transaction: revert you are a failure",
data: revertString
});
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ contract EthCall {
return block.basefee;
}

function doARevert() public pure {
revert("you are a failure");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,12 @@ describe("api", () => {
"Error code should be -32000"
);
assert.strictEqual(
result.data.reason,
null,
"The reason is undecodable, and thus should be null"
);
assert.strictEqual(
result.data.message,
"revert",
result.message,
"VM Exception while processing transaction: revert",
"The message should not have a reason string included"
);
assert.strictEqual(
result.data.result,
result.data,
revertString,
"The revert reason should be encoded as hex"
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,17 +200,12 @@ describe("api", () => {
"Error code should be -32000"
);
assert.strictEqual(
result.data.reason,
null,
"The reason is undecodable, and thus should be null"
);
assert.strictEqual(
result.data.message,
"revert",
result.message,
"VM Exception while processing transaction: revert",
"The message should not have a reason string included"
);
assert.strictEqual(
result.data.result,
result.data,
revertString,
"The revert reason should be encoded as hex"
);
Expand Down
25 changes: 25 additions & 0 deletions src/chains/ethereum/utils/src/errors/call-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { EVMResult } from "@ethereumjs/vm/dist/evm/evm";
import { VM_EXCEPTION } from "./errors";
import { CodedError } from "./coded-error";
import { JsonRpcErrorCode } from "@ganache/utils";
import { Data } from "@ganache/utils";

export class CallError extends CodedError {
public code: JsonRpcErrorCode;
public data: string;
constructor(result: EVMResult) {
const execResult = result.execResult;
const error = execResult.exceptionError.error;
let message = VM_EXCEPTION + error;

super(message, JsonRpcErrorCode.INVALID_INPUT);

CodedError.captureStackTraceExtended.bind(this, message);
this.name = this.constructor.name;

const { returnValue } = execResult;
const reason = CodedError.createRevertReason(returnValue);
this.message = reason ? message + " " + reason : message;
this.data = Data.from(returnValue).toString();
}
}
22 changes: 22 additions & 0 deletions src/chains/ethereum/utils/src/errors/coded-error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ExecResult } from "@ethereumjs/vm/dist/evm/evm";
import { JsonRpcErrorCode } from "@ganache/utils";
import { rawDecode } from "ethereumjs-abi";

const REVERT_REASON = Buffer.from("08c379a0", "hex"); // keccak("Error(string)").slice(0, 4)
export class CodedError extends Error {
code: number;
constructor(message: string, code: number) {
Expand Down Expand Up @@ -42,4 +45,23 @@ export class CodedError extends Error {
);
}
}
static createRevertReason(returnValue: Buffer) {
let reason: string | null;
if (
returnValue.length > 4 &&
REVERT_REASON.compare(returnValue, 0, 4) === 0
) {
try {
// it is possible for the `returnValue` to be gibberish that can't be
// decoded. See: https://github.com/trufflesuite/ganache/pull/452
reason = rawDecode(["bytes"], returnValue.slice(4))[0].toString();
} catch {
// ignore error since reason string recover is impossible
reason = null;
}
} else {
reason = null;
}
return reason;
}
}
25 changes: 3 additions & 22 deletions src/chains/ethereum/utils/src/errors/runtime-error.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { EVMResult } from "@ethereumjs/vm/dist/evm/evm";
import { VM_EXCEPTION } from "./errors";
import { Data } from "@ganache/utils";
import { rawDecode } from "ethereumjs-abi";
import { CodedError } from "./coded-error";
import { JsonRpcErrorCode } from "@ganache/utils";

const REVERT_REASON = Buffer.from("08c379a0", "hex"); // keccak("Error(string)").slice(0, 4)

export enum RETURN_TYPES {
TRANSACTION_HASH,
RETURN_VALUE
Expand Down Expand Up @@ -35,27 +32,11 @@ export class RuntimeError extends CodedError {
CodedError.captureStackTraceExtended.bind(this, message);
this.name = this.constructor.name;

const returnValue = execResult.returnValue;
const hash = transactionHash.toString();
let reason: string | null;
if (
returnValue.length > 4 &&
REVERT_REASON.compare(returnValue, 0, 4) === 0
) {
try {
// it is possible for the `returnValue` to be gibberish that can't be
// decoded. See: https://github.com/trufflesuite/ganache/pull/452
reason = rawDecode(["bytes"], returnValue.slice(4))[0].toString();
message += " " + reason;
} catch {
// ignore error since reason string recover is impossible
reason = null;
}
} else {
reason = null;
}
const { returnValue } = execResult;
const reason = CodedError.createRevertReason(returnValue);
this.message = reason ? message + " " + reason : message;

this.message = message;
this.data = {
hash: hash,
programCounter: execResult.runState.programCounter,
Expand Down
1 change: 1 addition & 0 deletions src/chains/ethereum/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./errors/coded-error";
export * from "./errors/errors";
export * from "./errors/runtime-error";
export * from "./errors/call-error";
export * from "./errors/abort-error";

export * from "./things/account";
Expand Down