Trying to create and recover a signer for a solidity contract

Hello,

I am trying to create a signature and send it to a smart contract but can't seem to get it working.

This is how the signature is created on the backend.

        const ganacheUserWallet = "0xF4F9E9eE0F4cB3EB3445FC3074A5b45e86a7D862";
        const ganachePrivateKey = "0xa90a794905a9e223471e2818ccc5b66df413f99534463a52e82e5595d49ef17a";
        let wallet = new ethers.Wallet(ganachePrivateKey);
        let to = ganacheUserWallet;
        let amount = ethers.utils.parseUnits("100", "wei");

        let nonce = Date.now();
        let contractAddress = "0x6C862c458e77028e85274dcaD6cB30E11b939081";

        let message = ethers.utils.solidityKeccak256(
            ["address", "uint256", "uint256"],
            [to, ethers.utils.hexlify(amount), nonce, contractAddress]
        );

        let prefix = "\x19Ethereum Signed Message:\n32";
        
        let prefixedMessage = ethers.utils.solidityKeccak256(
            ["string", "bytes32"],
            [prefix, message]
        );

        // Sign the message
        let signature = await wallet.signMessage(ethers.utils.arrayify(prefixedMessage));
        return res.status(200).send({ signature, nonce, to, amount });

The client/frontend receives the response and calls a contract function

if (signatureRequest.status === 200) {
        const { signature, nonce, to, amount } = signatureRequest.data;

        console.log(
          signature,
          nonce,
          to,
          amount,
          "signature, nonce, to, amount"
        );

        const logResult = await contract.recoverSignerAndLog(
          to,
          ethers.BigNumber.from(amount),
          nonce,
          signature
        );
        console.log(logResult, "logResult");

      } else {
        setTxError("Server responded with status: " + signatureRequest.status);
      }

And this is the contract. I tried using ECDSA.recover(message, sig) to recover the address since the other functions didn't work. The ECDSA is not working either. Any ideas of examples of a working contract with signature decoding would be greatly appreciated.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract UrbsToken {
    address owner = msg.sender;
    event LogData(address recoveredAddress, address owner);
    event LogString(string message);
    event LogMessage(bytes32 message);

    mapping(uint256 => bool) usedNonces;

    constructor() payable {}

    function recoverSignerAndLog(
        address to,
        uint256 amount,
        uint256 nonce,
        bytes memory sig
    ) public {
        bytes32 message = prefixed(
            keccak256(abi.encodePacked(to, amount, nonce, this))
        );

        emit LogMessage(message);

        address recoveredAddress = recoverSigner(message, sig);

        emit LogData(recoveredAddress, owner);
    }

    // function splitSignature(
    //     bytes memory sig
    // ) internal pure returns (uint8 v, bytes32 r, bytes32 s) {
    //     require(sig.length == 65, "Invalid signature length");

    //     assembly {
    //         r := mload(add(sig, 32))
    //         s := mload(add(sig, 64))
    //         v := byte(0, mload(add(sig, 96)))
    //     }

    //     return (v, r, s);
    // }

    function recoverSigner(
        bytes32 message,
        bytes memory sig
    ) internal pure returns (address) {
        return ECDSA.recover(message, sig);
    }

    // function recoverSigner(
    //     bytes32 message,
    //     bytes memory sig
    // ) internal pure returns (address) {
    //     uint8 v;
    //     bytes32 r;
    //     bytes32 s;

    //     (v, r, s) = splitSignature(sig);

    //     return ecrecover(message, v, r, s);
    // }

    function prefixed(bytes32 hash) internal pure returns (bytes32) {
        return
            keccak256(
                abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
            );
    }
}

:1234: Code to reproduce


:computer: Environment

I am running a blockchain locally using Ganache and Truffle.

1 Like

You should use abi.encode instead of abi.encodePacked.

Also, instead of implementing your own signature prefix in Solidity, consider using OpenZeppelin Contracts' ECDSA.toEthSignedMessageHash.

Thank you ... but it still doesn't work. I really don't understand what I'm doing wrong. Checked the private key, contract address, ABI ... everything is set up correct.

Tried creating the signature with ethers and web3 ... but still nothing.

const signWithWeb3 = async (to, amount, nonce, contractAddress, ganachePrivateKey) => {
    amount = web3.utils.toWei(amount, "wei");

    const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:7545"));

    console.log("to: ", to);
    console.log("amount: ", amount.toString());
    console.log("nonce: ", nonce.toString());
    console.log("contractAddress: ", contractAddress);

    let message = web3.utils.soliditySha3(
        { type: 'address', value: to },
        { type: 'uint256', value: amount.toString() },
        { type: 'uint256', value: nonce.toString() },
        { type: 'address', value: contractAddress }
    );

    let prefix = "\x19Ethereum Signed Message:\n32";

    console.log(message, "message");

    let prefixedMessage = web3.utils.soliditySha3(
        { type: 'string', value: prefix },
        { type: 'string', value: message }
    );

    // Sign the message
    let signature = web3.eth.accounts.sign(prefixedMessage, '0x' + ganachePrivateKey).signature;
    return { signature, amount };
};
const signWithEthers = async (to, amount, nonce, contractAddress, ganachePrivateKey) => {
    amount = ethers.utils.parseUnits(amount, "wei");

    let wallet = new ethers.Wallet(ganachePrivateKey);

    let message = ethers.utils.solidityKeccak256(
        ["address", "uint256", "uint256", "address"],
        [to, ethers.utils.hexlify(amount), nonce, contractAddress]
    );

    let prefix = "\x19Ethereum Signed Message:\n32";

    let prefixedMessage = ethers.utils.solidityKeccak256(
        ["string", "bytes32"],
        [prefix, message]
    );

    // Sign the message
    let signature = await wallet.signMessage(ethers.utils.arrayify(prefixedMessage));

    return { signature, amount };
};

Tried

Do you know of any working examples that I might see?

In general you don't need to include the "Ethereum Signed Message" prefix manually. That is only necessary when you are signing with the private key directly at a lower level.

Both Ether's wallet.signMessage and web3.eth.accounts.sign add the prefix automatically.

You should pass a string, and it will be hashed and prefixed then signed.

You can find an example using Web3.js in the OpenZeppelin Contracts test suite:

try this

const ganacheUserWallet = "0xF4F9E9eE0F4cB3EB3445FC3074A5b45e86a7D862";
    const ganachePrivateKey = "0xa90a794905a9e223471e2818ccc5b66df413f99534463a52e82e5595d49ef17a";
    let wallet = new ethers.Wallet(ganachePrivateKey);
    let to = ganacheUserWallet;
    let amount = ethers.utils.parseUnits("100", "wei");

    let nonce = Date.now();
    let contractAddress = "0x6C862c458e77028e85274dcaD6cB30E11b939081";

    let messageString = ethers.utils.solidityKeccak256(
        ["address", "uint256", "uint256", "address"],
        [to, ethers.utils.hexlify(amount), nonce, contractAddress]
    );
    const messageBytes = Buffer.from(messageString.slice(2), 'hex');

    // Sign the message
    let signature = await wallet.signMessage(messageBytes);
    return res.status(200).send({ signature, nonce, to, amount });

the signMessage function already prefixes the message with ethereum signed ... and it treats javascripts strings differetly than javascritp bytes

if you sent a hash as a string it will interpret as a 64 character string, 66 with the 0x. Now if you send a hash as bytes it will treat as 32 bytes

Hi. Thank you for replying ... Got so many different functions... This is what I used but still not working.

Server:

const ganacheUserWallet = "0xF4F9E9eE0F4cB3EB3445FC3074A5b45e86a7D862";
        const ganachePrivateKey = "0xa90a794905a9e223471e2818ccc5b66df413f99534463a52e82e5595d49ef17a";
        let wallet = new ethers.Wallet(ganachePrivateKey);
        let to = ganacheUserWallet;
        let amount = ethers.utils.parseUnits("100", "wei");

        let nonce = Date.now();
        let contractAddress = "0x6C862c458e77028e85274dcaD6cB30E11b939081";

        let messageString = ethers.utils.solidityKeccak256(
            ["address", "uint256", "uint256", "address"],
            [to, ethers.utils.hexlify(amount), nonce, contractAddress]
        );
        const messageBytes = Buffer.from(messageString.slice(2), 'hex');

        // Sign the message
        let signature = await wallet.signMessage(messageBytes);
        return res.status(200).send({ signature, nonce, to, amount });

Client:

if (signatureRequest.status === 200) {
        const { signature, nonce, to, amount } = signatureRequest.data;

        console.log("Signature:", signature);
//Signature: 0x3ed4dfc1f09721efc44d68675b8b03cbd6b67bef32c54bcf7721a76d9c6887873927e45a5652ac3366a3df40eaa30dc28076f6a44eb9804f496ff0ce9356299e1b

        console.log("Nonce:", nonce);
//Nonce: 1689278891740

        console.log("Recipient's address:", to);
//Recipient's address: 0xF4F9E9eE0F4cB3EB3445FC3074A5b45e86a7D862

        console.log("Amount (wei):", amount);
//Amount (wei): {type: 'BigNumber', hex: '0x64'}

        const logResult = await contract.recoverSignerAndLogWithPrefix(
          to,
          ethers.BigNumber.from(amount).toString(),
          nonce,
          signature
        );

        console.log(logResult, "logResult");
      }

If the amount returned from the server is a BigNumber already, should I still do ethers.BigNumber.from(amount).toString(), when calling the contract function?

        console.log("Amount (wei):", amount);
//Amount (wei): {type: 'BigNumber', hex: '0x64'}

Contract functions:

I have a function that uses ECDSA.toEthSignedMessageHash to add the prefix

    function recoverSignerAndLogWithPrefix(
        address to,
        uint256 amount,
        uint256 nonce,
        bytes memory sig
    ) public {

        address cAddress = address(this);

        emit LogThisAddress(cAddress);

        bytes32 message = keccak256(
            abi.encodePacked(to, amount, nonce, address(this))
        );

        // Add the standard Ethereum signature prefix using OpenZeppelin's function
        bytes32 prefixedMessage = ECDSA.toEthSignedMessageHash(message);

        address recoveredAddress = recoverSigner(prefixedMessage, sig);

        emit LogData(recoveredAddress, owner);
    }

Another that adds the prefix using a function - prefixed()

    function recoverSignerAndLog(
        address to,
        uint256 amount,
        uint256 nonce,
        bytes memory sig
    ) public {
        address cAddress = address(this);

        emit LogThisAddress(cAddress);

        bytes32 nonPrefixedMessage = keccak256(
            abi.encode(to, amount, nonce, cAddress)
        );

        emit LogMessage(nonPrefixedMessage);
        bytes32 message = prefixed(nonPrefixedMessage);
        emit LogMessagePrefix(message);

        address recoveredAddress = recoverSigner(message, sig);

        emit LogData(recoveredAddress, owner);
    }

This is how the prefix is added

    /// builds a prefixed hash to mimic the behavior of eth_sign.
    function prefixed(bytes32 hash) internal returns (bytes32) {
        emit LogPrefixHash(hash);
        emit LogHashWithPrefix(
            keccak256(abi.encode("\x19Ethereum Signed Message:\n32", hash))
        );
        return keccak256(abi.encode("\x19Ethereum Signed Message:\n32", hash));
    }

They both call

    function recoverSigner(
        bytes32 message,
        bytes memory sig
    ) internal returns (address) {
        emit LogSignature(sig);
        return ECDSA.recover(message, sig);
    }

Hope all this makes sense. Tried both of them but the logged owner and recovered address are still different

Recovered address: 0x54696978495Ec1EC36ae5F47a5f31de5985AAAdb
Owner: 0xDd7e43b181EBa89279C1D4c0769690325F63632E

Ethersjs use a Numberish type, so it will try to parse a a bignumber if you send a number, string, bitgnumber, etc.. In this case the number itself is 100 so you could work with 100 directly it would work

in the function function recoverSignerAndLogWithPrefix the contract uses it's own address, have you double checked the address you are using to sign the transaction?

That was it ... I copy pasted the code you provided with a different address. Now it seems to work.

Thank you!

1 Like