Skip to content

Normalize ECDSA signature v parameter for Solidity ecrecover compatibility#4107

Merged
squadgazzz merged 8 commits intomainfrom
signature-normalization-fix
Feb 2, 2026
Merged

Normalize ECDSA signature v parameter for Solidity ecrecover compatibility#4107
squadgazzz merged 8 commits intomainfrom
signature-normalization-fix

Conversation

@squadgazzz
Copy link
Copy Markdown
Contributor

@squadgazzz squadgazzz commented Jan 29, 2026

Description

This PR fixes an issue where EIP-712 signatures with v = 0 or 1 (modern EIP-2 format) pass off-chain validation but fail on-chain settlement with GPv2: invalid signature.

Problem

Some wallets (e.g., Bitget Wallet) produce ECDSA signatures using the modern EIP-2 format, where v ∈ {0, 1}, while Solidity's ecrecover precompile expects the legacy format where v ∈ {27, 28}.

Off-chain (Alloy library): The https://github.com/alloy-rs/core/blob/main/crates/primitives/src/signature/sig.rs internally normalizes v to a boolean parity before recovery, so signatures with v = 0 or 1 recover correctly.

On-chain (Solidity): The ecrecover precompile https://coders-errand.com/ecrecover-signature-verification-ethereum/. When given v = 0, it returns address(0), which triggers the
https://github.com/cowprotocol/contracts/blob/main/src/contracts/mixins/GPv2Signing.sol#L207-L208:

signer = ecrecover(message, v, r, s);
require(signer != address(0), "GPv2: invalid ecdsa signature");

This mismatch causes orders to pass orderbook validation but fail at settlement.

Solution

Normalize v to the legacy format (27/28) at signature parsing time in EcdsaSignature::from_bytes():

let normalized_v = match v {
    0 | 27 => 27,
    1 | 28 => 28,
    _ => anyhow::bail!("invalid signature v value: {v}, expected 0, 1, 27, or 28"),
};

This ensures:

  1. Signatures are stored with normalized v values
  2. Both off-chain validation and on-chain ecrecover receive compatible parameters
  3. The fix applies to all entry points (Signature::from_bytes, JSON deserialization)

Reproducing the Issue

The issue can be verified using a real failed order and Foundry's cast tool to call the ecrecover precompile directly:

Failed order:

  • Order UID: 0xb8e19962dd762067afb9f169684abfcbf2cb13bdc7a62ae2e680ebd5ce18c9bcca0c9c4a650cc4ed406d4a6dd031cdd9d4ebf0dc697a0686
  • Order hash (struct hash): 0xb8e19962dd762067afb9f169684abfcbf2cb13bdc7a62ae2e680ebd5ce18c9bc
  • Expected signer (owner): 0xca0c9c4a650cc4ed406d4a6dd031cdd9d4ebf0dc
  • Signature: 0xAB2E74AA0D67233ADC7B52C3B832357ED35F2052338D820D4DA66210EFA7A9684601726CB76BD26DDD958EFE291CFB57E02C39B3F60FBB8BBED1E891FB14CB5D00
    • r: 0xAB2E74AA0D67233ADC7B52C3B832357ED35F2052338D820D4DA66210EFA7A968
    • s: 0x4601726CB76BD26DDD958EFE291CFB57E02C39B3F60FBB8BBED1E891FB14CB5D
    • v: 0x00 ← the problem

Step 1: Compute the EIP-712 message hash

To avoid computing it manually, I grabbed it from a Tenderly simulation[URL].

MESSAGE_HASH="0xb8e19962dd762067afb9f169684abfcbf2cb13bdc7a62ae2e680ebd5ce18c9bc"

Step 2: Test ecrecover with v=0 (returns zero address - FAILS on-chain)

cast call 0x0000000000000000000000000000000000000001 \
"${MESSAGE_HASH}0000000000000000000000000000000000000000000000000000000000000000AB2E74AA0D67233ADC7B52C3B832357ED35F2052338D820D4DA66210EFA7A9684601726CB76BD26DDD958EFE291CFB57E02C39B3F60FBB8BBED1E891FB14CB5D" \
--rpc-url https://eth.llamarpc.com

Returns: 0x0000000000000000000000000000000000000000000000000000000000000000

Step 3: Test ecrecover with v=27 (returns correct signer - WORKS)

cast call 0x0000000000000000000000000000000000000001 \
"${MESSAGE_HASH}000000000000000000000000000000000000000000000000000000000000001bAB2E74AA0D67233ADC7B52C3B832357ED35F2052338D820D4DA66210EFA7A9684601726CB76BD26DDD958EFE291CFB57E02C39B3F60FBB8BBED1E891FB14CB5D" \
--rpc-url https://eth.llamarpc.com

Returns: 0x000000000000000000000000ca0c9c4a650cc4ed406d4a6dd031cdd9d4ebf0dc

@squadgazzz squadgazzz marked this pull request as ready for review January 29, 2026 21:02
@squadgazzz squadgazzz requested a review from a team as a code owner January 29, 2026 21:02
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly normalizes the v parameter of ECDSA signatures to ensure compatibility with Solidity's ecrecover. The changes are well-contained, and the added tests thoroughly verify the new normalization logic for various scenarios. I have one suggestion to improve the robustness of the EcdsaSignature type by enforcing the normalization invariant at the type level, and another to restore test coverage that was lost during refactoring.

Comment thread crates/model/src/signature.rs
Comment thread crates/model/src/order.rs
@squadgazzz squadgazzz marked this pull request as draft January 29, 2026 21:07
@squadgazzz squadgazzz marked this pull request as ready for review January 30, 2026 08:17
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request effectively addresses the v parameter incompatibility between modern EIP-2 signatures and Solidity's ecrecover precompile by normalizing v values (0 to 27, 1 to 28) during signature parsing. The implementation is robust, including proper error handling for invalid v values and updating the Default implementation for EcdsaSignature. The changes are thoroughly covered by comprehensive new and updated test cases, ensuring correctness and reliability. No critical issues found.

Copy link
Copy Markdown
Contributor

@jmg-duarte jmg-duarte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super minor nits. Amazing work with the investigation 🕵️🔍

Comment thread crates/model/src/order.rs Outdated
Comment thread crates/model/src/signature.rs Outdated
Comment thread crates/model/src/signature.rs Outdated
Comment thread crates/model/src/signature.rs Outdated
Comment thread crates/model/src/signature.rs
Copy link
Copy Markdown
Contributor

@m-sz m-sz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! Looks good to me and the comments describe the relevant section well for posterity.

@squadgazzz squadgazzz added the hotfix Labels PRs that should be applied into production right away label Jan 30, 2026
Copy link
Copy Markdown
Contributor

@MartinquaXD MartinquaXD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice PR!

  • good investigation
  • great description
  • good test coverage

Just minor nits.

Comment thread crates/driver/src/domain/competition/order/signature.rs Outdated
Comment thread crates/model/src/order.rs Outdated
@squadgazzz squadgazzz enabled auto-merge February 2, 2026 11:07
@squadgazzz squadgazzz added this pull request to the merge queue Feb 2, 2026
Merged via the queue into main with commit 00ae19d Feb 2, 2026
19 checks passed
@squadgazzz squadgazzz deleted the signature-normalization-fix branch February 2, 2026 11:32
@github-actions github-actions bot locked and limited conversation to collaborators Feb 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

hotfix Labels PRs that should be applied into production right away

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants