Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: prevent external transactions to internal accounts if data included #5418

Merged
merged 6 commits into from
Mar 11, 2025
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]

### Changed

- Prevent external transactions to internal accounts if `data` included ([#5418](https://github.com/MetaMask/core/pull/5418))

## [48.0.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,7 @@ export class TransactionController extends BaseController<
const internalAccounts = this.#getInternalAccounts();

await validateTransactionOrigin({
data: txParams.data,
from: txParams.from,
internalAccounts,
origin,
Expand Down
32 changes: 27 additions & 5 deletions packages/transaction-controller/src/utils/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { TransactionEnvelopeType, TransactionType } from '../types';
import type { TransactionParams } from '../types';

const DATA_MOCK = '0x12345678';
const FROM_MOCK = '0x1678a085c290ebd122dc42cba69373b5953b831d';
const TO_MOCK = '0xfbb5595c18ca76bab52d66188e4ca50c7d95f77a';
const ORIGIN_MOCK = 'test-origin';
Expand Down Expand Up @@ -680,9 +681,10 @@ describe('validation', () => {
);
});

it('throws if external and to is internal account', async () => {
it('throws if external and to is internal account and data', async () => {
await expect(
validateTransactionOrigin({
data: DATA_MOCK,
from: FROM_MOCK,
internalAccounts: [TO_MOCK],
origin: ORIGIN_MOCK,
Expand All @@ -693,11 +695,33 @@ describe('validation', () => {
}),
).rejects.toThrow(
rpcErrors.invalidParams(
'External transactions to internal accounts are not supported',
'External transactions to internal accounts cannot include data',
),
);
});

it.each([
['undefined', undefined],
['empty', ''],
['empty hex', '0x'],
])(
'does not throw if external and to is internal account but data is %s',
async (_title, data) => {
expect(
await validateTransactionOrigin({
data,
from: FROM_MOCK,
internalAccounts: [TO_MOCK],
origin: ORIGIN_MOCK,
selectedAddress: '0x123',
txParams: {
to: TO_MOCK,
} as TransactionParams,
}),
).toBeUndefined();
},
);

it('does not throw if external and to is internal account but type is batch', async () => {
expect(
await validateTransactionOrigin({
Expand Down Expand Up @@ -752,9 +776,7 @@ describe('validation', () => {
},
}),
).toThrow(
rpcErrors.invalidParams(
'External transactions to internal accounts are not supported',
),
rpcErrors.invalidParams('Calls to internal accounts are not supported'),
);
});

Expand Down
10 changes: 8 additions & 2 deletions packages/transaction-controller/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type GasFieldsToValidate =
* Validates whether a transaction initiated by a specific 'from' address is permitted by the origin.
*
* @param options - Options bag.
* @param options.data - The data included in the transaction.
* @param options.from - The address from which the transaction is initiated.
* @param options.internalAccounts - The internal accounts added to the wallet.
* @param options.origin - The origin or source of the transaction.
Expand All @@ -38,6 +39,7 @@ type GasFieldsToValidate =
* @throws Throws an error if the transaction is not permitted.
*/
export async function validateTransactionOrigin({
data,
from,
internalAccounts,
origin,
Expand All @@ -46,6 +48,7 @@ export async function validateTransactionOrigin({
txParams,
type,
}: {
data?: string;
from: string;
internalAccounts?: string[];
origin?: string;
Expand Down Expand Up @@ -82,15 +85,18 @@ export async function validateTransactionOrigin({
);
}

const hasData = Boolean(data && data !== '0x');

if (
isExternal &&
hasData &&
internalAccounts?.some(
(account) => account.toLowerCase() === to?.toLowerCase(),
) &&
type !== TransactionType.batch
) {
throw rpcErrors.invalidParams(
'External transactions to internal accounts are not supported',
'External transactions to internal accounts cannot include data',
);
}
}
Expand Down Expand Up @@ -279,7 +285,7 @@ export function validateBatchRequest({
)
) {
throw rpcErrors.invalidParams(
'External transactions to internal accounts are not supported',
'Calls to internal accounts are not supported',
);
}
}
Expand Down
Loading