8

I'm signing typed data using _signTypedData() method of ethers. But when I retrieve the signer's address in Solidity Smart Contract using ECDSA.recover(), it returns the wrong address. Let me know if I'm missing anything or taking the wrong approach. I'd really appreciate any help!

Front-End Code

let token = {
    tokenId: 1,
    account: "<< my_wallet_address >>"
};
const provider = new ethers.providers.Web3Provider(window.ethereum);
let signature;
let oContract = new ethers.Contract(CONTRACT_ADDRESS, ABI, provider);
$("#btnSignTransaction").on("click", async function () {
    const signer = provider.getSigner();
    const {
        chainId
    } = await provider.getNetwork();
    signature = await signer._signTypedData({
            name: 'Name',
            version: '1.0.0',
            chainId: chainId,
            verifyingContract: CONTRACT_ADDRESS,
        }, {
            NFT: [{
                    name: 'tokenId',
                    type: 'uint256'
                },
                {
                    name: 'account',
                    type: 'address'
                },
            ],
        },
        token
    );
    token.signature = signature;
});

$("#btnCall").on("click", async function () {
    const addressFromContract = await oContract.getAccountAddress(token.account, token.tokenId, token.signature);
    console.log(addressFromContract);
});

Solidity Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 

contract ERC721LazyMintWith712 is ERC721, EIP712, AccessControl {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor(string memory name, string memory symbol)
        ERC721(name, symbol)
        EIP712(name, "1.0.0")
    {
        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
    }

    function _hash(address account, uint256 tokenId)
        internal
        view
        returns (bytes32)
    {
        return
            _hashTypedDataV4(
                keccak256(
                    abi.encode(
                        keccak256("NFT(uint256 tokenId,address account)"),
                        tokenId,
                        account
                    )
                )
            );
    }

    function _verify(bytes32 digest, bytes memory signature)
        internal
        view
        returns (bool)
    {
        return hasRole(MINTER_ROLE, ECDSA.recover(digest, signature));
    }

    function getAccountAddress(address account, uint256 tokenId, bytes calldata signature) external view returns(address) {
        return ECDSA.recover(_hash(account, tokenId), signature);
    }
}

I'm trying to implement ERC721LazyMintWith712.sol Smart Contract from this repo. However, When I test the contract by creating Test Cases, it works perfectly but, With Front-End integration, it doesn't!

1 Answer 1

3

I stumbled upon this question and went through the Ethers.js code as well as the draft-EIP712.sol code, but kept having the same result as you.

The codes are entirely correct and adhere to EIP-712. My best guess is that you were probably using a test blockchain environment such as ganache / ganache-cli which has a very specific behavior:

  • networkId returned by RPC is 1337 by default
  • chainId returned by CHAINID Opcode is 1 by default.

With this line:

const { chainId } = await provider.getNetwork();

You are actually getting the networkId, not the chainId that draft-EIP712.sol gets with block.chainid. So the different Ids lead to different data, different hashes, and therefore different signatures.

While you can hardcode a chainId of 1 in your JavaScript code, the cleanest solution is to use the --chainId X flag on ganache where X can be replaced by the chainId of your choice (i.e., 1337) and it will ensure that both RPC chainId and Opcode chainID are the same value.

With ganache-cli --chainId 1337, your code works fine with Remix or MetaMask, with Hardhat chainId and networkId being coherent between RPC and Opcode by default.

A second ongoing issue with MetaMask (I assume that you were using it) is that the default http://localhost:8545 endpoint is assumed to have a chainId of 1337, which might not be the case with Hardhat or a specific configuration of ganache that can also lead to a wrong signature on the client side but should forbid sending any tx too. Either set your node to a chainId of 1337 or create your own MetaMask endpoint.

So your code is fine. It's definitely a configuration issue either with MetaMask / Ganache or Hardhat as described above.

3
  • 1
    I did deploy to rinkeby and still have same issue, i will try to use eth-sig-util now to confirm which one wrong, encoding with ethers or decoding with ecdsa Commented Jul 18, 2022 at 7:23
  • @DellWatson Did you try to use eth-sig-util? If so, did it solve the problem?
    – BonisTech
    Commented Feb 1, 2023 at 6:09
  • @Dell Watson, Yash Gajjar Is this issue resolved? I am also facing same issue on Mumbai polygon Commented May 27, 2023 at 7:08

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.