Skip to content

bug: ECDSA cosignature malleability allows unauthorized fillers to bypass exclusive fill rights in V2DutchOrderReactor#365

Open
chef-carb wants to merge 1 commit into
Uniswap:mainfrom
chef-carb:fix/v2-cosignature-malleability
Open

bug: ECDSA cosignature malleability allows unauthorized fillers to bypass exclusive fill rights in V2DutchOrderReactor#365
chef-carb wants to merge 1 commit into
Uniswap:mainfrom
chef-carb:fix/v2-cosignature-malleability

Conversation

@chef-carb
Copy link
Copy Markdown

Summary

V2DutchOrderReactor._validateOrder uses raw ecrecover to verify cosignatures. The secp256k1 curve allows two valid signatures for any message — a canonical low-s form and a malleable high-s form. Because ecrecover accepts both, any party that observes a valid cosignature in the mempool can derive a second mathematically equivalent signature without access to the cosigner's private key.

This breaks the exclusive filler access control mechanism.


Attack

  1. Cosigner issues a cosignature designating fillerA as the exclusive filler
  2. fillerA broadcasts a fill transaction — the cosignature is visible in the mempool
  3. Attacker computes (r, secp256k1_order - s, v ^ 1) — O(1) arithmetic, no secret key needed
  4. Attacker broadcasts a competing fill using the malleable cosignature with higher gas
  5. Attacker's fill is confirmed first; Permit2 nonce is consumed
  6. fillerA's transaction reverts — permanently locked out of the fill

When exclusivityOverrideBps > 0, the attack succeeds outright. The swapper still receives correct output (at the override premium), but the designated filler's exclusive window is stolen.


Vulnerable code

// V2DutchOrderReactor.sol:116-122
(bytes32 r, bytes32 s) = abi.decode(order.cosignature, (bytes32, bytes32));
uint8 v = uint8(order.cosignature[64]);
address signer = ecrecover(keccak256(abi.encodePacked(orderHash, abi.encode(order.cosignerData))), v, r, s);
if (order.cosigner != signer || signer == address(0)) {
    revert InvalidCosignature();
}

Recommended fix

Replace raw ecrecover with OpenZeppelin's ECDSA.recover, which enforces the EIP-2 low-s constraint and reverts with ECDSAInvalidSignatureS on high-s input:

import {ECDSA} from "openzeppelin-contracts/utils/cryptography/ECDSA.sol";

bytes32 msgHash = keccak256(abi.encodePacked(orderHash, abi.encode(order.cosignerData)));
address signer = ECDSA.recover(msgHash, order.cosignature);
if (order.cosigner != signer) {
    revert InvalidCosignature();
}

Additional benefits:

  • Removes the redundant signer == address(0) check — ECDSA.recover handles it internally
  • Validates cosignature length (65 bytes) before byte access — eliminates a secondary out-of-bounds panic on malformed input

POC

Two Foundry tests are included in test/reactors/V2DutchOrderReactorMalleability.t.sol:

  • testPOC_MalleableCosignaturePassesValidation — shows that (r, s', v') derived from any observed valid cosignature passes _validateOrder without the cosigner's private key
  • testPOC_NonWhitelistedFillerFillsDuringExclusiveWindow — full end-to-end attack: a non-whitelisted filler front-runs the designated filler using a malleable cosignature, steals the fill, and permanently locks out the legitimate filler

Both tests pass against the unmodified contract. After applying the fix, both revert with ECDSAInvalidSignatureS.

forge test --match-path test/reactors/V2DutchOrderReactorMalleability.t.sol -v

References


Note: I was unable to submit this through the Cantina bug bounty program due to a bugged KYC process on their platform, so I'm disclosing it directly here instead.

…pass exclusive fill rights

Raw `ecrecover` in `V2DutchOrderReactor._validateOrder` accepts both the canonical
low-s signature and its malleable high-s counterpart. An attacker who observes a
whitelisted filler's pending transaction in the mempool can derive the malleable
cosignature without the cosigner's private key and front-run the fill during the
exclusivity window, permanently consuming the Permit2 nonce and locking out the
legitimate filler.

Both POC tests pass against the unmodified contract, confirming the vulnerability.

Note: unable to submit through Cantina due to a bugged KYC process.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant