The previous article introduced the usage of ecrecover, but the built-in ecrecover has a signature malleability problem. This article will explain this problem and how to solve it.

Problem

Examples from the previous article:

1
2
3
4
5
6
7
function test() external view returns(address) {
bytes32 hash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53;
uint8 v = 0x1b;
bytes32 r = 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a3;
bytes32 s = 0x56320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f8;
return ecrecover(hash , v, r, s);
}

The signer’s wallet address is 0xDD980c315dFA75682F04381E98ea38BD2A151540. We made the following changes to the signature:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test() external view returns(address) {
bytes32 hash = 0xc1af4b94166cd32fc49b7b926cbb91ee421de2d04450e8ae57857b9b56ac7e53;
uint8 v = 0x1b;
bytes32 r = 0xe1077fb9321c187d8a43926896abac5455ce6add269e098f855ff059d6b846a3;
bytes32 s = 0x56320be5f6d79c4d0e5583d6b9a2e50fae78f1fb5ff0553541e69c66dae2b2f8;

// tamper with
// v: 0x1c
// s: 0xa9cdf41a092863b2f1aa7c29465d1aef0c35eaeb4f584b067debc225f5538e49
v = v == 0x1b ? 0x1c : 0x1b;
s = bytes32(uint(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141) - uint(s));

return ecrecover(hash , v, r, s);
}

You will find that it return the same signer, and the signature may be reused. For example, modify the example of the previous article and use s as the test to see whether the signature has been used:

1
2
3
4
5
6
7
8
9
10
11
12
contract EcrecoverTest {
mapping(uint => bool) usedSignatures;

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, "signature expired");
require(!usedSignatures[s], "used signature");
bytes32 messageHash = abi.encodePacked(block.chainid, address(this), spender, value, deadline);
bytes32 hash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash));
require(owner == ecrecover(hash , v, r, s), "invalid signature");
usedSignatures[s] = true;
}
}

The above program will cause a signature to be used twice.

Root Cause

The ECDSA graph of the elliptic curve function used by Ethereum looks like this
ECDSA

We can simply understand that another solution can be found in the mirror place, so it can be easily achieved by modifying some data.

Solution

Verify Signature Range

We can verify the numerical range of the signature

1
require(uint(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, "invalid s");

OpenZeppelin Library

Or use OpenZeppelin ECDSA

1
2
3
4
5
6
7
8
9
10
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract EcrecoverTest {
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
// ...
// Replace ecrecover
require(owner == ECDSA.recover(hash , v, r, s), "invalid signature");
// ...
}
}

Further Reading

Solidity - ecrecover