Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Smart Contract Accounts – ERC-8128
Skip to content

Smart Contract Accounts

How to use ERC-8128 with smart contract accounts (SCAs) like ERC-4337 wallets and Safe.

How It Works

ERC-8128 supports smart contract accounts through ERC-1271 signature verification. When verifying a signature:

  1. The verifier calls isValidSignature(hash, signature) on the signer's contract
  2. The contract returns 0x1626ba7e (magic value) if valid
  3. The signature format is contract-dependent (could be multi-sig, threshold, etc.)

Signing with an SCA

To use ERC-8128 with a smart contract account, create an EthHttpSigner where the address is the contract address and signMessage produces a signature the contract will accept via ERC-1271.

Session Keys

Many SCAs support session keys — temporary EOAs authorized to sign on behalf of the contract:

import type { EthHttpSigner } from '@slicekit/erc8128'
 
const sessionKeySigner: EthHttpSigner = {
  // The smart contract account address
  address: '0xSmartContractAccount...',
  chainId: 1,
  
  signMessage: async (message) => {
    // Sign with the session key EOA
    const sessionSig = await sessionKeyAccount.signMessage({
      message: { raw: message },
    })
    
    // Some SCAs require wrapping the signature with metadata
    // Check your SCA's documentation
    return sessionSig
  },
}

Delegate Signing

Some SCAs allow a delegate key to sign on behalf of the contract without wrapping. The contract's isValidSignature implementation verifies the delegate internally:

const delegateSigner: EthHttpSigner = {
  address: '0xSmartContractAccount...',
  chainId: 1,
  
  signMessage: async (message) => {
    // The SCA will verify this delegate signature internally
    return delegateKey.signMessage({ message: { raw: message } })
  },
}

Verifying SCA Signatures

The verifyMessage function you pass to verifyRequest must support ERC-1271:

With viem

viem's verifyMessage automatically handles both EOA signatures (ecrecover) and ERC-1271 contract signatures:

import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
 
const publicClient = createPublicClient({
  chain: mainnet,
  transport: http(),
})
 
// Use directly in policy — works for both EOA and SCA
const result = await verifyRequest({
  request,
  verifyMessage: publicClient.verifyMessage,
  nonceStore
})

No extra configuration is needed. When the signer address is a contract, viem automatically calls isValidSignature on it instead of using ecrecover.

Common SCA Patterns

Safe

Safe accounts require collecting signatures from multiple owners before producing a combined signature that passes ERC-1271 validation. Use the Safe Protocol Kit to manage this:

import Safe from '@safe-global/protocol-kit'
 
const protocolKit = await Safe.init({
  provider: RPC_URL,
  signer: OWNER_PRIVATE_KEY,
  safeAddress: SAFE_ADDRESS,
})
 
const safeSigner: EthHttpSigner = {
  address: SAFE_ADDRESS,
  chainId: 1,
  
  signMessage: async (message) => {
    // Create and sign the message with the Protocol Kit
    const safeMessage = protocolKit.createMessage(bytesToHex(message))
    const signed = await protocolKit.signMessage(safeMessage)
    
    // Encode signatures for ERC-1271 verification
    return buildSignatureBytes(
      Array.from(signed.signatures.values())
    )
  },
}

For multi-owner Safes, collect signatures from each owner via protocolKit.signMessage() before encoding. See the Safe SDK docs for details.

ERC-4337 Wallets

Most ERC-4337 smart accounts expose a signMessage method through their SDK. The exact API varies by provider (ZeroDev, Alchemy, Biconomy, etc.):

const erc4337Signer: EthHttpSigner = {
  address: smartAccountAddress,
  chainId: 1,
  
  signMessage: async (message) => {
    // Most 4337 wallets expose a signMessage method
    // The exact API depends on the wallet provider
    return smartAccount.signMessage({ message: { raw: message } })
  },
}

Chain Considerations

When using SCAs, the chainId in the keyid matters:

  1. Same chain verification — If the verifier is on the same chain as the SCA, ERC-1271 calls work directly.

  2. Cross-chain verification — If verifying on a different chain, the verifier must have an RPC client for the SCA's chain. Use parseKeyId to extract the chain and route to the right client:

import { parseKeyId } from '@slicekit/erc8128'
 
// Multi-chain verifier using the keyid from the signed request
function getVerifyMessage(keyid: string): VerifyMessageFn {
  const parsed = parseKeyId(keyid)
  if (!parsed) throw new Error('Invalid keyid')
  
  const publicClient = getClientForChain(parsed.chainId)
  return publicClient.verifyMessage
}

Security Considerations

  1. Session key expiry — Ensure session keys have appropriate TTLs
  2. Signature validity — SCA signatures may have their own expiry
  3. Contract state — SCA authorization state can change (signers removed, thresholds changed)
  4. RPC dependency — ERC-1271 verification requires an RPC call (eth_call), adding latency compared to EOA ecrecover

Best Practices

  1. Cache contract code — Check if address is a contract once, not per request
  2. Use appropriate timeouts — ERC-1271 calls can be slow
  3. Handle contract errors — Contracts may revert for various reasons
  4. Log SCA verification — Debugging SCA issues requires good observability