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
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,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add optional `authorizationList` to `TransactionBatchRequest` so callers can include pre-signed EIP-7702 authorizations (e.g. Money Account upgrades) alongside any upgrade authorization generated for the batch payer (`from`) ([#9765](https://github.com/MetaMask/core/pull/9765))

### Changed

- Bump `@metamask/network-controller` from `^35.0.0` to `^35.0.1` ([#9758](https://github.com/MetaMask/core/pull/9758))
Expand All @@ -24,6 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Refresh gas fee token quotes independently of balance changes for enforced-simulation transactions, and prevent stale simulation responses from overwriting newer transaction state. ([#9757](https://github.com/MetaMask/core/pull/9757))
- Retain pre-signed entries in `signAuthorizationList` instead of re-signing them with `txParams.from`, so authorizations signed by a different account survive publish ([#9765](https://github.com/MetaMask/core/pull/9765))
- Attribute EIP-7702 authorization nonces to their recovered authorities in the nonce tracker, so foreign authorizations (e.g. Money Account upgrades on same-chain pay batches) do not inflate the batch payer's pending nonce ([#9765](https://github.com/MetaMask/core/pull/9765))

## [69.4.0]

Expand Down
2 changes: 2 additions & 0 deletions packages/transaction-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
},
"dependencies": {
"@ethereumjs/common": "^4.4.0",
"@ethereumjs/rlp": "^5.0.2",
"@ethereumjs/tx": "^5.4.0",
"@ethereumjs/util": "^9.1.0",
"@ethersproject/abi": "^5.7.0",
Expand All @@ -78,6 +79,7 @@
"bignumber.js": "^9.1.2",
"bn.js": "^5.2.1",
"eth-method-registry": "^4.0.0",
"ethereum-cryptography": "^2.1.2",
"fast-json-patch": "^3.1.1",
"lodash": "^4.17.21",
"uuid": "^8.3.2"
Expand Down
10 changes: 10 additions & 0 deletions packages/transaction-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1831,6 +1831,16 @@ export type TransactionBatchRequest = {
*/
atomic?: boolean;

/**
* Pre-signed or unsigned EIP-7702 authorizations to include on the batch
* type-4 transaction, in addition to any upgrade authorization generated for
* `from` when the batch payer is not yet upgraded.
*
* Used when vaulting/delegation requires upgrading a different account than
* `from` (e.g. Money Account deposits paid by an EOA with account override).
*/
authorizationList?: AuthorizationList;

batchId?: Hex;

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

it('merges provided authorizationList with from upgrade authorization', async () => {
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
delegationAddress: undefined,
isSupported: false,
});

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

generateEIP7702BatchTransactionMock.mockReturnValueOnce(
TRANSACTION_BATCH_PARAMS_MOCK,
);

getEIP7702UpgradeContractAddressMock.mockReturnValueOnce(
CONTRACT_ADDRESS_MOCK,
);

const providedAuthorization = {
address: '0x1234567890123456789012345678901234567890' as const,
chainId: '0x1' as const,
nonce: '0x5' as const,
r: '0xabc' as const,
s: '0xdef' as const,
yParity: '0x1' as const,
};

request.request.authorizationList = [providedAuthorization];

await addTransactionBatch(request);

expect(addTransactionMock).toHaveBeenCalledWith(
expect.objectContaining({
type: TransactionEnvelopeType.setCode,
authorizationList: [
{ address: CONTRACT_ADDRESS_MOCK },
providedAuthorization,
],
}),
expect.anything(),
);
});

it('includes provided authorizationList when from is already upgraded', async () => {
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
delegationAddress: CONTRACT_ADDRESS_MOCK,
isSupported: true,
});

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

generateEIP7702BatchTransactionMock.mockReturnValueOnce(
TRANSACTION_BATCH_PARAMS_MOCK,
);

const providedAuthorization = {
address: '0x1234567890123456789012345678901234567890' as const,
chainId: '0x1' as const,
nonce: '0x5' as const,
r: '0xabc' as const,
s: '0xdef' as const,
yParity: '0x1' as const,
};

request.request.authorizationList = [providedAuthorization];

await addTransactionBatch(request);

expect(addTransactionMock).toHaveBeenCalledWith(
expect.objectContaining({
type: TransactionEnvelopeType.setCode,
authorizationList: [providedAuthorization],
}),
expect.anything(),
);
});

it('does not use type 4 if not upgraded but disableUpgrade set', async () => {
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
delegationAddress: undefined,
Expand Down Expand Up @@ -1390,6 +1471,47 @@ describe('Batch Utils', () => {
CHAIN_ID_MOCK,
);
});

it('does not use provided foreign authorization as delegation mock when from is upgraded', async () => {
isAccountUpgradedToEIP7702Mock.mockResolvedValueOnce({
delegationAddress: CONTRACT_ADDRESS_MOCK,
isSupported: true,
});

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

generateEIP7702BatchTransactionMock.mockReturnValueOnce(
TRANSACTION_BATCH_PARAMS_MOCK,
);

const providedAuthorization = {
address: '0x1234567890123456789012345678901234567890' as const,
chainId: '0x1' as const,
nonce: '0x5' as const,
r: '0xabc' as const,
s: '0xdef' as const,
yParity: '0x1' as const,
};

request.request.authorizationList = [providedAuthorization];

const validateSecurityMock = jest.fn();
validateSecurityMock.mockResolvedValueOnce({});

request.request.validateSecurity = validateSecurityMock;

await addTransactionBatch(request);

expect(validateSecurityMock).toHaveBeenCalledWith(
expect.objectContaining({
delegationMock: undefined,
}),
CHAIN_ID_MOCK,
);
});
});

describe('with publish batch hook', () => {
Expand Down
81 changes: 69 additions & 12 deletions packages/transaction-controller/src/utils/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
import { projectLogger } from '../logger.js';
import { TransactionEnvelopeType, TransactionType } from '../types.js';
import type {
AuthorizationList,
NestedTransactionMetadata,
SecurityAlertResponse,
TransactionBatchSingleRequest,
Expand Down Expand Up @@ -282,6 +283,58 @@ async function getNestedTransactionMeta(
};
}

/**
* Build the authorization list for an EIP-7702 batch transaction.
Comment thread
pedronfigueiredo marked this conversation as resolved.
*
* When the batch payer (`from`) requires an upgrade, an unsigned upgrade
* authorization for that account is included first. Any caller-provided
* authorizations (e.g. a pre-signed Money Account upgrade) are appended so
* both can be submitted on the same type-4 transaction.
*
* @param options - Options bag.
* @param options.chainId - Chain ID of the batch.
* @param options.messenger - Controller messenger.
* @param options.providedAuthorizationList - Optional authorizations from the batch request.
* @param options.publicKeyEIP7702 - Public key used to resolve the upgrade contract.
* @param options.requiresUpgrade - Whether the batch payer requires an EIP-7702 upgrade.
* @returns The combined authorization list, or undefined when none are needed.
*/
function buildBatchAuthorizationList({
chainId,
messenger,
providedAuthorizationList,
publicKeyEIP7702,
requiresUpgrade,
}: {
chainId: Hex;
messenger: TransactionControllerMessenger;
providedAuthorizationList?: AuthorizationList;
publicKeyEIP7702: Hex;
requiresUpgrade: boolean;
}): AuthorizationList | undefined {
const authorizationList: AuthorizationList = [];

if (requiresUpgrade) {
const upgradeContractAddress = getEIP7702UpgradeContractAddress(
chainId,
messenger,
publicKeyEIP7702,
);

if (!upgradeContractAddress) {
throw rpcErrors.internal(ERROR_MESSAGE_NO_UPGRADE_CONTRACT);
}

authorizationList.push({ address: upgradeContractAddress });
}

if (providedAuthorizationList?.length) {
authorizationList.push(...providedAuthorizationList);
Comment thread
pedronfigueiredo marked this conversation as resolved.
}

return authorizationList.length ? authorizationList : undefined;
}

/**
* Process a batch transaction using an EIP-7702 transaction.
*
Expand All @@ -300,6 +353,7 @@ async function addTransactionBatchWith7702(

const {
atomic,
authorizationList: providedAuthorizationList,
batchId: batchIdOverride,
disableUpgrade,
from,
Expand Down Expand Up @@ -382,22 +436,23 @@ async function addTransactionBatchWith7702(
maxPriorityFeePerGas: nestedTransactions[0]?.maxPriorityFeePerGas,
};

if (requiresUpgrade) {
const upgradeContractAddress = getEIP7702UpgradeContractAddress(
chainId,
messenger,
publicKeyEIP7702,
);

if (!upgradeContractAddress) {
throw rpcErrors.internal(ERROR_MESSAGE_NO_UPGRADE_CONTRACT);
}
const authorizationList = buildBatchAuthorizationList({
chainId,
messenger,
providedAuthorizationList,
publicKeyEIP7702,
requiresUpgrade,
});

if (authorizationList?.length) {
txParams.type = TransactionEnvelopeType.setCode;
txParams.authorizationList = [{ address: upgradeContractAddress }];
txParams.authorizationList = authorizationList;
Comment thread
jpuri marked this conversation as resolved.
}

if (validateSecurity) {
// `delegationMock` applies to the batch payer (`from`) only. When
// `requiresUpgrade` is true, that upgrade authorization is always first in
// the list. Caller-provided auths for other accounts must not be used here.
const securityRequest: ValidateSecurityRequest = {
method: 'eth_sendTransaction',
params: [
Expand All @@ -407,7 +462,9 @@ async function addTransactionBatchWith7702(
type: TransactionEnvelopeType.feeMarket,
},
],
delegationMock: txParams.authorizationList?.[0]?.address,
delegationMock: requiresUpgrade
? authorizationList?.[0]?.address
: undefined,
origin,
};

Expand Down
Loading