Background
In the world of ethereum, a signature serves as a critical mechanism to prove that a specific message or transaction originates from a particular address. This is achieved by combining a message with cryptographic proof that ensures its authenticity. The Ethereum Virtual Machine (EVM) relies on the widely used Elliptic Curve Digital Signature Algorithm (ECDSA) to validate these signatures.
The Standard Signature Validation Process
When interacting with decentralized applications (dApps), users often need to verify their identity or prove ownership of their wallet address. A common workflow for this involves the following steps:
- Challenge-Response Mechanism:
- The dApp issues a challenge, which is typically a random message that the user must sign.
- The user's wallet signs the message and returns a signature to the dApp.
- Verification Using
ecrecover
:- The dApp verifies the signature using the EVM's
ecrecover
function. This function recovers the public key used to sign the message. - If the recovered address matches the user's wallet address, the dApp can confirm ownership and proceed with the login or transaction request.
- The dApp verifies the signature using the EVM's
For subsequent interactions, the dApp may repeat this process to ensure that the user retains control of their wallet. This traditional method works seamlessly for Externally Owned Accounts (EOAs), as EOAs have private keys to sign messages.
The Rise of Account Abstraction
With the growing popularity of account abstraction, the landscape of blockchain wallets and accounts is evolving. In an account abstraction model:
- A smart contract can function as a wallet.
- Unlike EOAs, smart contracts do not possess private keys, which are fundamental for signing messages in EVM-based blockchains.
This introduces a key limitation: while EOAs can easily sign messages and validate signatures, smart contract wallets cannot. In other words, without a private key, a smart contract wallet cannot natively sign messages or prove ownership using the existing EVM signature validation methods.
The Need for Signing as Smart Contract
As account abstraction gains traction, the ability for smart contracts to participate in signature-based workflows becomes increasingly important. This is where ERC-1271, the standard for validating signatures on behalf of smart contracts, comes into play.
ERC-1271 Solution
To address the limitations of smart contracts in signature validation workflows, ERC-1271 introduces a solution that enables smart contracts to mimic the behavior of EOAs when it comes to signature validation. While a smart contract cannot truly "sign" a message like an EOA (since it lacks a private key), it can implement a mechanism to verify signatures, effectively acting as a proxy signer.
How it Works
ERC-1271 shifts the responsibility of signature validation from external processes to the smart contract itself. By implementing the ERC-1271 interface, a smart contract becomes capable of validating whether a given signature is valid for a specific message or hash
Let’s take a look at the official ERC-1271 interface
pragma solidity ^0.5.0;
contract ERC1271 {
// bytes4(keccak256("isValidSignature(bytes32,bytes)")
bytes4 constant internal MAGICVALUE = 0x1626ba7e;
/**
* @dev Should return whether the signature provided is valid for the provided hash
* @param _hash Hash of the data to be signed
* @param _signature Signature byte array associated with _hash
*
* MUST return the bytes4 magic value 0x1626ba7e when function passes.
* MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5)
* MUST allow external calls
*/
function isValidSignature(
bytes32 _hash,
bytes memory _signature)
public
view
returns (bytes4 magicValue);
}
At its core, ERC-1271 defines a function called isValidSignature(bytes32 hash, bytes memory signature)
, which smart contracts must implement. This function performs the following:
- Accepts the message or hash (
hash
) and its corresponding signature (signature
) as inputs. - Returns a magic value (
0x1626ba7e
) indicating the signature is valid. The implementation must return other than the magic value if the signature is invalid.
Here is the updated illustration of signature verification workflow:
The key point of what ERC-1271 brings into the table are:
- Verification Delegated to Smart Contracts
- Instead of relying on EOA-based
ecrecover
or similar methods, the dApp calls the smart contract directly to verify the signature. - The smart contract acts as a verifier, determining whether the signature is valid according to its own rules and logic.
- Instead of relying on EOA-based
- Flexibility in Implementation
- The standard does not prescribe how the signature is created or validated. This allows developers to implement custom signature mechanisms tailored to the requirements of their dApps or smart contracts.
- For example, a smart contract could validate signatures based on time-sensitive conditions, multi-signature schemes, or even entirely new cryptographic methods.
- Example of possible implementations:
- Single Owner EOA-dependent: The smart contract still relies on EOA-signed messages.
- Multi-signature schemes: The smart contract validates based on multiple approvals.
- Time or state-based conditions: The contract checks for validity against a specific on-chain state or timestamp.
- Entirely new cryptographic method
Example Implementation
dApps Side Implementation
import * as ethers from "ethers";
const MAGICVALUE = 0x1626ba7e;
const IERC1271ABI = [
{
inputs: [
{
internalType: "bytes32",
name: "hash",
type: "bytes32",
},
{
internalType: "bytes",
name: "signature",
type: "bytes",
},
],
name: "isValidSignature",
outputs: [
{
internalType: "bytes4",
name: "magicValue",
type: "bytes4",
},
],
stateMutability: "view",
type: "function",
},
];
const isValidSignature = async (
signingAddress: string,
message: string,
signature: string
): Promise<Boolean> => {
const hash = ethers.hashMessage(message);
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL as string);
const code = await provider.getCode(signingAddress); // get code located at signingAddress
const isSmartContract = code && ethers.stripZerosLeft(code) !== "0x";
if (isSmartContract) {
const contract = new ethers.Contract(signingAddress, IERC1271ABI, provider);
const verificationResult = await contract.isValidSignature(hash, signature);
return verificationResult === MAGICVALUE;
} else {
const actualSigningAddress = ethers.verifyMessage(message, signature); // use ecrecover to recover actual signing address
return actualSigningAddress === signingAddress;
}
};
Dumb/Vulnerable Smart Contract
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
contract DumbERC1271 {
bytes4 internal constant MAGICVALUE = 0x1626ba7e;
function isValidSignature(
bytes32 hash,
bytes calldata signature
) external view returns (bytes4 magicValue) {
// no verification, allow all signature.
// basically everyone can transact/act as this smart contract
return MAGICVALUE;
}
}
Code for create signature:
No need. Any signature is accepted 😂
Single Owner Smart Contract
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
contract SmartAccount1271 {
bytes4 internal constant MAGICVALUE = 0x1626ba7e;
address public owner;
constructor(address _owner) {
// the smart contract is controlled by EOA
// saving the address of the EOA
owner = _owner;
}
function isValidSignature(
bytes32 hash,
bytes calldata signature
) external view returns (bytes4 magicValue) {
// checking whether the signature is coming from the owner
bool success = SignatureChecker.isValidSignatureNow(
owner,
hash,
signature
);
magicValue = success ? MAGICVALUE : bytes4("");
}
}
Code for create signature:
import * as ethers from "ethers";
export const signERC1271Message = async (msg: string): Promise<string> => {
// use the EOA owner private key which is used in the contract constructor
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY as string);
const signature = await wallet.signMessage(msg);
return signature;
};
FAQ
1. Why is the return value 0x1626ba7e
and not simply true
?
The return value 0x1626ba7e
was chosen to avoid potential vulnerabilities caused by unintentional or incorrect fallback function behavior.
- If a smart contract does not implement the
isValidSignature
function, its fallback function will be called. - A fallback function can return anything, depending on its implementation. It's much more likely for a fallback function to accidentally return
true
(a common return value for general logic) than the specific hexadecimal constant0x1626ba7e
. - By requiring a unique, predefined value, ERC-1271 reduces the risk of false positives and ensures that the signature validation mechanism remains secure and intentional.
2. Who implements ERC-1271?
The implementation of ERC-1271 is divided between two parties:
- Smart Contracts: The smart contract that wants to act as a signer implements the ERC-1271 interface. It must define the
isValidSignature
function and handle signature validation logic. - dApps: Decentralized applications must integrate support for ERC-1271 signature verification to interact with smart contract wallets. This involves calling the
isValidSignature
function instead of traditional EOA-based signature checks likeecrecover
.
3. How does ERC-1271 handle smart contracts that are not yet deployed?
For scenarios where the smart contract is not yet deployed, ERC-6492 offers a solution. ERC-6492 provides a mechanism to handle signatures for smart contracts that are still in the deployment phase. This is particularly relevant for account abstraction and gasless wallet setups. The details of ERC-6492 will be covered in a future article.
4. Why is ERC-1271 important?
ERC-1271 plays a crucial role in the evolution of blockchain wallet infrastructure, especially in the context of account abstraction.
- Account Abstraction: This paradigm allows smart contracts to act as wallets instead of relying solely on EOAs. Unlike EOAs, smart contracts do not have private keys, so a new signature verification mechanism is necessary.
- Flexibility: EOAs are tied to specific cryptographic implementations (e.g., ECDSA), making it difficult to upgrade or extend their functionality. Smart contract wallets, on the other hand, offer far greater flexibility.
- Protocol Compatibility: ERC-1271 enables smart contracts to act as wallets without requiring changes to the core EVM protocol, making it a seamless and backward-compatible solution.
- Future-Proofing: By abstracting wallet functionality into smart contracts, developers can innovate and expand the capabilities of wallets without the limitations imposed by EOAs.