Skip to content

Commit 7e40115

Browse files
feat: introduce operator fee for OP stack networks
1 parent 0a8b97b commit 7e40115

File tree

3 files changed

+184
-26
lines changed

3 files changed

+184
-26
lines changed

packages/transaction-controller/CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- Calculate operator fee for OP stack networks and include it in `layer1GasFee` ([#6979](https://github.com/MetaMask/core/pull/6979))
13+
1014
### Changed
1115

1216
- Identify OP stack chains using gas API ([#6899](https://github.com/MetaMask/core/pull/6899))

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

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { TypedTransaction } from '@ethereumjs/tx';
22
import { TransactionFactory } from '@ethereumjs/tx';
3+
import { BigNumber } from '@ethersproject/bignumber';
34
import { Contract } from '@ethersproject/contracts';
45
import type { Provider } from '@metamask/network-controller';
56
import type { Hex } from '@metamask/utils';
6-
7-
import { OracleLayer1GasFeeFlow } from './OracleLayer1GasFeeFlow';
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 { OracleLayer1GasFeeFlow } from './OracleLayer1GasFeeFlow';
1212

1313
jest.mock('@ethersproject/contracts', () => ({
1414
Contract: jest.fn(),
@@ -36,7 +36,8 @@ const TRANSACTION_META_MOCK: TransactionMeta = {
3636

3737
const SERIALIZED_TRANSACTION_MOCK = '0x1234';
3838
const ORACLE_ADDRESS_MOCK = '0x5678' as Hex;
39-
const LAYER_1_FEE_MOCK = '0x9ABCD';
39+
const LAYER_1_FEE_MOCK = '0x09abcd';
40+
const OPERATOR_FEE_MOCK = '0x5';
4041
const DEFAULT_GAS_PRICE_ORACLE_ADDRESS =
4142
'0x420000000000000000000000000000000000000F';
4243

@@ -98,9 +99,10 @@ class DefaultOracleLayer1GasFeeFlow extends OracleLayer1GasFeeFlow {
9899

99100
describe('OracleLayer1GasFeeFlow', () => {
100101
const contractMock = jest.mocked(Contract);
101-
const contractGetL1FeeMock: jest.MockedFn<
102-
() => Promise<{ toHexString: () => string }>
103-
> = jest.fn();
102+
const contractGetL1FeeMock: jest.MockedFn<() => Promise<BigNumber>> =
103+
jest.fn();
104+
const contractGetOperatorFeeMock: jest.MockedFn<() => Promise<BigNumber>> =
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(BigNumber.from(LAYER_1_FEE_MOCK));
120+
contractGetOperatorFeeMock.mockResolvedValue(BigNumber.from(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,75 @@ 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+
BigNumber.from(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: BigNumber.from(LAYER_1_FEE_MOCK)
258+
.add(BigNumber.from(OPERATOR_FEE_MOCK))
259+
.toHexString(),
260+
});
261+
});
262+
263+
it('defaults operator fee to zero when call fails', async () => {
264+
const gasUsed = '0x1';
265+
request = {
266+
...request,
267+
transactionMeta: {
268+
...request.transactionMeta,
269+
gasUsed,
270+
},
271+
};
272+
273+
contractGetOperatorFeeMock.mockRejectedValueOnce(new Error('revert'));
274+
275+
const flow = new MockOracleLayer1GasFeeFlow(false);
276+
const response = await flow.getLayer1Fee(request);
277+
278+
expect(contractGetOperatorFeeMock).toHaveBeenCalledTimes(1);
279+
expect(response).toStrictEqual({
280+
layer1Fee: LAYER_1_FEE_MOCK,
281+
});
282+
});
283+
284+
it('defaults operator fee to zero when call returns undefined', async () => {
285+
const gasUsed = '0x2';
286+
request = {
287+
...request,
288+
transactionMeta: {
289+
...request.transactionMeta,
290+
gasUsed,
291+
},
292+
};
293+
294+
contractGetOperatorFeeMock.mockResolvedValueOnce(
295+
undefined as unknown as BigNumber,
296+
);
297+
298+
const flow = new MockOracleLayer1GasFeeFlow(false);
299+
const response = await flow.getLayer1Fee(request);
300+
301+
expect(contractGetOperatorFeeMock).toHaveBeenCalledTimes(1);
302+
expect(response).toStrictEqual({
303+
layer1Fee: LAYER_1_FEE_MOCK,
304+
});
305+
});
231306
});
232307
});

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

Lines changed: 96 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { BigNumber } from '@ethersproject/bignumber';
12
import { Contract } from '@ethersproject/contracts';
23
import { Web3Provider, type ExternalProvider } from '@ethersproject/providers';
34
import type { Hex } from '@metamask/utils';
@@ -15,14 +16,48 @@ import { prepareTransaction } from '../utils/prepare';
1516

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

19+
const ZERO = BigNumber.from(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,35 @@ 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: oracleFee.add(operatorFee).toHexString(),
129+
};
77130
} catch (error) {
78131
log('Failed to get oracle layer 1 gas fee', error);
79132
throw new Error(`Failed to get oracle layer 1 gas fee`);
80133
}
81134
}
82135

83136
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-
137+
contract: Contract,
138+
transactionMeta: TransactionMeta,
139+
): Promise<BigNumber> {
95140
const serializedTransaction = this.#buildUnserializedTransaction(
96141
transactionMeta,
97142
this.shouldSignTransaction(),
@@ -103,9 +148,31 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow {
103148
throw new Error('No value returned from oracle contract');
104149
}
105150

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

111178
#buildUnserializedTransaction(
@@ -134,4 +201,16 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow {
134201
gasLimit: transactionMeta.txParams.gas,
135202
};
136203
}
204+
205+
#getGasPriceOracleContract(
206+
provider: Layer1GasFeeFlowRequest['provider'],
207+
chainId: Hex,
208+
) {
209+
return new Contract(
210+
this.getOracleAddressForChain(chainId),
211+
GAS_PRICE_ORACLE_ABI,
212+
// Network controller provider type is incompatible with ethers provider
213+
new Web3Provider(provider as unknown as ExternalProvider),
214+
);
215+
}
137216
}

0 commit comments

Comments
 (0)