Solidity - ecrecover
Introduction
The ecrecover
function in the smart contract can be used to verify the signature of the wallet. The verifier can confirm the identity of the signer without the signer’s private key. It is often used for off-chain authorization, and then used on-chain by a third party or for simple identity verification.
Function
1 | function ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address) |
- hash: The hash of the signed message.
- v: v value of signature.
- r: r value of signature.
- s: s value of signature.
- Return: The address of the signer.
Example
For example:
1 | contract EcrecoverTest { |
The signer’s wallet address 0xDD980c315dFA75682F04381E98ea38BD2A151540
will be returned, indicating that he signed the message 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53
.
In normal use, parameters are passed in instead of hard-coded, and verify the signer. For example:
1 | function verify(address owner, bytes32 hash, uint8 v, bytes32 r, bytes32 s) external { |
However, passing the hash directly cannot know what content the user authorized, so it will actually be some raw data and hashed on the chain:
1 | function permit(address owner, uint value, uint8 v, bytes32 r, bytes32 s) external { |
EIP-191
EIP-191 defines that a prefix must be added when using a signature.
1 | "\x19Ethereum Signed Message:\n" + Message bytes length + Message content |
uint is 32 bytes, so the above example can be changed to:
1 | bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", value)); |
This is because the original signature method may produce a valid transaction signature.
Replay
This means that the same signature is used repeatedly. If the scope and validity status of the signature are not correctly handled, it may be used as a means of attack. It can be seen from the above example that there is unhandled, so the signature can be reused. Usually, we can add a nonce to determine whether the signature has been used:
1 | contract EcrecoverTest { |
The nonce can also be an incrementing value for each user on the chain. In addition to the nonce, a valid time is usually added. However, even if the nonce and valid time are added, the signature may still be reused. If the same verification method is used in different contracts, the same signature can be reused. At this time, the contract address can be added to limit the scope. After the above restrictions and processing, there is a situation that may replay. There happens to be the same contract address in different chains, although the probability is very low. We can add chain ID and contract address as restrictions to get the following example:
1 | function permit(address owner, address spender, uint value, uint nonce, uint deadline, uint8 v, bytes32 r, bytes32 s) external { |
Hash signed message
Since the prefix requires the input of the message length, it may be a bit troublesome to calculate the dynamic length of the message. Therefore, there is also a way to hash the signed message first:
1 | bytes32 messageHash = abi.encodePacked(block.chainid, address(this), spender, value, nonce, deadline); |
After this, it can be a fixed value 32.
Split Signature
The signature format sometimes is not splitted.
1 | 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a356320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f81b |
vrs can be splitted on-chain in the following way:
1 | function splitSignature(bytes memory signature) internal pure returns (uint8 v, bytes32 r, bytes32 s) { |
However, it is not recommended to split it on-chain. It will cause an increase in gas, so it should be splitted off-chain first.
Cancel Signature
Since once a signature is signed and sent, the holder has the authority to perform relevant operations before it expires. It may cause losses to the signer. For example, it is common to use signatures to place orders and quotes. Suppose an NFT holder signs to sell for 1 ETH, which is valid for one month. Later, the holder cancels the pending order on the web page. If there is no cancellation mechanism on-chain, people who know the signature can still buy it for 1 ETH within a month, even if the market price rises to 10 ETH. Related incidents have occurred in OpenSea.
In order to save costs and improve efficiency when placing orders, using an off-chain signature quotation mechanism actually causes the above problems. To solve this problem, you must use the on-chain signature cancellation mechanism and simply change the above example to invalidate the nonce:
1 | contract EcrecoverTest { |
OpenZeppelin Library
We can also use OpenZeppelin MessageHashUtils to handle prefixes for signed messages. We can automatically add the EIP-191 prefix using the following overload method:
1 | function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest); |
Fixed length hash signature message
1 | import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; |
Dynamic length content
1 | // uint value = ...; |
Further Reading
Solidity - Signature Malleability
Using Web3.js to Sign and Recover
Using Ethers.js to Sign and Recover
Solidity - EIP-712 Typed Structured Data Hashing and Signing