Solidity and Ethers.js – Solving Verification Issues with SignTypedData in Ethers.js

ecdsametamasknftsolidity

i am using _signTypedData in etherjs to sign and encode a signature out of the data as follows

 const domain = {
    name: "og-nft",
    version: "1",
  };
  const types = {
    Nft: [
      { name: "URI", type: "string" },
      { name: "price", type: "uint256" },
    ],
  };

  // The data to sign
  const [voucher, setVoucher] = useState({
    URI: "",
    price: '1',
  });
const signature = await signer._signTypedData(domain, types, voucher);

reference

I am storing the voucher and signature in the mongo database, I have deployed smart contract on hardhat and I am verifying the authenticity of signature by peering out the signer of the voucher using ECDSA.recover

function verifyVoucher(NFTVoucher calldata voucher, bytes memory signature)
        public
        view
        returns (address)
    {
        require(voucher.price > 0, "Price must be greater than 0");
      //  require(voucher.tokenId > 0, "Token ID must be greater than 0");
        bytes32 hash = _hash(voucher);
        //string memory hash="";
        return ECDSA.recover(hash, signature);
    }

but the result of this is not matching with actual signer. i think I am making some mistake in the hash function above used.

0xe8c795f9168269940b31a470ad82e89a453e88b9 signer
0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 owner

below is the hash function.

function _hash(NFTVoucher calldata voucher)
        internal
        view
        returns (bytes32)
    {
        return
            _hashTypedDataV4(
                keccak256(
                    abi.encode(
                        keccak256(
                            "Nft(string URI,uint256 price)"
                        ),
                        keccak256(bytes(voucher.URI)),
                        voucher.price
                    )
                )
            );
    }

reference

Best Answer

You are missing 2 fields in your domain separator :

  • chainId
  • verifyingContract

While they are not mandatory as per EIP-712, the draft-EIP712.sol that you seem to be using, is relying on them under the hood when computing the domain separator hash :

function _buildDomainSeparator(bytes32 typeHash, bytes32 nameHash, bytes32 versionHash) private view returns (bytes32) {
    return keccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid, address(this)));
    //                                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
}

So to match draft-EIP712.sol computation you should include them in your domain separator on the client side to generate / sign the exact same data that your contract will verify :

 const domain = {
    name: "og-nft",
    version: "1",
    chainId: chainId,
    verifyingContract: contract.address,
  };

where chainId is : const { chainId, _ } = await provider.getNetwork(); and contract.address is simply the address of your deployed contract.

I hope that answers your question.

Related Topic