Skip to content
Draft
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
4 changes: 4 additions & 0 deletions packages/transaction-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add optional `atomic` property to `TransactionBatchRequest` to configure whether EIP-7702 batch calls revert together or can fail independently ([#8320](https://github.com/MetaMask/core/pull/8320))

## [63.3.1]

### Changed
Expand Down
8 changes: 8 additions & 0 deletions packages/transaction-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1805,6 +1805,14 @@ export type TransactionBatchSingleRequest = {
* Currently only atomic batches are supported via EIP-7702.
*/
export type TransactionBatchRequest = {
/**
* Whether the EIP-7702 batch transaction should be executed atomically.
* When `true` (default), all calls in the batch either succeed or revert together.
* When `false`, calls are independent — individual calls can fail without
* reverting the entire batch.
*/
atomic?: boolean;

batchId?: Hex;

/** Whether to disable batch transaction processing via an EIP-7702 upgraded account. */
Expand Down
50 changes: 50 additions & 0 deletions packages/transaction-controller/src/utils/batch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,56 @@ describe('Batch Utils', () => {
);
});

it('passes atomic option to generateEIP7702BatchTransaction', async () => {
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
delegationAddress: undefined,
isSupported: true,
});

addTransactionMock.mockResolvedValueOnce({
transactionMeta: TRANSACTION_META_MOCK,
result: Promise.resolve(''),
});

generateEIP7702BatchTransactionMock.mockReturnValueOnce(
TRANSACTION_BATCH_PARAMS_MOCK,
);

request.request.atomic = false;

await addTransactionBatch(request);

expect(generateEIP7702BatchTransactionMock).toHaveBeenCalledWith(
FROM_MOCK,
expect.any(Array),
{ atomic: false },
);
});

it('passes atomic as undefined to generateEIP7702BatchTransaction by default', async () => {
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
delegationAddress: undefined,
isSupported: true,
});

addTransactionMock.mockResolvedValueOnce({
transactionMeta: TRANSACTION_META_MOCK,
result: Promise.resolve(''),
});

generateEIP7702BatchTransactionMock.mockReturnValueOnce(
TRANSACTION_BATCH_PARAMS_MOCK,
);

await addTransactionBatch(request);

expect(generateEIP7702BatchTransactionMock).toHaveBeenCalledWith(
FROM_MOCK,
expect.any(Array),
{ atomic: undefined },
);
});

it('throws if chain not supported', async () => {
doesChainSupportEIP7702Mock.mockReturnValue(false);

Expand Down
9 changes: 8 additions & 1 deletion packages/transaction-controller/src/utils/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ async function addTransactionBatchWith7702(
} = request;

const {
atomic,
batchId: batchIdOverride,
disableUpgrade,
from,
Expand Down Expand Up @@ -357,7 +358,13 @@ async function addTransactionBatchWith7702(
),
);

const batchParams = generateEIP7702BatchTransaction(from, nestedTransactions);
const batchParams = generateEIP7702BatchTransaction(
from,
nestedTransactions,
{
atomic,
},
);

const txParams: TransactionParams = {
...batchParams,
Expand Down
71 changes: 71 additions & 0 deletions packages/transaction-controller/src/utils/eip7702.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const ETH_QUERY_MOCK = {} as EthQuery;
const DATA_MOCK =
'0xe9ae5c530100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000009876543210987654321098765432109876543210000000000000000000000000000000000000000000000000000000000005678000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd000000000000000000000000000000000000000000000000000000000000def0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000029abc000000000000000000000000000000000000000000000000000000000000';

const DATA_NON_ATOMIC_MOCK =
'0xe9ae5c530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000009876543210987654321098765432109876543210000000000000000000000000000000000000000000000000000000000005678000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd000000000000000000000000000000000000000000000000000000000000def0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000029abc000000000000000000000000000000000000000000000000000000000000';

const DATA_EMPTY_MOCK =
'0xe9ae5c5301000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000';

Expand Down Expand Up @@ -432,6 +435,74 @@ describe('EIP-7702 Utils', () => {
to: ADDRESS_MOCK,
});
});

it('uses atomic mode by default', () => {
const result = generateEIP7702BatchTransaction(ADDRESS_MOCK, [
{
data: '0x1234',
to: ADDRESS_2_MOCK,
value: '0x5678',
},
{
data: '0x9abc',
to: ADDRESS_3_MOCK,
value: '0xdef0',
},
]);

expect(result).toStrictEqual({
data: DATA_MOCK,
to: ADDRESS_MOCK,
});
});

it('uses atomic mode when atomic is true', () => {
const result = generateEIP7702BatchTransaction(
ADDRESS_MOCK,
[
{
data: '0x1234',
to: ADDRESS_2_MOCK,
value: '0x5678',
},
{
data: '0x9abc',
to: ADDRESS_3_MOCK,
value: '0xdef0',
},
],
{ atomic: true },
);

expect(result).toStrictEqual({
data: DATA_MOCK,
to: ADDRESS_MOCK,
});
});

it('uses non-atomic mode when atomic is false', () => {
const result = generateEIP7702BatchTransaction(
ADDRESS_MOCK,
[
{
data: '0x1234',
to: ADDRESS_2_MOCK,
value: '0x5678',
},
{
data: '0x9abc',
to: ADDRESS_3_MOCK,
value: '0xdef0',
},
],
{ atomic: false },
);

expect(result).toStrictEqual({
data: DATA_NON_ATOMIC_MOCK,
to: ADDRESS_MOCK,
});
});
});

describe('getDelegationAddress', () => {
Expand Down
9 changes: 7 additions & 2 deletions packages/transaction-controller/src/utils/eip7702.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,18 @@ export async function isAccountUpgradedToEIP7702(
*
* @param from - The sender address.
* @param transactions - The transactions to batch.
* @param options - Options bag.
* @param options.atomic - Whether the batch should be atomic. Defaults to `true`.
* When `true`, mode `0x01` is used and all calls revert together.
* When `false`, mode `0x00` is used and individual calls can fail independently.
* @returns The batch transaction.
*/
export function generateEIP7702BatchTransaction(
from: Hex,
transactions: BatchTransactionParams[],
options?: { atomic?: boolean },
): BatchTransactionParams {
const atomic = options?.atomic ?? true;
const erc7821Contract = Contract.getInterface(ABI_IERC7821);

const calls = transactions.map((transaction) => {
Expand All @@ -132,8 +138,7 @@ export function generateEIP7702BatchTransaction(
];
});

// Single batch mode, no opData.
const mode = '0x01'.padEnd(66, '0');
const mode = (atomic ? '0x01' : '0x00').padEnd(66, '0');

const callData = defaultAbiCoder.encode([CALLS_SIGNATURE], [calls]);

Expand Down
Loading