ERC-1271 Contract signature validation standard (Standard Signature Validation Method for Contracts)

Final · Signatures & auth · created 2018-07-25

Lets a smart contract without a private key report whether a signature made on its behalf is valid. When a signing contract implements the isValidSignature function, any app can verify signatures in the same way.

Why it is needed

A regular Ethereum account (EOA, Externally Owned Account) can sign messages with a private key. A smart contract, however, has no private key and cannot produce a signature in the same way. Meanwhile, many contracts already check authority to move assets using signed messages, and more will do so. For example, a decentralized exchange that keeps its order book off-chain receives buy and sell orders as signed messages. The user expresses the intent to trade with a signature, and this gives the exchange contract the authority to finalize the trade. Contract accounts such as contract wallets, DAOs and multisig wallets could not use such apps. There was no common way for a contract to say which signature is a valid one made on its behalf. This standard defines that way as a single function.

What to implement

In depth

One function and one magic value

The standard requires only one function, isValidSignature(bytes32 _hash, bytes memory _signature). _hash is the hash of the data being signed, and _signature is the byte array holding the signature over that hash. The return type is bytes4, and if verification passes it MUST return the magic value 0x1626ba7e. This value is bytes4(keccak256("isValidSignature(bytes32,bytes)")). The reason for returning a specific value instead of a boolean is to make signature verification stricter and simpler. The caller only needs to compare whether the return value equals 0x1626ba7e. The reference implementation returns 0xffffffff on failure, but the standard only specifies the value on success.

It must be callable externally without changing state

isValidSignature MUST NOT change state. Below solc 0.5 it is called with STATICCALL, and from 0.5 onward the view modifier is attached. It also MUST allow external calls. The reason for forbidding state changes is to block attack paths such as GasToken minting. Another purpose is to keep the implementation scope simple so standardization is easier, and to allow querying the contract from off-chain.

The contract decides the verification rules

Inside isValidSignature, any method can be called to verify the signature. The source gives three examples. Context-dependent verification that varies with time or state, EOA-dependent verification that checks the signer's permission level inside a smart wallet, and signature-scheme-dependent verification such as ECDSA, multisig or BLS. This function is implemented by contracts that want to sign messages, namely smart contract wallets, DAOs, multisig wallets and the like. Apps that want to support contract signatures call this function when the signer is a contract. The word "valid" is used in the function name because a signing contract regards only signatures made by an authorized signer as valid. A signed action message is valid only when that signer has the authority to perform that action on behalf of the wallet.

Why it takes a hash rather than a message

The arguments are split into a hash and a signature to separate the signed data from the signature simply. The reason it takes a bytes32 hash instead of the original message is that a contract may expect a specific non-standard hash function, as EIP-712 does. The standard does not specify how the hash is built. So the app must build the hash the way the signing contract expects and pass it in. Whatever hashing method is used, the function signature stays the same with a single bytes32.

Reference implementation: recovering the owner signature

The reference implementation in the original checks whether the signer is the contract's owner. It recovers the signer with recoverSigner(_hash, _signature) and returns 0x1626ba7e if the signer equals owner, or 0xffffffff otherwise. recoverSigner is for EthSign signatures only, and the signature must be 65 bytes in the order (bytes32 r, bytes32 s, uint8 v). To prevent signature malleability, it reverts if s is greater than 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, and it accepts only 27 or 28 for v. A comment notes that if a library produces v as 0/1, 27 can be added to make it acceptable. If the ecrecover result is the 0x0 address, it is treated as a failure. These checks are credited to OpenZeppelin's ECDSA library as their source.

Caller-side implementation and backwards compatibility

The original also gives an example for the side that calls the signing contract. callERC1271isValidSignature(address _addr, bytes32 _hash, bytes calldata _signature) calls IERC1271Wallet(_addr).isValidSignature(_hash, _signature) and reverts with INVALID_SIGNATURE if the result is not equal to 0x1626ba7e. The return value is not interpreted as a boolean but compared exactly against the magic value. This standard applies only to contract signatures and does not cover EOA signatures, so it does not conflict with existing signature verification methods and can be used alongside them.

Things to watch out for

Original ERC · ← ERCs