How to Make Your Dapp Compatible With Smart Contract Wallets Using ERC-1271

Learn how to verify signatures of smart contract wallets in your dapp by implementing ERC-1271.

In this tutorial, we will discuss smart contract wallets, how they work, and how to allow them to log in to your dapps. We will cover EIP-1271, which defines a way to verify signatures when an account is a smart contract (smart contract wallet). We will also discuss EIP-4337 as an additional context for smart contract wallets.

Smart Contract Wallets and ERC-4337

Smart Contract Wallets

A Smart Contract Wallet (SCW) is a type of cryptocurrency wallet that is built on top of smart contracts on a blockchain network, such as Ethereum. Unlike traditional Externally Owned Accounts (EOAs), which are controlled by private keys, smart contract wallets provide additional functionality and security features. They allow for more complex operations, customizable rules, recovery options and multi-signature capabilities.

Key Features of Smart Contract Wallets are:

  • Customizable: Users can add specific features or modify existing ones according to their needs.
  • Secure: These wallets use advanced security features such as multi-signature access and recovery mechanisms.
  • Upgradable: The functionality of smart contract wallets can be updated without losing stored assets.
  • Interoperable: They can interact with other smart contracts and decentralized applications (dApps) on blockchain networks.

ERC-4337

ERC-4337, also known as the Ethereum Improvement Proposal (EIP) 4337, is a standard for account abstraction in Ethereum. It aims to simplify the user experience by introducing a new transaction type, the UserOperation, which separates transaction verification from execution. This standard allows users to interact with the Ethereum network using smart contract wallets without having to manage gas fees and nonce management directly.

Key Components of ERC-4337 are:

  • UserOperation: A new transaction type that encapsulates the user's desired operation, including the sender, payload, and gas-related information. UserOperation separates the verification and execution steps, allowing for more efficient and secure transaction processing.

  • EntryPoint: A smart contract that serves as a gateway for UserOperations. It is responsible for validating and executing UserOperations and can interact with other contracts, such as paymasters and factories, to facilitate transactions.

  • Paymasters: These are smart contracts that can sponsor transactions on behalf of users. They enable various payment mechanisms, such as ERC-20 tokens, to be used for gas fees, offering greater flexibility for users.

  • Factories: Smart contracts responsible for creating new smart contract wallets. They use CREATE2 to ensure wallet addresses are deterministic and independent of the order of wallet creation. This allows users to generate wallet addresses locally without relying on an existing user or performing custom actions.

  • Reputation Scoring and Throttling: To prevent abuse and denial-of-service (DoS) attacks, ERC-4337 introduces reputation scoring and throttling mechanisms for global entities, such as paymasters and factories. Entities need to stake a certain amount of ETH to ensure their actions do not lead to the invalidation of other UserOperations.

In summary, ERC-4337 aims to improve the user experience by simplifying interactions with the Ethereum network and introducing advanced features through smart contract wallets. It separates transaction verification from execution, allows for more flexible gas payment options, and supports seamless account creation.

How can Contracts Sign Messages?

When a user connects their wallet to an application, they may be asked to sign a message to prove their identity. For an externally owned account (EOA), the user signs the message with their private key. The verifying party can use the recovery algorithm, such as ecrecover, to determine who signed the message.

Smart contract wallets can generate signatures, but they don't have a private key like an EOA. Instead, the smart contract itself provides a mechanism for verifying signatures. EIP-1271 solves this problem through a standard interface.

EIP-1271

EIP-1271 is a standard interface for contracts that want to verify signatures generated by smart contract wallets (SCWs). It was proposed as an Ethereum Improvement Proposal (EIP) in 2018 and has since been widely adopted by DApps that require signature verification.

The EIP-1271 interface defines a single function called isValidSignature. This function takes two arguments:

  • bytes32 _messageHash: The message hash that was signed
  • bytes _signature: The signature generated by the smart contract wallet

The function returns a bytes4 value indicating whether the signature is valid or not. The possible return values are:

  • 0x1626ba7e: Signature is valid
  • 0xffffffff: Signature is invalid

How Wallet Verification Usually Works

Here is how a common web3 authentication flow works:

  1. The server generates a message that the user must sign. This message contains a random nonce to protect from replay attacks.
  2. The user signs a hash of the message with their private key.
  3. The server verifies the signature using ecrecover (recovery algorithm) which, if the message was signed correctly, should return the address of the user.

But this approach does not directly work with smart contract wallets as you cannot use ecrecover on a SCW signature since SCWs can have custom signature validation logic.

How to Make Wallet Verification Work for Smart Contracts

To make wallet verification work for smart contracts, we need to modify the verification process. We should call the isValidSignature function of the smart contract wallet to verify if the signature is approved by it, as defined in EIP-1271.

Example script using ethers.js

Here is an example script to verify signatures that works for both EOAs and Smart Contract Wallets in ethers.js:

// importing the required modules from ethers.js
const { providers, utils, Contract } = require("ethers");

// importing ABI for interface of ERC1271 so we can call the `isValidSignature` function
const IERC1271Abi = [{"inputs":[{"internalType":"address[]","name":"addrs","type":"address[]"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"},{"indexed":false,"internalType":"bytes","name":"returnData","type":"bytes"}],"name":"LogErr","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"addr","type":"address"},{"indexed":false,"internalType":"bytes32","name":"priv","type":"bytes32"}],"name":"LogPrivilegeChanged","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Identity.Transaction[]","name":"txns","type":"tuple[]"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"execute","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Identity.Transaction[]","name":"txns","type":"tuple[]"}],"name":"executeBySelf","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Identity.Transaction[]","name":"txns","type":"tuple[]"}],"name":"executeBySender","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"isValidSignature","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"nonce","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"privileges","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"},{"internalType":"bytes32","name":"priv","type":"bytes32"}],"name":"setAddrPrivilege","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceID","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"tipMiner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"tryCatch","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]

// This is a constant magic value defined in EIP-1271 that's returned when the signature is valid
const MAGICVALUE = 0x1626ba7e;

// function to check if a signature is valid
const isValidSignature = async (signingAddress, message, signature) => {
  const hash = utils.hashMessage(message); // hash the message
  const apiKey = "demo" // replace with your Alchemy API key for the network you are verifying the signature for, in this case Polygon Mainnet
  const provider = new providers.JsonRpcProvider(
    `https://polygon-mainnet.g.alchemy.com/v2/${apiKey}`
  ); // get your provider
  const bytecode = await provider.getCode(signingAddress); // get the bytecode
  const isSmartContract = bytecode && utils.hexStripZeros(bytecode) !== "0x"; // check if it is a smart contract wallet

  if (isSmartContract) {
    // verify the message for a decentralized account (contract wallet)
    const contractWallet = new Contract(signingAddress, IERC1271Abi, provider); // make an instance for the contact wallet
    const verification = await contractWallet.isValidSignature(hash, signature); // verify if the signature is valid using the `isValidSignature` function
    console.log("Message is verified?", verification === MAGICVALUE); // log if the signature is valid
    return verification === MAGICVALUE; // return true or false based on if the signature is valid or not
  } else {
    // verify the message for an externally owned account (EOA) using the recovery algorithm
    const sig = ethers.utils.splitSignature(signature);
    const recovered = await contract.verifyHash(hash, sig.v, sig.r, sig.s);
    console.log("Message is verified?", recovered === signingAddress);
    return recovered === signingAddress;
  }
};

async function main() {
  let isValid = await isValidSignature(
    "0x4836a472ab1dd406ecb8d0f933a985541ee3921f",
    "0x787177",
    "0xc0f8db6019888d87a0afc1299e81ef45d3abce64f63072c8d7a6ef00f5f82c1522958ff110afa98b8c0d23b558376db1d2fbab4944e708f8bf6dc7b977ee07201b00"
  );

  console.log(isValid);
}

main();

Here, we check if an account is a smart contract wallet or an EOA, if it's a SCW we call the isValidSignature function of it to verify the validity of the signature, otherwise, we verify the signature validity using the recovery algorithm for EOA.

Currently, there is a PR in the Ethers Github repo to add this functionality directly to Ethers. Signature validation for SCWs using Ethers will become much easier once the PR is merged.

Libraries for SCW Signature Verification

There are also some libraries that make the signature verification process easy for the developers in case of smart contract wallets. Examples of such libraries are eip1271-verification-util and signature-validator.

Below you can see an example script that depicts the usage of eip1271-verification-util library to verify signatures for smart contract wallets in front-end dapps:

📘

NOTE

The code given below will not work in a nodejs project, it will only work in a front-end application as the library eip1271-verification-util is specifically made for dapps to verify smart contract wallet signatures.

// Setup: npm i @etherspot/eip1271-verification-util
// importing ethers
import ethers from "ethers";

// importing the `isValidEip1271Signature` function from `eip1271-verification-util`
import { isValidEip1271Signature } from "@etherspot/eip1271-verification-util";

const checkSig = async () => {
  // the random message (nonce) that was signed
  const data = "0x787177";
  
  // defining signer and the rpc url
	const signerAddress = '0x4836a472ab1dd406ecb8d0f933a985541ee3921f';

	// the rpc url to make requests
	const rpcUrl = 'https://polygon-mainnet.g.alchemy.com/v2/demo'
  
  // The signature to verify as a hex string
  const signature = '0xc0f8db6019888d87a0afc1299e81ef45d3abce64f63072c8d7a6ef00f5f82c1522958ff110afa98b8c0d23b558376db1d2fbab4944e708f8bf6dc7b977ee07201b00' 
  
  // Hashed data used for the signature to verify. The dApp will need to pre-compute this as no hashing will occur in the function, and this will be directly used in isValidEip1271Signature
  const hash = ethers.utils.hashMessage(ethers.utils.arrayify(data)); 

  // calling the imported function to verify the signature and passing the required params
  const isValidSig = await isValidEip1271Signature(
    rpcUrl,
    signerAddress,
    hash,
    signature
  );

	// logging if the signature is valid or not
  console.log("is signature valid:", isValidSig);
};

As you can see that using the isValidEip1271Signature function of eip1271-verification-util library you can verify the signatures of smart contract wallets.

Conclusion

In conclusion, EIP-1271 is an essential proposal that defines a standard for smart contracts to verify signatures. It allows people to use smart contract wallets with decentralized apps. Implementing EIP-1271 is crucial for any dApp that wants to stay ahead of the curve.


ReadMe