Skip to content

Commit 130f8b4

Browse files
feat: introduce operator fee for OP stack networks
1 parent e3af02a commit 130f8b4

File tree

5 files changed

+342
-35
lines changed

5 files changed

+342
-35
lines changed

packages/transaction-controller/CHANGELOG.md

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

1414
- 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))
15+
- Calculate operator fee for OP stack networks and include it in `layer1GasFee` ([#6979](https://github.com/MetaMask/core/pull/6979))
1516

1617
### Changed
1718

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

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ 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';
5+
import { add0x, type Hex } from '@metamask/utils';
6+
import BN from 'bn.js';
67

78
import { OracleLayer1GasFeeFlow } from './OracleLayer1GasFeeFlow';
89
import { CHAIN_IDS } from '../constants';
910
import type { TransactionControllerMessenger } from '../TransactionController';
10-
import type { Layer1GasFeeFlowRequest, TransactionMeta } from '../types';
11-
import { TransactionStatus } from '../types';
11+
import {
12+
type Layer1GasFeeFlowRequest,
13+
type TransactionMeta,
14+
TransactionStatus,
15+
} from '../types';
16+
import { bnFromHex, padHexToEvenLength } from '../utils/utils';
1217

1318
jest.mock('@ethersproject/contracts', () => ({
1419
Contract: jest.fn(),
@@ -36,7 +41,8 @@ const TRANSACTION_META_MOCK: TransactionMeta = {
3641

3742
const SERIALIZED_TRANSACTION_MOCK = '0x1234';
3843
const ORACLE_ADDRESS_MOCK = '0x5678' as Hex;
39-
const LAYER_1_FEE_MOCK = '0x9ABCD';
44+
const LAYER_1_FEE_MOCK = '0x09abcd';
45+
const OPERATOR_FEE_MOCK = '0x5';
4046
const DEFAULT_GAS_PRICE_ORACLE_ADDRESS =
4147
'0x420000000000000000000000000000000000000F';
4248

@@ -98,9 +104,9 @@ class DefaultOracleLayer1GasFeeFlow extends OracleLayer1GasFeeFlow {
98104

99105
describe('OracleLayer1GasFeeFlow', () => {
100106
const contractMock = jest.mocked(Contract);
101-
const contractGetL1FeeMock: jest.MockedFn<
102-
() => Promise<{ toHexString: () => string }>
103-
> = jest.fn();
107+
const contractGetL1FeeMock: jest.MockedFn<() => Promise<BN>> = jest.fn();
108+
const contractGetOperatorFeeMock: jest.MockedFn<() => Promise<BN>> =
109+
jest.fn();
104110

105111
let request: Layer1GasFeeFlowRequest;
106112

@@ -112,13 +118,14 @@ describe('OracleLayer1GasFeeFlow', () => {
112118

113119
contractMock.mockClear();
114120
contractGetL1FeeMock.mockClear();
121+
contractGetOperatorFeeMock.mockClear();
115122

116-
contractGetL1FeeMock.mockResolvedValue({
117-
toHexString: () => LAYER_1_FEE_MOCK,
118-
});
123+
contractGetL1FeeMock.mockResolvedValue(bnFromHex(LAYER_1_FEE_MOCK));
124+
contractGetOperatorFeeMock.mockResolvedValue(new BN(0));
119125

120126
contractMock.mockReturnValue({
121127
getL1Fee: contractGetL1FeeMock,
128+
getOperatorFee: contractGetOperatorFeeMock,
122129
} as unknown as Contract);
123130
});
124131

@@ -156,6 +163,7 @@ describe('OracleLayer1GasFeeFlow', () => {
156163
expect(contractGetL1FeeMock).toHaveBeenCalledWith(
157164
serializedTransactionMock,
158165
);
166+
expect(contractGetOperatorFeeMock).not.toHaveBeenCalled();
159167
});
160168

161169
it('signs transaction with dummy key if supported by flow', async () => {
@@ -180,6 +188,7 @@ describe('OracleLayer1GasFeeFlow', () => {
180188
});
181189

182190
expect(typedTransactionMock.sign).toHaveBeenCalledTimes(1);
191+
expect(contractGetOperatorFeeMock).not.toHaveBeenCalled();
183192
});
184193

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

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

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +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';
4+
import { add0x, createModuleLogger } from '@metamask/utils';
5+
import BN from 'bn.js';
56

67
import { projectLogger } from '../logger';
78
import type { TransactionControllerMessenger } from '../TransactionController';
@@ -12,17 +13,52 @@ import type {
1213
TransactionMeta,
1314
} from '../types';
1415
import { prepareTransaction } from '../utils/prepare';
16+
import { padHexToEvenLength, toBN } from '../utils/utils';
1517

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

20+
const ZERO = new BN(0);
21+
1822
const DUMMY_KEY =
1923
'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789';
2024

2125
const GAS_PRICE_ORACLE_ABI = [
2226
{
23-
inputs: [{ internalType: 'bytes', name: '_data', type: 'bytes' }],
27+
inputs: [
28+
{
29+
internalType: 'bytes',
30+
name: '_data',
31+
type: 'bytes',
32+
},
33+
],
2434
name: 'getL1Fee',
25-
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
35+
outputs: [
36+
{
37+
internalType: 'uint256',
38+
name: '',
39+
type: 'uint256',
40+
},
41+
],
42+
stateMutability: 'view',
43+
type: 'function',
44+
},
45+
// only available post Isthmus
46+
{
47+
inputs: [
48+
{
49+
internalType: 'uint256',
50+
name: '_gasUsed',
51+
type: 'uint256',
52+
},
53+
],
54+
name: 'getOperatorFee',
55+
outputs: [
56+
{
57+
internalType: 'uint256',
58+
name: '',
59+
type: 'uint256',
60+
},
61+
],
2662
stateMutability: 'view',
2763
type: 'function',
2864
},
@@ -73,25 +109,37 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow {
73109
request: Layer1GasFeeFlowRequest,
74110
): Promise<Layer1GasFeeFlowResponse> {
75111
try {
76-
return await this.#getOracleLayer1GasFee(request);
112+
const { provider, transactionMeta } = request;
113+
114+
const contract = this.#getGasPriceOracleContract(
115+
provider,
116+
transactionMeta.chainId,
117+
);
118+
119+
const oracleFee = await this.#getOracleLayer1GasFee(
120+
contract,
121+
transactionMeta,
122+
);
123+
const operatorFee = await this.#getOperatorLayer1GasFee(
124+
contract,
125+
transactionMeta,
126+
);
127+
128+
return {
129+
layer1Fee: add0x(
130+
padHexToEvenLength(oracleFee.add(operatorFee).toString(16)),
131+
) as Hex,
132+
};
77133
} catch (error) {
78134
log('Failed to get oracle layer 1 gas fee', error);
79135
throw new Error(`Failed to get oracle layer 1 gas fee`);
80136
}
81137
}
82138

83139
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-
140+
contract: Contract,
141+
transactionMeta: TransactionMeta,
142+
): Promise<BN> {
95143
const serializedTransaction = this.#buildUnserializedTransaction(
96144
transactionMeta,
97145
this.shouldSignTransaction(),
@@ -103,9 +151,31 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow {
103151
throw new Error('No value returned from oracle contract');
104152
}
105153

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

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

0 commit comments

Comments
 (0)