Verifying Account Abstracted Instant Actions (ERC-4337)

When using Account Abstraction with ERC-4337, you may need to verify a user’s signature on-chain (EIP-1271). This process allows smart contract wallets to sign off-chain data (via the user’s wallet logic) and then be validated on-chain by calling a contract’s isValidSignature() function.

The example below uses:

  • SIWE (Sign-In with Ethereum) to structure the message (nonce, domain, expiration, etc.).

  • ERC-4337 style message hashing to produce the correct pre-hashed data.

EIP-1271 on-chain validation via a contract call to isValidSignature().

Example Code Snippet

import json

from eth_typing import HexStr
from eth_utils import keccak, to_bytes, to_hex
from siwe import SiweMessage
from siwe import siwe
from web3 import Web3

RPC = ''
web3 = Web3(Web3.HTTPProvider(RPC))


def _encode_erc4337(message: SiweMessage) -> HexStr:
    data_str = message.prepare_message()
    print(data_str)
    hex_message = data_str.encode('utf-8').hex()
    presign_message_prefix = f'\x19Ethereum Signed Message:\n{len(data_str)}'
    prefix = presign_message_prefix.encode('utf-8').hex()
    combined_message = f'0x{prefix}{hex_message}'
    return to_hex(keccak(to_bytes(hexstr=combined_message)))


message_dict = {
    "account_address": "0x94b1e92397bC7Bc0964e1d6C736277AF27E5a76a",
    "expiration_time": "2024-12-19T08:09:21.231Z",
    "issued_at": "2024-12-16T08:09:21.231Z",
    "signature": "0xf8582d00465b648c7d3f0f06d1ca41553af52bf70e1f21b115c7a18a0f548dd032000319a7947d17575ffba4a358a9e5be4f9302216a41e22ba1f4aa7f5108ac20",
    "nonce": "1ESHS1N2VcQ"
}

domain = "vibe-ui-git-hedger-whitelist-link-vibe-tradings-projects.vercel.app"
uri = "https://base-hedger82.rasa.capital/login"
signature = message_dict['signature']
owner = "0x3Da94BF626065EF31dab46529ea0A69761a01a1e"

fields = {
    "domain": domain,
    "address": owner,
    "uri": uri,
    "version": siwe.VersionEnum.one,
    "chain_id": "8453",
    "nonce": message_dict["nonce"],
    "issued_at": message_dict["issued_at"],
    "statement": f'msg: {message_dict["account_address"]}',
    "expiration_time": message_dict["expiration_time"]
}

siwe_message = SiweMessage(**fields)
hash_message = _encode_erc4337(siwe_message)

with open('SignerValidatorAbi.json') as f:
    abi = json.load(f)

contract = web3.eth.contract(address=owner, abi=abi)

try:
    magic_value = contract.functions.isValidSignature(hash_message, signature).call()
    print("Magic Value Returned:", magic_value.hex())
    print(magic_value == Web3.to_bytes(hexstr=HexStr('0x1626ba7e')))
except Exception as e:
    print("Error calling isValidSignature:", e)

Below is a breakdown of how this code example verifies Instant Trades in an AA context.

Constructing the SIWE Message

fields = {
    "domain": domain,
    "address": owner,
    "uri": uri,
    "version": siwe.VersionEnum.one,
    "chain_id": "8453",
    "nonce": message_dict["nonce"],
    "issued_at": message_dict["issued_at"],
    "statement": f'msg: {message_dict["account_address"]}',
    "expiration_time": message_dict["expiration_time"]
}
siwe_message = SiweMessage(**fields)
  • SIWE (Sign-In with Ethereum) is used to define an off-chain message containing:

    • domain: The site or application domain.

    • address: The user’s (or smart wallets) address—this is an EIP-1271 contract in this example.

    • nonce & issued_at: Used to prevent replay attacks.

    • expiration_time: Defines how long this signed authorization is valid.

    • statement: here account_address is the ERC-4337 compliant wallet

    • chain_id: The chain on which the address is deployed.

Encoding the Message for EIP-1271 Verification

def _encode_erc4337(message: SiweMessage) -> HexStr:
    data_str = message.prepare_message()
    print(data_str)
    hex_message = data_str.encode('utf-8').hex()
    presign_message_prefix = f'\x19Ethereum Signed Message:\n{len(data_str)}'
    prefix = presign_message_prefix.encode('utf-8').hex()
    combined_message = f'0x{prefix}{hex_message}'
    return to_hex(keccak(to_bytes(hexstr=combined_message)))
  1. message.prepare_message() gives you the SIWE message as a string.

  2. The code applies EIP-191 style prefix: \x19Ethereum Signed Message:\n<length>. This is the same prefix typically used in MetaMask or other wallets for “personal_sign.”

  3. The prefixed message is then hashed with keccak-256.

The resulting hash_message becomes the “message hash” that the smart contract will verify.

On-Chain Signature Verification

contract = web3.eth.contract(address=owner, abi=abi)
magic_value = contract.functions.isValidSignature(hash_message, signature).call()
print("Magic Value Returned:", magic_value.hex())
print(magic_value == Web3.to_bytes(hexstr=HexStr('0x1626ba7e')))
  1. owner is not an EOA– it’s an EIP-1271-compatible contract (smart wallet).

  2. isValidSignature(bytes32 _hash, bytes _signature) is part of EIP-1271. The contract checks internally if the provided signature is valid for _hash.

  3. A valid signature must return 0x1626ba7e (the EIP-1271 magic value).

  4. If the returned value is not 0x1626ba7e, the signature is invalid.

Account Abstracted Flow– Verifying Instant Actions:

  1. The frontend generates a SIWE message for "instant actions"

    • E.g., “I authorize solver XYZ to execute opens/closes until expiration_time.”

    • The user (or user’s wallet) signs it off-chain, producing a signature.

  2. The solver/backend calls _encode_erc4337(siwe_message) to construct the same SIWE string and get the hash_message.

  3. The solver then invokes isValidSignature(hash_message, signature) on the user’s smart wallet contract:

  • Success (0x1626ba7e) means the user’s contract acknowledges the signature, so the solver can proceed with “instant actions.”

  • Failure (any other value or revert) means the signature is invalid or expired.

This ensures no new signature is required for every single open/close, provided the user already authorized a “session.”

Last updated

Logo

All rights to the people (c) 2023 Symmetry Labs A.G.