diff --git a/packages/signature-controller/src/utils/validation.test.ts b/packages/signature-controller/src/utils/validation.test.ts index effa714ab81..e9f1654adca 100644 --- a/packages/signature-controller/src/utils/validation.test.ts +++ b/packages/signature-controller/src/utils/validation.test.ts @@ -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', () => { diff --git a/packages/signature-controller/src/utils/validation.ts b/packages/signature-controller/src/utils/validation.ts index d846dd2d093..db3e55b0323 100644 --- a/packages/signature-controller/src/utils/validation.ts +++ b/packages/signature-controller/src/utils/validation.ts @@ -189,6 +189,7 @@ function validateTypedSignatureRequestV3V4({ data, internalAccounts, origin, + signerAddress: messageData.from, }); validateDelegation({ @@ -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(),