1

I'm trying to sign a message in Viem and recover the signer in Solidity, it works as expected, however, the Metamask pop-up window displays the data to be signed incorrectly.

enter image description here

Here is my Viem ts code:

const signature = await walletClient.signMessage({
        message: { raw: "0xe4A98D2bFD66Ce08128FdFFFC9070662E489a28E" as `0x${string}` },
      });
     console.log("signature:", signature);
     // signature: 0x09a180eb5c1ab7914a811f5e216ac3741a1501d001497446c7ced23f3c72fd9f457cbbba457bcd514bb593b99b3ff52ab2f8e17042b600f1a6e54d9f32b33b041c

Here is my Solidity code that recovers the address correctly

First when given the address from above as input:

function getAddressHashedMsg(address input) public pure returns (bytes32 messageHash) {
        messageHash = keccak256(
            abi.encodePacked("\x19Ethereum Signed Message:\n", "20", input)
        );    
    }

returns 0xf5b59420194a3cbe3e9f66c88c83b61ab6979585c19e8a7652a8791d85e705f2

When given the hash from getAddressHashedMsg() and the signature from Viem:

    /**@notice Returns an address for an inputted signed message and signature */
    function recoverSigner(
        bytes32 _ethSignedMessageHash,
        bytes memory _signature
    ) public pure returns (address) {
        (bytes32 r, bytes32 s, uint8 v) = splitSignature(_signature);

        return ecrecover(_ethSignedMessageHash, v, r, s);
    }

This returns 0xe4A98D2bFD66Ce08128FdFFFC9070662E489a28E correctly.

1 Answer 1

2

Under the hood, viem's signMessage works by calling the personal_sign JSON-RPC method on the wallet (in this case, MetaMask). When MetaMask receives a personal_sign request, it will represent the bytes as a UTF-8 string to the end-user. This is because personal_sign is intended to be used with human-readable string messages and not raw byte arrays.

As noted in the MetaMask documentation:

important

  • Don't use this method to display binary data, because the user wouldn't be able to understand what they're agreeing to.

So the questions marks and other characters in the dialog are simply the result of MetaMask interpreting the bytes of the address as a UTF-8 string. You can reproduce the logic using this JavaScript:

const byteString = '0xe4A98D2bFD66Ce08128FdFFFC9070662E489a28E';
const byteArray = Uint8Array.from(byteString.substring(2).match(/.{2}/g), v => parseInt(v, 16));
const utf8 = new TextDecoder().decode(byteArray);
// '䩍+�f�\b\x12����\x07\x06b䉢�'

If you wanted the message 0xe4A98D2bFD66Ce08128FdFFFC9070662E489a28E to appear in the MetaMask dialog, you must instead request a personal_sign on the bytes representing this string. In viem, you can do this by directly passing a string to message, instead of passing it through the raw attribute:

const signature = await walletClient.signMessage({
    message: "0xe4A98D2bFD66Ce08128FdFFFC9070662E489a28E"
});

The corresponding Solidity would also need to be updated to hash a string of the address, and not the address literal.

2
  • So if my solidity function takes an address as input, I would be forced to convert it to a string? Also, if I only want to hash a single address, would it be overkill to use EIP 712?
    – Rohan Nero
    Commented Dec 16, 2023 at 1:16
  • 1
    Yes, you'd need to convert the address to a string representing the address. Note that the len(message) would also need to be updated from "20" to "42" to reflect the number of bytes the string takes. As for EIP-712, I think this would depend on your overall use case/intent; MetaMask's documentation has good explanation on the pros and cons of both approaches.
    – ardislu
    Commented Dec 16, 2023 at 4:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.