Solidity MetaMask – Fixing Unicode Question Marks Issue When Signing with Viem

ecrecovermetamasksignaturesolidityviem

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.

Best Answer

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.