Back to Foundry

Unsafe ecrecover

crates/lint/docs/ecrecover.md

1.8.12.2 KB
Original Source

Unsafe ecrecover

Severity: Med ID: ecrecover

Flags direct calls to Solidity's ecrecover builtin when the signature's s value is not proven to be in the canonical lower half of the secp256k1 curve order.

What it does

The lint follows simple local aliases and recognizes low-s bounds established by require, assert, conditionals, and early-return or revert branches. It accepts the canonical maximum 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, stricter bounds, reversed comparisons, and the equivalent strict comparison against that value plus one. Facts are invalidated by reassignment, loop-carried writes, and calls that may change mutable state. They are retained only when they hold on every path reaching the call.

Checks on v, the recovered address, nonces, domain separators, or the top bit of an EIP-2098 signature do not prove that s is canonical and do not suppress the warning.

The analysis is intentionally local. A low-s guard may be established before the call or after assigning the recovered address to a local, provided it dominates the address's first observable use. The analysis does not summarize internal helpers or modifiers, recognize bound signature schemes that fix one recovery ID, or inspect Yul and low-level calls to precompile address 0x01.

Why is this bad?

For each valid high-s ECDSA signature, an attacker can derive a second signature for the same message and signer by replacing s with its complement in the curve order and flipping v. Contracts that use the signature bytes as a unique identifier can therefore have replay or double-use protections bypassed. The ecrecover precompile does not enforce the low-s rule that Ethereum transactions enforce.

Example

Bad

solidity
function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) pure returns (address) {
    return ecrecover(hash, v, r, s);
}

Good

solidity
uint256 constant HALF_ORDER =
    0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0;

function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) pure returns (address) {
    require(uint256(s) <= HALF_ORDER, "invalid signature s");
    return ecrecover(hash, v, r, s);
}