From 491de9ccfa82b438a33b3f56676b4182b76002aa Mon Sep 17 00:00:00 2001 From: mukku Date: Mon, 28 Jul 2025 18:29:23 +0000 Subject: [PATCH] feat: Add Yul-based ECDSA signature recovery utility and tests --- src/test/utils/ECDSA.t.sol | 50 ++++++++++++++++++++++++++++++++++++++ src/utils/ECDSA.sol | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/test/utils/ECDSA.t.sol create mode 100644 src/utils/ECDSA.sol diff --git a/src/test/utils/ECDSA.t.sol b/src/test/utils/ECDSA.t.sol new file mode 100644 index 00000000..68f3c3eb --- /dev/null +++ b/src/test/utils/ECDSA.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +import {ECDSA} from "src/utils/ECDSA.sol"; +import {DSTest} from "ds-test/test.sol"; + +interface Vm { + function sign(uint256 privateKey, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); + function addr(uint256 privateKey) external returns (address); +} + +contract ECDSATest is DSTest { + Vm public constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function testRecoverValidSignature() public { + bytes32 message = keccak256("hello solmate"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, message); + address expected = vm.addr(1); + + bytes memory sig = abi.encodePacked(r, s, v); + address recovered = ECDSA.recover(message, sig); + + assertEq(recovered, expected); + } + + function testInvalidSigLength() public { + address recovered = ECDSA.recover(keccak256("msg"), hex"1234"); + assertEq(recovered, address(0)); + } + + function testWrongSignatureReturnsZero() public { + address recovered = ECDSA.recover( + keccak256("hello"), + hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabb" + ); + + emit log_address(recovered); // <-- This will print the address + assertEq(recovered, address(0)); +} + function testMalleableSignature() public { + bytes32 message = keccak256("test"); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(2, message); + + // Modify the signature to make it malleable + bytes memory sig = abi.encodePacked(r, s, v + 1); // Invalid v value + + address recovered = ECDSA.recover(message, sig); + assertEq(recovered, address(0)); // Should return zero for malleable signature + } +} diff --git a/src/utils/ECDSA.sol b/src/utils/ECDSA.sol new file mode 100644 index 00000000..1709321f --- /dev/null +++ b/src/utils/ECDSA.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/// @notice Yul-based gas-optimized ECDSA signature recovery +library ECDSA { + function recover(bytes32 hash, bytes memory signature) internal view returns (address) { + if (signature.length != 65) return address(0); + + assembly { + let ptr := mload(0x40) + + let r := mload(add(signature, 0x20)) + let s := mload(add(signature, 0x40)) + let v := byte(0, mload(add(signature, 0x60))) + + // Reject malleable signatures by ensuring s <= secp256k1n / 2 + if gt(s, div(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141, 2)) { + mstore(ptr, 0) + return(ptr, 0x20) + } + + // Pack for ecrecover + mstore(ptr, r) + mstore(add(ptr, 0x20), s) + mstore(add(ptr, 0x40), hash) + mstore(add(ptr, 0x60), v) + + let success := staticcall(gas(), 0x01, add(ptr, 0x40), 0x80, ptr, 0x20) + + if iszero(success) { + mstore(ptr, 0) + } + + return(ptr, 0x20) + } + } + + function recover(bytes32 hash, bytes32 r, bytes32 s, uint8 v) internal view returns (address) { + if (v < 27) v += 27; + bytes memory signature = abi.encodePacked(r, s, v); + return recover(hash, signature); + } +}