Skip to content

Commit 48adf87

Browse files
fix: cp-8.0.0 require EIP-7702 for sponsored gas transactions (#32253)
## **Description** Sponsored and gas-included transaction publishing depends on the EIP-7702 delegation relay path. Previously, when atomic EIP-7702 support was unavailable for the selected chain, the delegation publish hook returned an empty result, which allowed the publish pipeline to continue to later publication paths. This PR makes that path fail closed for sponsored and gas-included transactions by throwing a clear error when the chain does not support EIP-7702 atomic batching. Non-sponsored transactions keep the existing empty-result behavior when EIP-7702 is unavailable. ## **Changelog** CHANGELOG entry: Fixed sponsored and gas-included transactions falling back when EIP-7702 support is unavailable ## **Related issues** Refs: MetaMask/core#9240 ## **Manual testing steps** N/A - covered by unit tests for the transaction publish hook. ## **Screenshots/Recordings** N/A ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes transaction publish behavior for gas-sponsored and gas-included flows on chains without EIP-7702, blocking unsafe fallbacks instead of silently continuing. > > **Overview** > **Sponsored** and **gas-included** publishes must use the EIP-7702 delegation relay. When atomic batch support is missing for the chain, `Delegation7702PublishHook` used to return an empty result so later publish hooks could run. > > This change **throws** for `isGasFeeSponsored` or `isGasFeeIncluded` when the chain is not EIP-7702-capable, with a clear error (prefixed as `Gas Station 7702:`). Regular transactions still get the empty result and can fall through. > > Gas flags are normalized earlier (`Boolean` on `isGasFeeIncluded` / `isGasFeeSponsored`) and `includeTransfer` now keys off the same `isSponsored` flag. Unit tests cover sponsored and gas-included cases when atomic batch is unsupported. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4e5dfa7. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 50ffd90 commit 48adf87

2 files changed

Lines changed: 38 additions & 6 deletions

File tree

app/util/transactions/hooks/delegation-7702-publish.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,34 @@ describe('Delegation 7702 Publish Hook', () => {
223223
});
224224
});
225225

226+
it('throws when atomic batch is not supported for a gas-sponsored transaction', async () => {
227+
await expect(
228+
hookClass.getHook()(
229+
{
230+
...TRANSACTION_META_MOCK,
231+
isGasFeeSponsored: true,
232+
},
233+
SIGNED_TX_MOCK,
234+
),
235+
).rejects.toThrow(
236+
'Gas Station 7702: Chain must support EIP-7702 for sponsored or gas included transaction',
237+
);
238+
});
239+
240+
it('throws when atomic batch is not supported for a gas-included transaction', async () => {
241+
await expect(
242+
hookClass.getHook()(
243+
{
244+
...TRANSACTION_META_MOCK,
245+
isGasFeeIncluded: true,
246+
},
247+
SIGNED_TX_MOCK,
248+
),
249+
).rejects.toThrow(
250+
'Gas Station 7702: Chain must support EIP-7702 for sponsored or gas included transaction',
251+
);
252+
});
253+
226254
it('no selected gas fee token', async () => {
227255
isAtomicBatchSupportedMock.mockResolvedValueOnce([
228256
{

app/util/transactions/hooks/delegation-7702-publish.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export class Delegation7702PublishHook {
120120
transactionMeta;
121121

122122
const { from } = txParams;
123+
const isGaslessBridge = Boolean(transactionMeta.isGasFeeIncluded);
124+
const isSponsored = Boolean(transactionMeta.isGasFeeSponsored);
123125

124126
const atomicBatchSupport = await this.#isAtomicBatchSupported({
125127
address: from as Hex,
@@ -137,16 +139,19 @@ export class Delegation7702PublishHook {
137139

138140
if (!isChainSupported) {
139141
log('Skipping as EIP-7702 is not supported', { from, chainId });
142+
143+
if (isGaslessBridge || isSponsored) {
144+
throw new Error(
145+
'Chain must support EIP-7702 for sponsored or gas included transaction',
146+
);
147+
}
148+
140149
return EMPTY_RESULT;
141150
}
142151

143152
const { delegationAddress, upgradeContractAddress } =
144153
atomicBatchChainSupport;
145154

146-
const isGaslessBridge = transactionMeta.isGasFeeIncluded;
147-
148-
const isSponsored = Boolean(transactionMeta.isGasFeeSponsored);
149-
150155
if (
151156
(!selectedGasFeeToken || !gasFeeTokens?.length) &&
152157
!isGaslessBridge &&
@@ -173,8 +178,7 @@ export class Delegation7702PublishHook {
173178
parseInt(isE2ETest(chainId) ? SEPOLIA_CHAIN_ID : chainId, 16),
174179
);
175180
const delegationManagerAddress = delegationEnvironment.DelegationManager;
176-
const includeTransfer =
177-
!isGaslessBridge && !transactionMeta.isGasFeeSponsored;
181+
const includeTransfer = !isGaslessBridge && !isSponsored;
178182

179183
if (includeTransfer && (!gasFeeToken || gasFeeToken === undefined)) {
180184
throw new Error('Gas fee token not found');

0 commit comments

Comments
 (0)