Skip to content

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

Description

@PatrickAlphaC

When an account-management (keyring) Snap registers a smart-contract account (e.g. a Safe 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 signingeth_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)

  2. Executing the account's own methodseth_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 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, 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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions