6

I have the following message, domain and types:

const domain = {
        // Defining the chain aka Goerli testnet or Ethereum Main Net
        chainId: NETWORK_ID,
        // User friendly name to the specific contract you are signing for.
        name: 'Commit',
        // If name isn't enough add verifying contract to make sure you are establishing contracts with the proper entity
        verifyingContract: verifyingContract,
        // Just let's you know the latest version. Definitely make sure the field name is correct.
        version: '1',
      }
      const types = {

        // Refer to PrimaryType
        Fragmentation: [
            { name: 'Commit', type: 'Commit' },
            { name: 'Consideration', type: 'Consideration'}
        ],
        Commit: [
          { name: 'Commiter', type: 'address' },
          { name: 'Type', type: 'string' },
          { name: 'Contract', type: 'address' },
          { name: 'Amount', type: 'string'}
        ],
        // Not an EIP712Domain definition
        Consideration: [
          { name: 'ContractAddress', type: 'address' },
          { name: 'TokenId', type: 'string' },
          { name: 'Recipient', type: 'address'}
        ],
      }
   const message: any = {
        domain: domain,
    
        // Defining the message signing data content.
        message: {
          Commit: {
            Commiter: taker,
            Type: 
              COMMIT_TYPE,
            Contract: WETH_CONTRACT_TESTNET, 
            Amount: commitedAmount
          },

          Consideration: 
            {
              ContractAddress: collection,
              TokenId: tokenId,
              Recipient: taker
            },
        },
       
        primaryType: 'Func',
        types: types
    }

I then sign this message using let sig = await signer2._signTypedData(domain, types, message.message) And for the given signer: 0x192Ae5441D992f5351F4da1033BEac0d60EA2F4f and signature 0x3216e290e972a7b3db0639b19fc7c8a894ee31bfaee273071d4f960e87be702e030cfe4cb478bea9b458035b7f45b6da784bd51cba66529f5f211fc193b8937f1c I cant manage to verify it on chain. My process is that I hash the message using

const hashedMessage = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(message.message))

I then get 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 I then verify it on remix with those solidity functions:

 function getETHSignedMessage(bytes32 _messageHash) public pure returns (bytes32) {
    return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _messageHash));
  }

function recover(bytes32 _ethSignedMessage, bytes memory _sig) public pure returns (address) {
  (bytes32 r, bytes32 s, uint8 v ) = _split(_sig);
  return ecrecover(_ethSignedMessage, v, r, s);
}

But cant manage to recover the signer address, it keeps getting me other addresses and I have no idea why

1
  • Are you using eth_signTypedData_v3 or eth_signTypedData_v4? What library are you using to sign? Apr 1, 2023 at 2:44

4 Answers 4

3
+50

Signing the typed data offchain and verifying it onchain requires great amount of efforts. It is complex in its nature.The process is, any thing you define offchian you need to define a struct for it onchain, and define its type onchain as well. make sure everything on both side remains same, for example if you define type of variable as string in offchain code, so define the type as string onchain as well. A working example:

        const domain = [
            { name: "name", type: "string" },
            { name: "version", type: "string" },
            { name: "chainId", type: "uint256" },
            { name: "verifyingContract", type: "address" }
        ];
        const bid = [
            { name: "amount", type: "uint256" },
            { name: "bidder", type: "address" },
        ];

onchain code for this should be a struct and define the type of domain and bid


    struct Bid {
        uint256 amount;
        address bidder;
    }

    string private constant EIP712_DOMAIN = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
    string private constant BID_TYPE = "Bid(uint256 amount,address bidder)";

After this hash these defined type using keccack256, but remember to encode them first

   bytes32 constant private EIP712_DOMAIN_TYPEHASH = keccak256(abi.encodePacked(EIP712_DOMAIN));
    bytes32 constant private BID_TYPEHASH = keccak256(abi.encodePacked(BID_TYPE));

After this get the domain separator, its the value that prevents replay attacks

    bytes32 private DOMAIN_SEPARATOR = keccak256(abi.encode(
        EIP712_DOMAIN_TYPEHASH,
        keccak256("your app name"),
        keccak256("version"),
        chainId, // replace this with your chain id
        address(this)
    ));

now you need to hash the message that you will be passing in verify function as tupple, and use ecrecover to recover the signer

    function hashBid(Bid memory bid) private view returns (bytes32) {
        return keccak256(abi.encodePacked(
            "\x19\x01",
            DOMAIN_SEPARATOR,
            keccak256(abi.encode(
                BID_TYPEHASH,
                bid.amount,
                bid.bidder
            ))
          // In your case you will be hashing each of your type inside the keccak256 of return statement separately,
        ));
    }

    function verify(address signer, Bid memory bid, bytes32 sigR, bytes32 sigS, uint8 sigV) public view returns (bool) {
        return signer == ecrecover(hashBid(bid), sigV, sigR, sigS);
    }

offchain code for calling the verify function and sending message, r,s,v to contract

    var message = {
            amount: 100,
            bidder: "0xF6f8196eEE9B04a3cF5711C32A21A8fd625E5dAA"
        };

        const data = JSON.stringify({
            types: {
                EIP712Domain: domain,
                Bid: bid,
            },
            domain: domainData,
            primaryType: "Bid",
            message: message
        });
       //sign the message
    let r,s,v;
    const sign = await window.ethereum.request({
         method: 'eth_signTypedData_v4',
         params: [signer, data],
    });
    console.log("Sign",sign);
    const signature = sign.substring(2);
    r = "0x" + signature.substring(0, 64);
    s = "0x" + signature.substring(64, 128);
    v = parseInt(signature.substring(128, 130), 16);
    // call the contract function
    const res = await contract.methods.verify(message,r, s, v).call();
    

In your case you have more complex typed data, so you need to construct your data accordingly. this is only the demonstration to go for it.

1
  • Been stuck on same issue for days, thanks it worked for me Apr 7, 2023 at 10:12
3

If you are trying to use the EIP-712 standard, particularly the SignTypedData V4 defined by MetaMask, verifying that signed data via smart contract is not straightforward.

It's so complex indeed that an ad hoc tool exists (eip712-codegen) to generate most of the solidity code for you, and still, you need to add something by yourself to make the verification work, even though some example exists for that.

I suggest reading carefully the documentation provided by MetaMask, creating your types, and then using the CLI provided by EIP-712 Codegen to generate the smart contract.

In particular, concentrate on Entrypoints. From the documentation:

Entrypoints

The --entryPoints flag generates a signature verification code for the specified types (which must also be included in the input file). These verification methods will be of the form verifySigned${YourType}(Signed${YourType} input) returns (address);. So if you are signing a struct called Bid it will generate a method called verifySignedBid(SignedBid input) returns (address);

Returns an address of the account that signed this struct.

The Signed{Type} struct format looks like this:

{ bytes signature; address signer; YourType message; }

For regular EOA signatures, the signer should be set to the zero address (0x0000000000000000000000000000000000000000). If the signer value is set to anything other than the zero address, rather than recover a signature normally, the contract will execute EIP-1271 style signature recovery, which allows contract accounts to perform custom verification logic allowing them to effectively "sign" messages as an EOA does.

1

As I understand probably, you get different signatures, it has two reasons: first of all, you check your input values carefully, any spaces or other characters in the input signer function, or verifier function change the signature. on the other your signer must have the same as the transaction sender, in your code, I don't see which address sent transaction.it must the same as your function caller on the network. for better help please add all of the code(test and smart contract hash and verify function)

-3

Either try looking into the chains CSV reports or what is allowed in the contracts

 const convertedSignature = ethers.utils.splitSignature(signature);


 const recoveredAddress = ethers.utils.recoverAddress(
ethers.utils.hashMessage(message),
convertedSignature,
);

  console.log('Recovered address:', recoveredAddress);

   const messageHash = ethers.utils.hashMessage(message);
 const messageData = ethers.utils.defaultAbiCoder.encode(
 ['bytes4', 'bytes32', 'bytes'],
 [functionSelector, messageHash, signature],
);
6
  • What do you mean by "CSV reports"?
    – Ismael
    Apr 4, 2023 at 20:15
  • CSV exports are a function on the etherscan.io/address to verify addresses and the type of signatures on the blockchain eoa user-controlled accounts identified by a unique public key on the blockchain can be verified by how metamask or wallets handle the signature to the chain. command-line interface provide various function similar to active REPL connected when the contract is deployed Apr 5, 2023 at 4:08
  • REPL to debug their smart contract, and EIP-712 used by the smart contract, in the development process compared to EIP-1085 or EIP-999-EIP999.5. Apr 5, 2023 at 4:17
  • How are the CSV exported data related to a signature verification? You mention EIP-1085, but there's no such EIP. EIP-999 isn't related to signature at all. Please, stop writing answers that make no sense and are unrelated to the question asked.
    – Ismael
    Apr 5, 2023 at 4:53

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.