Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Include Mantle operator fee in the displayed L1 gas estimate when simulated `gasUsed` is unavailable, by falling back to the transaction's gas limit ([#8837](https://github.com/MetaMask/core/pull/8837))
- Adds a `protected getOperatorFeeGas` hook on `OracleLayer1GasFeeFlow` that subclasses can override to supply a fallback value. The default behaviour is unchanged (returns `transactionMeta.gasUsed`).
- `MantleLayer1GasFeeFlow` overrides the hook with `gasUsed ?? txParams.gas ?? txParams.gasLimit`, so the operator-fee oracle is called with the gas limit when `gasUsed` is missing. Gas limit is an upper bound on actual gas used, so the operator fee is over-estimated rather than under-reported.

## [66.0.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,85 @@ describe('MantleLayer1GasFeeFlow', () => {
});
});

it('returns converted L1 fee when no gasUsed (no operator fee)', async () => {
it('falls back to txParams.gas for operator fee when gasUsed is missing', async () => {
contractGetOperatorFeeMock.mockResolvedValueOnce(
bnFromHex(OPERATOR_FEE_MOCK),
);

jest
.spyOn(TransactionFactory, 'fromTxData')
.mockReturnValueOnce(
createMockTypedTransaction(
Buffer.from(SERIALIZED_TRANSACTION_MOCK, 'hex'),
),
);

const flow = new MantleLayer1GasFeeFlow();
const response = await flow.getLayer1Fee(request);

const expectedL1FeeInMnt = bnFromHex(L1_FEE_MOCK).mul(TOKEN_RATIO_MOCK);
const expectedTotal = expectedL1FeeInMnt.add(
bnFromHex(OPERATOR_FEE_MOCK),
);

expect(contractGetOperatorFeeMock).toHaveBeenCalledTimes(1);
expect(contractGetOperatorFeeMock).toHaveBeenCalledWith(
TRANSACTION_PARAMS_MOCK.gas,
);
expect(response).toStrictEqual({
layer1Fee: add0x(padHexToEvenLength(expectedTotal.toString(16))),
});
});

it('falls back to txParams.gasLimit for operator fee when gasUsed and txParams.gas are missing', async () => {
const gasLimit = '0x9999';
request = {
...request,
transactionMeta: {
...request.transactionMeta,
txParams: {
from: TRANSACTION_PARAMS_MOCK.from,
gasLimit,
},
},
};

contractGetOperatorFeeMock.mockResolvedValueOnce(
bnFromHex(OPERATOR_FEE_MOCK),
);

jest
.spyOn(TransactionFactory, 'fromTxData')
.mockReturnValueOnce(
createMockTypedTransaction(
Buffer.from(SERIALIZED_TRANSACTION_MOCK, 'hex'),
),
);

const flow = new MantleLayer1GasFeeFlow();
const response = await flow.getLayer1Fee(request);

const expectedL1FeeInMnt = bnFromHex(L1_FEE_MOCK).mul(TOKEN_RATIO_MOCK);
const expectedTotal = expectedL1FeeInMnt.add(
bnFromHex(OPERATOR_FEE_MOCK),
);

expect(contractGetOperatorFeeMock).toHaveBeenCalledTimes(1);
expect(contractGetOperatorFeeMock).toHaveBeenCalledWith(gasLimit);
expect(response).toStrictEqual({
layer1Fee: add0x(padHexToEvenLength(expectedTotal.toString(16))),
});
});

it('skips operator fee when no gasUsed, txParams.gas, or txParams.gasLimit is available', async () => {
request = {
...request,
transactionMeta: {
...request.transactionMeta,
txParams: { from: TRANSACTION_PARAMS_MOCK.from },
},
};

jest
.spyOn(TransactionFactory, 'fromTxData')
.mockReturnValueOnce(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ export class MantleLayer1GasFeeFlow extends OracleLayer1GasFeeFlow {
return MANTLE_CHAIN_IDS.includes(transactionMeta.chainId);
}

protected override getOperatorFeeGas(
transactionMeta: TransactionMeta,
): string | undefined {
return (
transactionMeta.gasUsed ??
transactionMeta.txParams.gas ??
transactionMeta.txParams.gasLimit
);
}

protected override async transformOracleFee(
oracleFee: BN,
request: Layer1GasFeeFlowRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,18 +174,33 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow {
return toBN(result);
}

/**
* Returns the gas value to pass to the operator-fee oracle, or undefined
* to skip the operator-fee call entirely. Defaults to the simulated
* `gasUsed` on the transaction. Subclasses can override to supply a
* fallback (e.g. estimated gas limit) when `gasUsed` is unavailable.
*
* @param transactionMeta - The transaction metadata.
* @returns The gas value, or undefined to skip the operator-fee call.
*/
protected getOperatorFeeGas(
transactionMeta: TransactionMeta,
): string | undefined {
return transactionMeta.gasUsed;
}

async #getOperatorLayer1GasFee(
contract: Contract,
transactionMeta: TransactionMeta,
): Promise<BN> {
const { gasUsed } = transactionMeta;
const gas = this.getOperatorFeeGas(transactionMeta);

if (!gasUsed) {
if (!gas) {
return ZERO;
}

try {
const result = await contract.getOperatorFee(gasUsed);
const result = await contract.getOperatorFee(gas);

if (result === undefined) {
return ZERO;
Expand Down
Loading