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
bytes4 constant internal MAGICVALUE = 0x1626ba7e;: The magic value returned when verification succeeds. It is the first 4 bytes of the keccak256 hash of the signature isValidSignature(bytes32,bytes).
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
- Do not fix the amount of gas for the call: The isValidSignature call has no set gas limit, so some implementations may use a lot of gas. If the amount of gas sent when calling this function on an external contract is hardcoded, some signatures may fail to verify.
- Responsibility for verification lies with the implementing contract: Each contract that implements this function must itself ensure that the signature passed in is actually valid. Otherwise, the consequences can be severe.