Skip to content
Merged
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))

## [64.1.0]

### Added
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 @@ -1808,6 +1808,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 @@ -894,6 +894,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 @@ -284,6 +284,7 @@ async function addTransactionBatchWith7702(
} = request;

const {
atomic,
batchId: batchIdOverride,
disableUpgrade,
from,
Expand Down Expand Up @@ -349,7 +350,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 @@ -44,6 +44,9 @@ const NETWORK_CLIENT_ID_MOCK = 'testNetworkClientId' as NetworkClientId;
const DATA_MOCK =
'0xe9ae5c530100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000009876543210987654321098765432109876543210000000000000000000000000000000000000000000000000000000000005678000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd000000000000000000000000000000000000000000000000000000000000def0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000029abc000000000000000000000000000000000000000000000000000000000000';

const DATA_NON_ATOMIC_MOCK =
'0xe9ae5c530101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000009876543210987654321098765432109876543210000000000000000000000000000000000000000000000000000000000005678000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd000000000000000000000000000000000000000000000000000000000000def0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000029abc000000000000000000000000000000000000000000000000000000000000';

const DATA_EMPTY_MOCK =
'0xe9ae5c5301000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000';

Expand Down Expand Up @@ -431,6 +434,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
23 changes: 21 additions & 2 deletions packages/transaction-controller/src/utils/eip7702.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ export const BATCH_FUNCTION_NAME = 'execute';
export const CALLS_SIGNATURE = '(address,uint256,bytes)[]';
export const ERROR_MESSGE_PUBLIC_KEY = 'EIP-7702 public key not specified';

/**
* ERC-7579 ModeCode encoding for the ERC-7821 `execute` function.
*
* Layout: | CallType (1 byte) | ExecType (1 byte) | Unused (4 bytes) | ModeSelector (4 bytes) | ModePayload (22 bytes) |
*
* - CallType 0x01 = batch
* - ExecType 0x00 = default (revert on failure)
* - ExecType 0x01 = try (skip on failure)
*/
const ERC7579_CALL_TYPE_BATCH = '01';
const ERC7579_EXEC_TYPE_DEFAULT = '00';
const ERC7579_EXEC_TYPE_TRY = '01';

const log = createModuleLogger(projectLogger, 'eip-7702');

/**
Expand Down Expand Up @@ -126,12 +139,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`, uses ERC-7579 ExecType `default` and all calls revert together.
* When `false`, uses ERC-7579 ExecType `try` 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 @@ -144,8 +163,8 @@ export function generateEIP7702BatchTransaction(
];
});

// Single batch mode, no opData.
const mode = '0x01'.padEnd(66, '0');
const execType = atomic ? ERC7579_EXEC_TYPE_DEFAULT : ERC7579_EXEC_TYPE_TRY;
const mode = `0x${ERC7579_CALL_TYPE_BATCH}${execType}`.padEnd(66, '0');

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

Expand Down
Loading