Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/test/utils/ECDSA.t.sol
Original file line number Diff line number Diff line change
@@ -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
}
}
43 changes: 43 additions & 0 deletions src/utils/ECDSA.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}