Skip to content

Commit 4ed8e2a

Browse files
chore: cherry picked PR#2834 (#2845)
fix: `eth_estimateGas` opts to fallback estimation for contract revert (#2834) * fix: `eth_estimateGas` opts to fallback estimation when a contract reverts * fix: `eth_estimateGas` opts to fallback estimation when a contract reverts * fix: `eth_estimateGas` opts to fallback estimation when a contract reverts * test: extend tests in eth_estimateGas.spec.ts to cover the new logic * test: fix failing postman tests due to missing 'from' field * fix: throw predefined.CONTRACT_REVERT only for CONTRACT_REVERT_EXECUTED error message * fix: use `Status.ContractRevertExecuted` from `@hashgraph/sdk` --------- Signed-off-by: Victor Yanev <[email protected]> Signed-off-by: Logan Nguyen <[email protected]> Co-authored-by: Victor Yanev <[email protected]>
1 parent 67d0d90 commit 4ed8e2a

File tree

6 files changed

+167
-6
lines changed

6 files changed

+167
-6
lines changed

packages/relay/src/lib/errors/JsonRpcError.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,20 @@ export class JsonRpcError {
3333
}
3434

3535
export const predefined = {
36-
CONTRACT_REVERT: (errorMessage?: string, data: string = '') =>
37-
new JsonRpcError({
36+
CONTRACT_REVERT: (errorMessage?: string, data: string = '') => {
37+
let message: string;
38+
if (errorMessage?.length) {
39+
message = `execution reverted: ${decodeErrorMessage(errorMessage)}`;
40+
} else {
41+
const decodedData = decodeErrorMessage(data);
42+
message = decodedData.length ? `execution reverted: ${decodedData}` : 'execution reverted';
43+
}
44+
return new JsonRpcError({
3845
code: 3,
39-
message: `execution reverted: ${decodeErrorMessage(errorMessage)}`,
40-
data: data,
41-
}),
46+
message,
47+
data,
48+
});
49+
},
4250
GAS_LIMIT_TOO_HIGH: (gasLimit, maxGas) =>
4351
new JsonRpcError({
4452
code: -32005,

packages/relay/src/lib/errors/MirrorNodeClientError.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*
1919
*/
2020

21+
import { Status } from '@hashgraph/sdk';
22+
2123
export class MirrorNodeClientError extends Error {
2224
public statusCode: number;
2325
public data?: string;
@@ -37,6 +39,7 @@ export class MirrorNodeClientError extends Error {
3739

3840
static messages = {
3941
INVALID_HEX: 'data field invalid hexadecimal string',
42+
CONTRACT_REVERT_EXECUTED: Status.ContractRevertExecuted.toString(),
4043
};
4144

4245
constructor(error: any, statusCode: number) {
@@ -64,6 +67,10 @@ export class MirrorNodeClientError extends Error {
6467
return this.statusCode === MirrorNodeClientError.ErrorCodes.CONTRACT_REVERT_EXECUTED;
6568
}
6669

70+
public isContractRevertOpcodeExecuted() {
71+
return this.message === MirrorNodeClientError.messages.CONTRACT_REVERT_EXECUTED;
72+
}
73+
6774
public isNotFound(): boolean {
6875
return this.statusCode === MirrorNodeClientError.statusCodes.NOT_FOUND;
6976
}

packages/relay/src/lib/eth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,12 +592,17 @@ export class EthImpl implements Eth {
592592
this.logger.info(`${requestIdPrefix} Returning gas: ${response.result}`);
593593
return prepend0x(trimPrecedingZeros(response.result));
594594
} else {
595+
this.logger.error(`${requestIdPrefix} No gas estimate returned from mirror-node: ${JSON.stringify(response)}`);
595596
return this.predefinedGasForTransaction(transaction, requestIdPrefix);
596597
}
597598
} catch (e: any) {
598599
this.logger.error(
599600
`${requestIdPrefix} Error raised while fetching estimateGas from mirror-node: ${JSON.stringify(e)}`,
600601
);
602+
// in case of contract revert, we don't want to return a predefined gas but the actual error with the reason
603+
if (this.estimateGasThrows && e instanceof MirrorNodeClientError && e.isContractRevertOpcodeExecuted()) {
604+
return predefined.CONTRACT_REVERT(e.detail ?? e.message, e.data);
605+
}
601606
return this.predefinedGasForTransaction(transaction, requestIdPrefix, e);
602607
}
603608
}

packages/relay/tests/lib/errors/JsonRpcError.spec.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
*/
2020

2121
import { expect } from 'chai';
22-
import { JsonRpcError } from '../../../src/lib/errors/JsonRpcError';
22+
import { JsonRpcError, predefined } from '../../../src';
23+
import { AbiCoder, keccak256 } from 'ethers';
2324

2425
describe('Errors', () => {
2526
describe('JsonRpcError', () => {
@@ -50,5 +51,84 @@ describe('Errors', () => {
5051
// Check that request ID is prefixed
5152
expect(err.message).to.eq('[Request ID: abcd-1234] test error: foo');
5253
});
54+
55+
describe('predefined.CONTRACT_REVERT', () => {
56+
const defaultErrorSignature = keccak256(Buffer.from('Error(string)')).slice(0, 10); // 0x08c379a0
57+
const customErrorSignature = keccak256(Buffer.from('CustomError(string)')).slice(0, 10); // 0x8d6ea8be
58+
const decodedMessage = 'Some error message';
59+
const encodedMessage = new AbiCoder().encode(['string'], [decodedMessage]).replace('0x', '');
60+
const encodedCustomError = customErrorSignature + encodedMessage;
61+
const encodedDefaultError = defaultErrorSignature + encodedMessage;
62+
63+
it('Returns decoded message when decoded message is provided as errorMessage and encoded default error is provided as data', () => {
64+
const error = predefined.CONTRACT_REVERT(decodedMessage, encodedDefaultError);
65+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
66+
});
67+
68+
it('Returns decoded message when decoded message is provided as errorMessage and encoded custom error is provided as data', () => {
69+
const error = predefined.CONTRACT_REVERT(decodedMessage, encodedCustomError);
70+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
71+
});
72+
73+
it('Returns decoded message when encoded default error is provided as errorMessage and data', () => {
74+
const error = predefined.CONTRACT_REVERT(encodedDefaultError, encodedDefaultError);
75+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
76+
});
77+
78+
it('Returns decoded message when encoded custom error is provided as errorMessage and data', () => {
79+
const error = predefined.CONTRACT_REVERT(encodedCustomError, encodedCustomError);
80+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
81+
});
82+
83+
it('Returns decoded message when decoded errorMessage is provided', () => {
84+
const error = predefined.CONTRACT_REVERT(decodedMessage);
85+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
86+
});
87+
88+
it('Returns decoded message when encoded default error is provided as errorMessage', () => {
89+
const error = predefined.CONTRACT_REVERT(encodedDefaultError);
90+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
91+
});
92+
93+
it('Returns decoded message when encoded custom error is provided as errorMessage', () => {
94+
const error = predefined.CONTRACT_REVERT(encodedCustomError);
95+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
96+
});
97+
98+
it('Returns decoded message when encoded default error is provided as data', () => {
99+
const error = predefined.CONTRACT_REVERT(undefined, encodedDefaultError);
100+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
101+
});
102+
103+
it('Returns decoded message when encoded custom error is provided as data', () => {
104+
const error = predefined.CONTRACT_REVERT(undefined, encodedCustomError);
105+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
106+
});
107+
108+
it('Returns decoded message when message is empty and encoded default error is provided as data', () => {
109+
const error = predefined.CONTRACT_REVERT('', encodedDefaultError);
110+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
111+
});
112+
113+
it('Returns decoded message when message is empty and encoded custom error is provided as data', () => {
114+
const error = predefined.CONTRACT_REVERT('', encodedCustomError);
115+
expect(error.message).to.eq(`execution reverted: ${decodedMessage}`);
116+
});
117+
118+
it('Returns default message when errorMessage is empty', () => {
119+
const error = predefined.CONTRACT_REVERT('');
120+
expect(error.message).to.eq('execution reverted');
121+
});
122+
123+
it('Returns default message when data is empty', () => {
124+
const error = predefined.CONTRACT_REVERT(undefined, '');
125+
expect(error.message).to.eq('execution reverted');
126+
});
127+
128+
it('Returns default message when neither errorMessage nor data is provided', () => {
129+
const error = predefined.CONTRACT_REVERT();
130+
expect(error.message).to.eq('execution reverted');
131+
});
132+
});
53133
});
54134
});

packages/relay/tests/lib/eth/eth_estimateGas.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { Precheck } from '../../../src/lib/precheck';
3232
import { createStubInstance, stub, SinonStub, SinonStubbedInstance } from 'sinon';
3333
import { IContractCallRequest, IContractCallResponse } from '../../../src/lib/types/IMirrorNode';
3434
import { DEFAULT_NETWORK_FEES, NO_TRANSACTIONS, ONE_TINYBAR_IN_WEI_HEX, RECEIVER_ADDRESS } from './eth-config';
35+
import { AbiCoder, keccak256 } from 'ethers';
3536

3637
dotenv.config({ path: path.resolve(__dirname, '../test.env') });
3738
use(chaiAsPromised);
@@ -431,6 +432,54 @@ describe('@ethEstimateGas Estimate Gas spec', async function () {
431432
expect(result.message).to.equal('execution reverted: Invalid number of recipients');
432433
});
433434

435+
it('should eth_estimateGas with contract revert for contract call and custom contract error', async function () {
436+
const decodedMessage = 'Some error message';
437+
const customErrorSignature = keccak256(Buffer.from('CustomError(string)')).slice(0, 10); // 0x8d6ea8be
438+
const encodedMessage = new AbiCoder().encode(['string'], [decodedMessage]).replace('0x', '');
439+
const encodedCustomError = customErrorSignature + encodedMessage;
440+
441+
web3Mock.onPost('contracts/call', { ...transaction, estimate: true }).reply(400, {
442+
_status: {
443+
messages: [
444+
{
445+
message: 'CONTRACT_REVERT_EXECUTED',
446+
detail: decodedMessage,
447+
data: encodedCustomError,
448+
},
449+
],
450+
},
451+
});
452+
453+
const result: any = await ethImpl.estimateGas(transaction, id);
454+
455+
expect(result.data).to.equal(encodedCustomError);
456+
expect(result.message).to.equal(`execution reverted: ${decodedMessage}`);
457+
});
458+
459+
it('should eth_estimateGas with contract revert for contract call and generic revert error', async function () {
460+
const decodedMessage = 'Some error message';
461+
const defaultErrorSignature = keccak256(Buffer.from('Error(string)')).slice(0, 10); // 0x08c379a0
462+
const encodedMessage = new AbiCoder().encode(['string'], [decodedMessage]).replace('0x', '');
463+
const encodedGenericError = defaultErrorSignature + encodedMessage;
464+
465+
web3Mock.onPost('contracts/call', { ...transaction, estimate: true }).reply(400, {
466+
_status: {
467+
messages: [
468+
{
469+
message: 'CONTRACT_REVERT_EXECUTED',
470+
detail: decodedMessage,
471+
data: encodedGenericError,
472+
},
473+
],
474+
},
475+
});
476+
477+
const result: any = await ethImpl.estimateGas(transaction, id);
478+
479+
expect(result.data).to.equal(encodedGenericError);
480+
expect(result.message).to.equal(`execution reverted: ${decodedMessage}`);
481+
});
482+
434483
it('should eth_estimateGas handles a 501 unimplemented response from the mirror node correctly by returning default gas', async function () {
435484
web3Mock.onPost('contracts/call', { ...transaction, estimate: true }).reply(501, {
436485
_status: {

packages/relay/tests/lib/formatters.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
} from '../../src/formatters';
4444
import constants from '../../src/lib/constants';
4545
import { BigNumber as BN } from 'bignumber.js';
46+
import { AbiCoder, keccak256 } from 'ethers';
4647

4748
describe('Formatters', () => {
4849
describe('formatRequestIdMessage', () => {
@@ -639,6 +640,17 @@ describe('Formatters', () => {
639640
'0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000';
640641
expect(decodeErrorMessage(hexErrorMessage)).to.equal('');
641642
});
643+
644+
it('should return empty string for custom error message without parameters', () => {
645+
expect(decodeErrorMessage('0x858d70bd')).to.equal('');
646+
});
647+
648+
it('should return the message of custom error with string parameter', () => {
649+
const signature = keccak256(Buffer.from('CustomError(string)')).slice(0, 10); // 0x8d6ea8be
650+
const message = new AbiCoder().encode(['string'], ['Some error message']).replace('0x', '');
651+
const hexErrorMessage = signature + message;
652+
expect(decodeErrorMessage(hexErrorMessage)).to.equal('Some error message');
653+
});
642654
});
643655
});
644656
});

0 commit comments

Comments
 (0)