---
description: Unsafe use of Solidity's ecrecover builtin
---

## 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.

When the recovered address is stored in a local variable, the diagnostic is deferred until its
first non-copy observable use. Local-to-local copies keep the diagnostic deferred, and a guard is
needed only on paths that later use the signer. A dominating low-`s` guard after the call can
therefore validate the exact `s` value before the signer affects behavior. The warning remains if
any path reaches an observable use or state store before a matching guard, if `s` is reassigned
before that guard, or if an unguarded named return exposes the result.

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. It 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);
}
```
