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:
- The verifier calls
isValidSignature(hash, signature)on the signer's contract - The contract returns
0x1626ba7e(magic value) if valid - 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:
-
Same chain verification — If the verifier is on the same chain as the SCA, ERC-1271 calls work directly.
-
Cross-chain verification — If verifying on a different chain, the verifier must have an RPC client for the SCA's chain. Use
parseKeyIdto 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
- Session key expiry — Ensure session keys have appropriate TTLs
- Signature validity — SCA signatures may have their own expiry
- Contract state — SCA authorization state can change (signers removed, thresholds changed)
- RPC dependency — ERC-1271 verification requires an RPC call (
eth_call), adding latency compared to EOA ecrecover
Best Practices
- Cache contract code — Check if address is a contract once, not per request
- Use appropriate timeouts — ERC-1271 calls can be slow
- Handle contract errors — Contracts may revert for various reasons
- Log SCA verification — Debugging SCA issues requires good observability