MetaMask / MetaMask/core

Support smart-contract / multisig accounts (e.g. Safe) via keyring Snaps — relax the internal-account guards

Open
#9,179 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
413
Forks
308
Avg merge
1d 4h
Merged PRs (30d)
253

Description

When an account-management (keyring) Snap registers a smart-contract account (e.g. a [Safe](https://safe.global) multisig) as a MetaMask account, two "internal account" guards in **MetaMask/core** block the account's normal operations from the same MetaMask:

1. **EIP-1271 signing** — `eth_signTypedData_v4` whose `domain.verifyingContract` is the account is rejected:
> External signature requests cannot use internal accounts as the verifying contract

`packages/signature-controller/src/utils/validation.ts` (`validateVerifyingContract`, added in [#5470](https://github.com/MetaMask/core/pull/5470))

2. **Executing the account's own methods** — `eth_sendTransaction` to the account with calldata (e.g. Safe `execTransaction`) is rejected:
> External transactions to internal accounts cannot include data

`packages/transaction-controller/src/utils/validation.ts`

## Why this is wrong for multisigs

Both guards assume an internal account is a single-key EOA whose key the user holds. For an M-of-N multisig that's false:

- A single signature is worthless without the threshold of other owners' signatures — a phishing dApp can't do anything with one tricked signature.
- EIP-1271 with `verifyingContract` = the Safe is the **defined** way a Safe signature works (`isValidSignature`), not an exploit.
- Calling a contract account's own methods with calldata (`execTransaction`) is how the account is operated.

The guards should key off "is this a single-key EOA?", not "is this address one of my accounts?"

## Re: the prior resolution

[#6239](https://github.com/MetaMask/core/issues/6239) hit guard #1 for EIP-7702 and was closed as intentional, pointing to the Delegation Toolkit. That doesn't apply here: a Safe is an existing, deployed M-of-N contract with real owners and funds — not a 7702-upgraded EOA, an ERC-4337 account, or a MetaMask delegator. There's nothing to migrate.

## Requested change (any one)

1. Scope both guards to **EOA-type** internal accounts — skip them when the account is a contract (non-empty `eth_getCode`) or a keyring-Snap-declared contract account.
2. Add a **smart-contract account type** to the Keyring API (beyond ERC-4337) so multisigs can be modeled, and apply the EOA guards only to EOAs.
3. At minimum, a **per-account override** for these two guards.
4. **Or, more broadly:** support common multisig wallets (e.g. Safe) as a **native** MetaMask account type — recognize the deployed contract, relay the signing request to its owners, and validate via EIP-1271 — so multisig support doesn't depend on each team shipping a keyring Snap, and these guards stop applying for the common case.

## Reproduction

With the LocalSafe keyring Snap ([Cyfrin/localsafe.eth#80](https://github.com/Cyfrin/localsafe.eth/pull/80), MetaMask Flask), register a deployed Safe as an account, then from the same MetaMask: a dApp `eth_signTypedData_v4` with `verifyingContract` = the Safe hits guard #1; executing the Safe's `execTransaction` hits guard #2. Both work only after removing the Safe from MetaMask or using a different wallet. (EIP-1271 sign-in itself works end-to-end — verified against the MetaMask Test Dapp — so only these guards block the flow.)

## Related

- [core#6239](https://github.com/MetaMask/core/issues/6239) — same signing guard (EIP-7702), closed as intentional
- [metamask-extension#32495](https://github.com/MetaMask/metamask-extension/issues/32495) — the "cannot include data" guard (open)
- [core#5470](https://github.com/MetaMask/core/pull/5470) — PR that added the signing guard

_Custom EVM Account Snap allowlisting is currently paused, so this is Flask-only today; a working keyring Snap is available for repro. Reported by the LocalSafe team (Cyfrin)._

Please take a look at this this video to see the multisig support "sort of" working as a snap:

https://youtu.be/C1s2IUJ3O9E

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with packages/signature-controller/src/utils/validation.ts and packages/transaction-controller/src/utils/validation.ts, focusing on validateVerifyingContract and the internal-account calldata guard. Reproduce the failures with the LocalSafe keyring Snap and the described EIP-1271 and Safe execTransaction flows. Done means supported contract accounts can perform these operations without weakening the existing single-key EOA protections.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authorization, backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.