Skip to content
Open
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
41 changes: 41 additions & 0 deletions packages/signature-controller/src/utils/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,47 @@ describe('Validation Utils', () => {
}),
).not.toThrow();
});

it('does not throw if external origin and verifying contract equals signer address (EIP-7702)', () => {
// EIP-7702 allows EOAs to temporarily delegate to smart contracts.
// When delegated, the user's address becomes the verifying contract.
// This is a legitimate use case that should be allowed.
const signerAddress = '0x3244e191f1b4903970224322180f1fbbc415696b';
const data = JSON.parse(DATA_TYPED_MOCK);
data.domain.verifyingContract = signerAddress;

expect(() =>
validateTypedSignatureRequest({
currentChainId: CHAIN_ID_MOCK,
internalAccounts: ['0x1234', signerAddress as Hex],
messageData: {
data,
from: signerAddress,
},
request: { origin: ORIGIN_MOCK } as OriginalRequest,
version,
}),
).not.toThrow();
});

it('does not throw if external origin and verifying contract equals signer address with different case (EIP-7702)', () => {
const signerAddress = '0x3244e191f1b4903970224322180f1fbbc415696b';
const data = JSON.parse(DATA_TYPED_MOCK);
data.domain.verifyingContract = signerAddress.toUpperCase();

expect(() =>
validateTypedSignatureRequest({
currentChainId: CHAIN_ID_MOCK,
internalAccounts: ['0x1234', signerAddress as Hex],
messageData: {
data,
from: signerAddress,
},
request: { origin: ORIGIN_MOCK } as OriginalRequest,
version,
}),
).not.toThrow();
});
});

describe('delegation', () => {
Expand Down
16 changes: 16 additions & 0 deletions packages/signature-controller/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ function validateTypedSignatureRequestV3V4({
data,
internalAccounts,
origin,
signerAddress: messageData.from,
});

validateDelegation({
Expand Down Expand Up @@ -220,23 +221,38 @@ function validateAddress(address: string, propertyName: string) {
* @param options.data - The typed data to validate.
* @param options.internalAccounts - The internal accounts.
* @param options.origin - The origin of the request.
* @param options.signerAddress - The address that will sign the message.
*/
function validateVerifyingContract({
data,
internalAccounts,
origin,
signerAddress,
}: {
data: MessageParamsTypedData;
internalAccounts: Hex[];
origin: string | undefined;
signerAddress?: string;
}) {
const verifyingContract = data?.domain?.verifyingContract;
const isExternal = origin && origin !== ORIGIN_METAMASK;

// EIP-7702 exception: When a user's EOA is delegated to a smart contract via EIP-7702,
// the EOA itself becomes the verifying contract. In this case, signatures where the
// verifyingContract matches the signer's address are legitimate and should be allowed.
// This enables protocols like account abstraction and sponsored transactions where
// the user signs messages verified by their own (temporarily upgraded) address.
const isEIP7702SelfSignature =
signerAddress &&
verifyingContract &&
typeof verifyingContract === 'string' &&
signerAddress.toLowerCase() === verifyingContract.toLowerCase();

if (
verifyingContract &&
typeof verifyingContract === 'string' &&
isExternal &&
!isEIP7702SelfSignature &&
internalAccounts.some(
(internalAccount) =>
internalAccount.toLowerCase() === verifyingContract.toLowerCase(),
Expand Down