Support smart-contract / multisig accounts (e.g. Safe) via keyring Snaps — relax the internal-account guards
- 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