What is ecrecover in Solidity?
A dive into the waters of signatures for smart contracts
Ever wondered what the hell the deal is with the ecrecover
command in Solidity?
It's all about signatures and keys...
What is ecrecover ?
You may have seen ecrecover
in a Solidity contract before and wondered what exactly the deal with this was. Well you came across the EVM precompile ecrecover. A precompile just means a common functionality for smart contracts which has been compiled, so Ethereum nodes can run this efficiently. From a contract's perspective this is just a single command like an opcode.
Look at the following code:
function recoverSignerFromSignature(uint8 v, bytes32 r, bytes32 s, bytes32 hash) external {
address signer = ecrecover(hash, v, r, s);
require(signer != address(0), "ECDSA: invalid signature");
}
This is essentially how one would use it, though there's more to it than this. Don't actually use above code in production as Patricio Palladino correctly pointed out. The correct way is shown in the last examples at the bottom of this post.
So what does all this mean? Assuming you are familiar with the basic concepts of public key cryptography, this will be easy to understand.
You may know that whenever you send a transaction to the Ethereum network, you have to sign this transaction with your private key. Naturally it makes sense to assume that Ethereum nodes have some way to verify a signature.
This functionality of verifying a signature was added to the smart contract itself. With this you can verify much more than just the transaction signature itself. In fact you can pass any data to a smart contract, hash it and then verify its signature against the data. The signature in the code above is the combination of v, r and s.
Why would I need this?
We've actually discussed several examples of how one would use this before. Those included
Essentially you can verify signed data which doesn't have to come from the transaction signer.
Which signing standards should I use?
First we need to decide on the type of signature. Yes that's right. While only for the ecrecover part this doesn't matter, for the parts around it there have been several standards that can be used by a client to sign data using an Ethereum key:
- eth_sign
- personal_sign
- EIP-712
eth_sign is for signing arbitrary data. This makes it the most powerful, the simplest (just sign data), but also the most dangerous. The big problem here is that you could get users to sign data which is actually a transaction. Imagine you have users login to your service, but you make them sign data which is actually a transaction that is 'Send 5 ETH form user to attacker'. A transaction just consists of bytes after all and people are likely to not check what this string of characters they are signing actually means. What seems like a harmless sign in just became an attack to steal funds. So generally usage of eth_sign directly is discouraged.
personal_sign was later added to solve this issue. The method prefixed any signed data with "\x19Ethereum Signed Message:\n" which meant if one was to sign transaction data, the added prefix string would make it an invalid transaction.
For more complex use cases, in particular when used in a smart contract, the EIP-712 standard was created. The standard changed over time, but the currently last version which is supported by MetaMask is signTypedData_v4. Or you could use a specific library like eip-712. The main problem EIP-712 solves is to ensure users know exactly what they are signing, for which contract address and network, and that each signature can only ever be used once at maximum. In short, this is done by signing hashes of all required configuration data (address, chain id, version, data types) + the actual data itself. ERC20-Permit is a great example on how to use it.
All functions can be used when interacting with MetaMask, see examples here. Alternatively they are available in the eth-sig-util library.
So back to the question 'Which signing standards should I use?'. From a contract's perspective use the latest EIP-712 standard! eth_sign is not safe and personal_sign is mostly useful for implementing user sign in features. In your contracts stick to EIP-712.
How to implement EIP-712
Now let's see how to implement EIP-712 in Solidity. The rough idea is
- compute a domain hash which captures the configuration data of contract address and chainId
- compute typed data hash
- combine both hashes and use it inside ecrecover
I would personally also recommend adding a nonce and deadline value which prevent replay attacks and ensure execution within a specific time. Those are not part of EIP-712 standard directly, but can easily be added. Below you'll find an example how to do all that and then execute a function with the parameters on the contract itself.
function executeMyFunctionFromSignature(
uint8 v,
bytes32 r,
bytes32 s,
address owner,
uint256 myParam,
uint256 deadline
) external {
bytes32 eip712DomainHash = keccak256(
abi.encode(
keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
),
keccak256(bytes("MyContractName")),
keccak256(bytes("1")),
block.chainid,
address(this)
)
);
bytes32 hashStruct = keccak256(
abi.encode(
keccak256("MyFunction(address owner,uint256 myParam,uint256 nonce,uint256 deadline)"),
owner,
myParam,
nonces[owner],
deadline
)
);
bytes32 hash = keccak256(abi.encodePacked("\x19\x01", eip712DomainHash, hashStruct));
address signer = ecrecover(hash, v, r, s);
require(signer == owner, "MyFunction: invalid signature");
require(signer != address(0), "ECDSA: invalid signature");
require(block.timestamp < deadline, "MyFunction: signed transaction expired");
nonces[owner]++;
_myFunction(owner, myParam);
}
Security issues with ecrecover + solution
There are a few problems with ecrecover which are not present in above code, but which you should be aware of.
- In some cases ecrecover can return a random address instead of 0 for an invalid signature. This is prevented above by the owner address inside the typed data.
- Signature are malleable, meaning you might be able to create a second also valid signature for the same data. In our case we are not using the signature data itself (which one may do as an id for example).
- An attacker can construct a hash and signature that look valid if the hash is not computed within the contract itself.
In practice I would recommend once again to use the Openzeppelin contracts. Their ECDSA implementation solves all three problems and they have an EIP-712 implementation (still a draft but usable in my opinion). Not only is this easier to use, but they also have further improvements:
- caching mechanism for eip712DomainHash, so it's only calculated whenever chainId changes (so usually just once)
- additional security checks for the signature as mentioned above
- ability to send signature as string
The code from above would then be reduced to this:
import "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin-contracts/contracts/utils/cryptography/draft-EIP712.sol";
contract MyContract is EIP712 {
function executeMyFunctionFromSignature(
bytes memory signature,
address owner,
uint256 myParam,
uint256 deadline
) external {
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
keccak256("MyFunction(address owner,uint256 myParam,uint256 nonce,uint256 deadline)"),
owner,
myParam,
nonces[owner],
deadline
)));
address signer = ECDSA.recover(digest, signature);
require(signer == owner, "MyFunction: invalid signature");
require(signer != address(0), "ECDSA: invalid signature");
require(block.timestamp < deadline, "MyFunction: signed transaction expired");
nonces[owner]++;
_myFunction(owner, myParam);
}
}
That's it. Again this is the currently latest v4 standard of EIP-712. If you come across EIP-712 implementations in other contracts, be aware of which version is used.
Also as a last note, debugging invalid signatures can be very painful as any small difference in any value will lead to an invalid signature, but you don't know which of the data might be wrong. So make sure to double check all your input if you ever run into invalid signatures.
Another interesting standard is EIP-1271. Since smart contracts in Ethereum don't have a private key behind them, they cannot create those v, r, s signatures. But with this standard it's still possible to have signatures created by a contract itself, see the bottom of my previous post here.
Solidity Developer