Skip to content

Commit 69fd896

Browse files
feat: introduce operator fee for OP stack networks
1 parent 3656f13 commit 69fd896

File tree

3 files changed

+190
-29
lines changed

3 files changed

+190
-29
lines changed

packages/transaction-controller/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- Add optional `isIntentComplete` property to `TransactionMeta` to indicate transaction outcome was achieved via an alternate chain or mechanism ([#6950](https://github.com/MetaMask/core/pull/6950))
13+
- Calculate operator fee for OP stack networks and include it in `layer1GasFee` ([#6979](https://github.com/MetaMask/core/pull/6979))
1314

1415
### Changed
1516

packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.test.ts

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import type { TypedTransaction } from '@ethereumjs/tx';
22
import { TransactionFactory } from '@ethereumjs/tx';
33
import { Contract } from '@ethersproject/contracts';
44
import type { Provider } from '@metamask/network-controller';
5-
import type { Hex } from '@metamask/utils';
6-
7-
import { OracleLayer1GasFeeFlow } from './OracleLayer1GasFeeFlow';
5+
import { add0x, type Hex } from '@metamask/utils';
6+
import BN from 'bn.js';
87
import { CHAIN_IDS } from '../constants';
98
import type { TransactionControllerMessenger } from '../TransactionController';
109
import type { Layer1GasFeeFlowRequest, TransactionMeta } from '../types';
1110
import { TransactionStatus } from '../types';
11+
import { padHexToEvenLength } from '../utils/utils';
12+
import { OracleLayer1GasFeeFlow } from './OracleLayer1GasFeeFlow';
1213

1314
jest.mock('@ethersproject/contracts', () => ({
1415
Contract: jest.fn(),
@@ -36,7 +37,8 @@ const TRANSACTION_META_MOCK: TransactionMeta = {
3637

3738
const SERIALIZED_TRANSACTION_MOCK = '0x1234';
3839
const ORACLE_ADDRESS_MOCK = '0x5678' as Hex;
39-
const LAYER_1_FEE_MOCK = '0x9ABCD';
40+
const LAYER_1_FEE_MOCK = '0x09abcd';
41+
const OPERATOR_FEE_MOCK = '0x5';
4042
const DEFAULT_GAS_PRICE_ORACLE_ADDRESS =
4143
'0x420000000000000000000000000000000000000F';
4244

@@ -98,9 +100,9 @@ class DefaultOracleLayer1GasFeeFlow extends OracleLayer1GasFeeFlow {
98100

99101
describe('OracleLayer1GasFeeFlow', () => {
100102
const contractMock = jest.mocked(Contract);
101-
const contractGetL1FeeMock: jest.MockedFn<
102-
() => Promise<{ toHexString: () => string }>
103-
> = jest.fn();
103+
const contractGetL1FeeMock: jest.MockedFn<() => Promise<BN>> = jest.fn();
104+
const contractGetOperatorFeeMock: jest.MockedFn<() => Promise<BN>> =
105+
jest.fn();
104106

105107
let request: Layer1GasFeeFlowRequest;
106108

@@ -112,13 +114,14 @@ describe('OracleLayer1GasFeeFlow', () => {
112114

113115
contractMock.mockClear();
114116
contractGetL1FeeMock.mockClear();
117+
contractGetOperatorFeeMock.mockClear();
115118

116-
contractGetL1FeeMock.mockResolvedValue({
117-
toHexString: () => LAYER_1_FEE_MOCK,
118-
});
119+
contractGetL1FeeMock.mockResolvedValue(new BN(LAYER_1_FEE_MOCK));
120+
contractGetOperatorFeeMock.mockResolvedValue(new BN(0));
119121

120122
contractMock.mockReturnValue({
121123
getL1Fee: contractGetL1FeeMock,
124+
getOperatorFee: contractGetOperatorFeeMock,
122125
} as unknown as Contract);
123126
});
124127

@@ -156,6 +159,7 @@ describe('OracleLayer1GasFeeFlow', () => {
156159
expect(contractGetL1FeeMock).toHaveBeenCalledWith(
157160
serializedTransactionMock,
158161
);
162+
expect(contractGetOperatorFeeMock).not.toHaveBeenCalled();
159163
});
160164

161165
it('signs transaction with dummy key if supported by flow', async () => {
@@ -180,6 +184,7 @@ describe('OracleLayer1GasFeeFlow', () => {
180184
});
181185

182186
expect(typedTransactionMock.sign).toHaveBeenCalledTimes(1);
187+
expect(contractGetOperatorFeeMock).not.toHaveBeenCalled();
183188
});
184189

185190
describe('throws', () => {
@@ -228,5 +233,79 @@ describe('OracleLayer1GasFeeFlow', () => {
228233
expect(oracleAddress).toBe(DEFAULT_GAS_PRICE_ORACLE_ADDRESS);
229234
expect(typedTransactionMock.sign).not.toHaveBeenCalled();
230235
});
236+
237+
it('adds operator fee when gas used is available', async () => {
238+
const gasUsed = '0x5208';
239+
request = {
240+
...request,
241+
transactionMeta: {
242+
...request.transactionMeta,
243+
gasUsed,
244+
},
245+
};
246+
247+
contractGetOperatorFeeMock.mockResolvedValueOnce(
248+
new BN(OPERATOR_FEE_MOCK),
249+
);
250+
251+
const flow = new MockOracleLayer1GasFeeFlow(false);
252+
const response = await flow.getLayer1Fee(request);
253+
254+
expect(contractGetOperatorFeeMock).toHaveBeenCalledTimes(1);
255+
expect(contractGetOperatorFeeMock).toHaveBeenCalledWith(gasUsed);
256+
expect(response).toStrictEqual({
257+
layer1Fee: add0x(
258+
padHexToEvenLength(
259+
new BN(LAYER_1_FEE_MOCK)
260+
.add(new BN(OPERATOR_FEE_MOCK))
261+
.toString(16),
262+
),
263+
) as Hex,
264+
});
265+
});
266+
267+
it('defaults operator fee to zero when call fails', async () => {
268+
const gasUsed = '0x1';
269+
request = {
270+
...request,
271+
transactionMeta: {
272+
...request.transactionMeta,
273+
gasUsed,
274+
},
275+
};
276+
277+
contractGetOperatorFeeMock.mockRejectedValueOnce(new Error('revert'));
278+
279+
const flow = new MockOracleLayer1GasFeeFlow(false);
280+
const response = await flow.getLayer1Fee(request);
281+
282+
expect(contractGetOperatorFeeMock).toHaveBeenCalledTimes(1);
283+
expect(response).toStrictEqual({
284+
layer1Fee: LAYER_1_FEE_MOCK,
285+
});
286+
});
287+
288+
it('defaults operator fee to zero when call returns undefined', async () => {
289+
const gasUsed = '0x2';
290+
request = {
291+
...request,
292+
transactionMeta: {
293+
...request.transactionMeta,
294+
gasUsed,
295+
},
296+
};
297+
298+
contractGetOperatorFeeMock.mockResolvedValueOnce(
299+
undefined as unknown as BN,
300+
);
301+
302+
const flow = new MockOracleLayer1GasFeeFlow(false);
303+
const response = await flow.getLayer1Fee(request);
304+
305+
expect(contractGetOperatorFeeMock).toHaveBeenCalledTimes(1);
306+
expect(response).toStrictEqual({
307+
layer1Fee: LAYER_1_FEE_MOCK,
308+
});
309+
});
231310
});
232311
});

packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Contract } from '@ethersproject/contracts';
22
import { Web3Provider, type ExternalProvider } from '@ethersproject/providers';
33
import type { Hex } from '@metamask/utils';
4-
import { createModuleLogger } from '@metamask/utils';
5-
4+
import { add0x, createModuleLogger } from '@metamask/utils';
5+
import BN from 'bn.js';
66
import { projectLogger } from '../logger';
77
import type { TransactionControllerMessenger } from '../TransactionController';
88
import type {
@@ -12,17 +12,52 @@ import type {
1212
TransactionMeta,
1313
} from '../types';
1414
import { prepareTransaction } from '../utils/prepare';
15+
import { padHexToEvenLength } from '../utils/utils';
1516

1617
const log = createModuleLogger(projectLogger, 'oracle-layer1-gas-fee-flow');
1718

19+
const ZERO = new BN(0);
20+
1821
const DUMMY_KEY =
1922
'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789';
2023

2124
const GAS_PRICE_ORACLE_ABI = [
2225
{
23-
inputs: [{ internalType: 'bytes', name: '_data', type: 'bytes' }],
26+
inputs: [
27+
{
28+
internalType: 'bytes',
29+
name: '_data',
30+
type: 'bytes',
31+
},
32+
],
2433
name: 'getL1Fee',
25-
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
34+
outputs: [
35+
{
36+
internalType: 'uint256',
37+
name: '',
38+
type: 'uint256',
39+
},
40+
],
41+
stateMutability: 'view',
42+
type: 'function',
43+
},
44+
// only available post Isthmus
45+
{
46+
inputs: [
47+
{
48+
internalType: 'uint256',
49+
name: '_gasUsed',
50+
type: 'uint256',
51+
},
52+
],
53+
name: 'getOperatorFee',
54+
outputs: [
55+
{
56+
internalType: 'uint256',
57+
name: '',
58+
type: 'uint256',
59+
},
60+
],
2661
stateMutability: 'view',
2762
type: 'function',
2863
},
@@ -73,25 +108,37 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow {
73108
request: Layer1GasFeeFlowRequest,
74109
): Promise<Layer1GasFeeFlowResponse> {
75110
try {
76-
return await this.#getOracleLayer1GasFee(request);
111+
const { provider, transactionMeta } = request;
112+
113+
const contract = this.#getGasPriceOracleContract(
114+
provider,
115+
transactionMeta.chainId,
116+
);
117+
118+
const oracleFee = await this.#getOracleLayer1GasFee(
119+
contract,
120+
transactionMeta,
121+
);
122+
const operatorFee = await this.#getOperatorLayer1GasFee(
123+
contract,
124+
transactionMeta,
125+
);
126+
127+
return {
128+
layer1Fee: add0x(
129+
padHexToEvenLength(oracleFee.add(operatorFee).toString(16)),
130+
) as Hex,
131+
};
77132
} catch (error) {
78133
log('Failed to get oracle layer 1 gas fee', error);
79134
throw new Error(`Failed to get oracle layer 1 gas fee`);
80135
}
81136
}
82137

83138
async #getOracleLayer1GasFee(
84-
request: Layer1GasFeeFlowRequest,
85-
): Promise<Layer1GasFeeFlowResponse> {
86-
const { provider, transactionMeta } = request;
87-
88-
const contract = new Contract(
89-
this.getOracleAddressForChain(transactionMeta.chainId),
90-
GAS_PRICE_ORACLE_ABI,
91-
// Network controller provider type is incompatible with ethers provider
92-
new Web3Provider(provider as unknown as ExternalProvider),
93-
);
94-
139+
contract: Contract,
140+
transactionMeta: TransactionMeta,
141+
): Promise<BN> {
95142
const serializedTransaction = this.#buildUnserializedTransaction(
96143
transactionMeta,
97144
this.shouldSignTransaction(),
@@ -103,9 +150,31 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow {
103150
throw new Error('No value returned from oracle contract');
104151
}
105152

106-
return {
107-
layer1Fee: result.toHexString(),
108-
};
153+
return new BN(result);
154+
}
155+
156+
async #getOperatorLayer1GasFee(
157+
contract: Contract,
158+
transactionMeta: TransactionMeta,
159+
): Promise<BN> {
160+
const { gasUsed } = transactionMeta;
161+
162+
if (!gasUsed) {
163+
return ZERO;
164+
}
165+
166+
try {
167+
const result = await contract.getOperatorFee(gasUsed);
168+
169+
if (result === undefined) {
170+
return ZERO;
171+
}
172+
173+
return new BN(result);
174+
} catch (error) {
175+
log('Failed to get operator layer 1 gas fee', error);
176+
return ZERO;
177+
}
109178
}
110179

111180
#buildUnserializedTransaction(
@@ -134,4 +203,16 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow {
134203
gasLimit: transactionMeta.txParams.gas,
135204
};
136205
}
206+
207+
#getGasPriceOracleContract(
208+
provider: Layer1GasFeeFlowRequest['provider'],
209+
chainId: Hex,
210+
) {
211+
return new Contract(
212+
this.getOracleAddressForChain(chainId),
213+
GAS_PRICE_ORACLE_ABI,
214+
// Network controller provider type is incompatible with ethers provider
215+
new Web3Provider(provider as unknown as ExternalProvider),
216+
);
217+
}
137218
}

0 commit comments

Comments
 (0)