ECDSA returning incorrect signer address

I'm trying to play around with ECDSA, but for some reason it's not returning the correct address to me. Here is how I'm trying to return the signing address.

const signer = new ethers.Wallet(PRIVATE_KEY)

    const hashedHello = await contract.rndHash('hello')
    const ethSignedHash = await contract.ethSignedHash(hashedHello);
    const signedMessageHash = await signer.signMessage(hashedHello);
    const recoveredSig = await contract.recover(ethSignedHash, signedMessageHash);

    console.log('should be my public key => ', recoveredSig)

Here is my contract code:

contract Greeter {
    using ECDSA for bytes32;

    string private greeting;
    
    function rndHash(string memory data) public pure returns(bytes32) {
        return keccak256(abi.encodePacked(data));
    }
    
    function ethSignedHash(bytes32 messageHash) public pure returns(bytes32) {
        return messageHash.toEthSignedMessageHash();
    }

    function recover(bytes32 hash, bytes memory signature) public pure returns(address) {
        return hash.recover(signature);
    }
}
1 Like

I'm experiencing the same issue.
I'm generating the signatures like so:

const hashString = ethers.utils.solidityKeccak256(
                ['uint256', 'address', 'string', 'address', 'uint256'],
                [amount, user1.address, OpId, dummyToken.address, 0],
            );
            console.log('hashString:', hashString);

            const hashBytes = ethers.utils.arrayify(hashString);

            console.log('hashBytes:', hashBytes);

            const signatures = [
                await signer1.signMessage(hashString),
                await signer2.signMessage(hashString),
                await signer3.signMessage(hashString),
            ];

And I'm trying to verify the signature like so :

function redeem(
        uint256 amount,
        address spender,
        string memory burnOpId,
        address tokenContract,
        bytes[] calldata signatures
    ) external {
        require(isTokenSupported(tokenContract), 'Token not supported');
        require(isSigner(msg.sender), 'Not an authorized signer');
        require(signatures.length == signers.length, 'Amount of signatures does not match amount of signers');
        bytes32 message = hashParameters(amount, spender, burnOpId, tokenContract, nonce);
        address[] memory uniqueSigners = new address[](signatures.length);
        console.log('message in bytes:');
        console.logBytes32(message);

        for (uint i = 0; i < signatures.length; i++) {
            console.log('Signer', i, ':', signers[i]);
            (address signer, ECDSA.RecoverError err) = ECDSA.tryRecover(message, signatures[i]);
            require(err == ECDSA.RecoverError.NoError, 'Invalid signature');
            console.log('signer recovered:', signer);
            // require(isSigner(signer), 'Invalid signer');
            for (uint j = 0; j < uniqueSigners.length; j++) {
                require(uniqueSigners[j] != signer, 'Duplicate signature');
            }
            uniqueSigners[i] = signer;
        }
        require(uniqueSigners.length == signers.length, 'Insufficient valid signatures');
        ERC20 token = ERC20(tokenContract);
        require(token.transfer(spender, amount), 'Transfer failed');
        nonce++;
        emit Redeemed(spender, tokenContract, burnOpId, amount);
    }

I've tried logging the hash generated by the solidity code and it matches the one generated by my tests but I still recover an address different that the one I used for signing.

I found a fix for my problem by adding the toEthSignedMessageHash method. It seems like the ethers.js sign method adds a prefix to the message. Without the toEthSignedMessageHash, this prefix is interpreted as part of the message, and thus, a wrong signer address is returned.

(address signer, ECDSA.RecoverError err) = ECDSA.tryRecover(
                ECDSA.toEthSignedMessageHash(message),
                signatures[i]
            );