feat(transaction-pay-controller): Add shared EIP-7702 quote gas estimation#8145
feat(transaction-pay-controller): Add shared EIP-7702 quote gas estimation#8145pedronfigueiredo wants to merge 1 commit intomainfrom
Conversation
c658ed5 to
7b400b2
Compare
7b400b2 to
49aa199
Compare
cryptotavares
left a comment
There was a problem hiding this comment.
Reviewing as @cryptotavares's AI assistant
Overall this is a clean refactor — the shared estimator logic is well-structured, fallbacks are correctly wired, and the batch/individual split in estimateQuoteGasLimits is sound. A few things worth addressing before merge:
Bug: Missing chainId fallback in getAcrossOrderedTransactions
File: src/strategy/across/transactions.ts + src/strategy/across/across-quotes.ts
getAcrossOrderedTransactions spreads approval transactions directly from the Across API response:
const approvalTransactions = (quote.approvalTxns ?? []).map((approval) => ({
...approval,
kind: 'approval' as const,
type: TransactionType.tokenMethodApprove,
}));If the Across API omits chainId on an approval transaction (which the old code explicitly guarded against with quote.approvalTxns?.[index]?.chainId ?? swapTx.chainId), then transaction.chainId will be undefined at runtime despite the type saying number. The gas estimation call in calculateSourceNetworkCost then does:
chainId: toHex(transaction.chainId), // toHex(undefined) → likely '0x0'This would target the wrong chain for gas estimation. The cost calculation later in calculateSourceNetworkCost still has the correct fallback (quote.approvalTxns?.[index]?.chainId ?? swapTx.chainId), but that doesn't help if estimation ran against chain 0x0.
The old fallback was intentional. Either:
- Add the fallback inside
getAcrossOrderedTransactions(passswapTx.chainIdand useapproval.chainId ?? swapChainId), or - Add it at the call site in
calculateSourceNetworkCostwhen building thetransactionsarray
Logic concern: useBuffer skips buffer when batch API echoes provided gas
File: src/utils/quote-gas.ts (~L105)
const useBuffer =
gasLimits.length === 1 || paramGasLimits[index] !== gasLimit;When estimateGasBatch returns per-transaction results (gasLimits.length === transactions.length), useBuffer is false if the batch API returns exactly the value that was passed in via transaction.gas. The intent seems to be "if the chain just echoed back our provided value, don't add a buffer on top." That's a reasonable interpretation, but paramGasLimits[index] is the parsed input gas; if the batch API happens to estimate the same number that was provided (not just echo it), buffer would also be skipped. Is that intentional? Worth adding a comment to clarify.
Minor: combinePostQuoteGas EIP-7702 detection heuristic
File: src/strategy/relay/relay-quotes.ts (~L480)
// TODO: Test EIP-7702 support on the chain as well before assuming single gas limit.
const isEIP7702 = gasLimits.length === 1;A single-step Relay quote (one transaction, individual estimation) also produces gasLimits.length === 1, so this misidentifies it as a 7702 batch and combines the original tx gas into a single limit. I see this has a TODO already — just flagging it as something that becomes more load-bearing now that the shared estimator is in use for both strategies.
One merge blocker (the chainId fallback regression), one question (buffer logic intent), one pre-existing TODO that's worth tracking. Happy to look at a follow-up once the chainId question is resolved.
0c900ca to
50c912d
Compare
ec0ec98 to
e0c0649
Compare
5317383 to
8a2e24a
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
8a2e24a to
33c04a7
Compare
| params.length === 1 | ||
| ? await calculateSourceNetworkGasLimitSingle(params[0], messenger) | ||
| : await calculateSourceNetworkGasLimitBatch(params, messenger); | ||
| const fallbackChainId = params.find( |
There was a problem hiding this comment.
These are all marked as required in the Relay type meaning we can assume they are present to simplify the code, and it can throw if they are not provided.
| params.length === 1 | ||
| ? await calculateSourceNetworkGasLimitSingle(params[0], messenger) | ||
| : await calculateSourceNetworkGasLimitBatch(params, messenger); | ||
| const fallbackChainId = params.find( |
There was a problem hiding this comment.
These are all marked as required in the Relay type meaning we can assume they are present to simplify the code, and it can throw if they are not provided.
| )?.data; | ||
|
|
||
| const relayGasResult = await estimateQuoteGasLimits({ | ||
| allowBatch: params.every( |
There was a problem hiding this comment.
Can we just assume this is always the case? Either the quote has valid calls we can process or we should throw?
| fallbackGas: getFeatureFlags(messenger).relayFallbackGas, | ||
| fallbackOnSimulationFailure: true, | ||
| messenger, | ||
| transactions: params.map((singleParams) => ({ |
There was a problem hiding this comment.
Minor, should we define in variable for readability?
| from: | ||
| singleParams.from ?? | ||
| fallbackFrom ?? | ||
| '0x0000000000000000000000000000000000000000', |
There was a problem hiding this comment.
Also here, we shouldn't need these fallbacks since these aren't optional properties in the params?
| } else if (gas.startsWith('0x')) { | ||
| parsedGas = new BigNumber(gas.slice(2), 16); | ||
| } else { | ||
| parsedGas = new BigNumber(gas); |
There was a problem hiding this comment.
Are all of these not identical and already handled by the BigNumber constructor with no arguments such as base?
| allowBatch && | ||
| transactions.length > 1 && | ||
| hasUniformBatchContext(transactions) && | ||
| isEIP7702Chain(messenger, firstTransaction.chainId); |
There was a problem hiding this comment.
This flow and estimateGasBatch isn't just for 7702, it also supports sequential transactions by simulating them together to estimate gas for each one.
The 7702 check and all these fallbacks are already inside estimateGasBatch.
| try { | ||
| return { | ||
| ...(await estimateQuoteGasLimitsBatch(transactions, messenger)), | ||
| usedBatch: true, |
There was a problem hiding this comment.
Since this flow isn't just for 7702, we could change this to return is7702 to the submit phases know whether the single gas limit is because 7702 worked?
| }; | ||
| } | ||
|
|
||
| async function estimateQuoteGasLimitsIndividually({ |
There was a problem hiding this comment.
This should just be for single via estimateGas, as the above batch flow already handles multiple non-7702 calls also.
| } as QuoteGasLimit; | ||
| } | ||
|
|
||
| const gasLimitResult = await estimateGasLimit({ |
There was a problem hiding this comment.
We can't estimate sequential calls since the latter would almost certainly revert since it requires state from the first, such as token approval then swap.
That's why estimateGasBatch does simulation on all the transactions to get sequential gas limits.
Explanation
Add a shared quote gas estimator that switches between per-transaction and EIP-7702 batch estimation, reuses Across’s ordered submission transaction list at quote time, and falls back to the single-transaction path when batch estimation is unsupported or fails.
References
https://github.com/MetaMask/MetaMask-planning/issues/7098
Checklist
Note
Medium Risk
Medium risk because it changes gas limit/cost calculation and batching behavior for Across and Relay, including optional EIP-7702
estimateGasBatchand 7702 batch submission paths. Incorrect estimation ordering or fallback handling could lead to failed transactions or mispriced fees on supported chains.Overview
Adds a new shared
estimateQuoteGasLimitsutility to compute per-transaction gas limits, optionally using EIP-7702estimateGasBatchon supported chains with buffering and safe fallback to individual estimation/fallback gas.Across quoting now uses a single ordered transaction list (
getAcrossOrderedTransactions) and can return a combinedgasLimits.batchfor multi-tx quotes; Across submission detects this and submits a 7702 batch (gasLimit7702, disables hook/sequential) while omitting per-txgas.Relay quoting replaces bespoke single/batch gas estimation with the shared estimator, including placeholder/fallback param population when quote items omit fields, and updates feature-flag helpers by adding
getEIP7702SupportedChains(used byisEIP7702Chain). Tests are expanded to cover batch usage, fallbacks, and edge cases (missing swap gas, missing approval chainId, non-7702 chains).Written by Cursor Bugbot for commit 33c04a7. This will update automatically on new commits. Configure here.