How to use EIP-712 Signature
Introduction to EIP-712
EIP-712, or "Typed structured data hashing and signing," is a standard in Ethereum Improvement Proposals. It provides a standardized way to sign structured data, making the signing process more secure and user-friendly.
Key Components of EIP-712 Signatures
- 
EIP712Domain: Every EIP-712 signature must include an EIP712Domain section. This section contains crucial information about the contract and the environment:
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" },
];This information is displayed during the signing process and ensures that the signature can only be verified by a specific contract on a specific chain.
 - 
Domain Object: In your signing script, you need to provide the domain information:
const domain = {
name: "EIP712Voting",
version: "1",
chainId: 71, // Conflux eSpace testnet
verifyingContract: "0xDD1184EeC78eD419d948887B8793E64a62f13895",
}; - 
Custom Types: You need to define custom types that match your contract's structure:
const types = {
Vote: [
{ name: "voter", type: "address" },
{ name: "proposal", type: "uint256" },
{ name: "nonce", type: "uint256" },
],
}; - 
Message: Create a message object with the data to be signed:
const value = {
voter: await signer.getAddress(),
proposal: 1, // Voting for proposal 1
nonce: await contract.nonces(signer.address),
}; - 
Signing Process: Use the wallet's
signTypedData()method to create the signature:const signature = await signer.signTypedData(domain, types, value); 
Benefits of EIP-712
- Improved Readability: Users can clearly see what they're signing, reducing the risk of malicious transactions.
 - Enhanced Security: The structured format helps prevent certain types of phishing attacks.
 - Better User Experience: Wallets and dApps can display more meaningful signing requests.
 - Cross-Platform Consistency: Ensures consistent behavior across different Ethereum-compatible platforms.
 
In this tutorial, we'll implement EIP-712 signatures on the Conflux eSpace network using Hardhat, creating a simple voting system to demonstrate its usage. Our voting system will allow users to sign their votes off-chain and submit them to the blockchain, ensuring both privacy and efficiency.
1. Project Setup
First, ensure you have Node.js and npm installed. Then, create a new project directory and initialize it:
mkdir eip712-conflux-demo
cd eip712-conflux-demo
npm init -y
Install the necessary dependencies:
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts dotenv
2. Configure Hardhat
Create a Hardhat configuration file hardhat.config.js:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
  solidity: "0.8.24",
  networks: {
    eSpaceTestnet: {
      url: "https://evmtestnet.confluxrpc.com",
      accounts: [process.env.PRIVATE_KEY],
    },
  },
};
Create a .env file to store your private key:
PRIVATE_KEY=your_private_key_here
Make sure to add .env to your .gitignore file.
3. Write the Smart Contract
Create a contracts/EIP712Voting.sol file:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
contract EIP712Voting is EIP712 {
    using ECDSA for bytes32;
    mapping(uint256 => uint256) public voteCount;
    // TypeHash for the Vote struct used in EIP-712 signing
    bytes32 private constant VOTE_TYPEHASH =
        keccak256("Vote(address voter,uint256 proposal,uint256 nonce)");
    mapping(address => uint256) public nonces;
    event VoteCast(address indexed voter, uint256 indexed proposal);
    constructor() EIP712("EIP712Voting", "1") {}
    function castVote(uint256 proposal, bytes memory signature) external {
        // Generate the hash of the structured data
        bytes32 structHash = keccak256(
            abi.encode(
                VOTE_TYPEHASH, // Type hash of the Vote struct, ensures data structure consistency
                msg.sender, // Address of the voter
                proposal, // ID of the proposal being voted on
                nonces[msg.sender] // Current nonce of the voter, prevents replay attacks
            )
        );
        // structHash now contains a unique identifier of the vote data
        // Generate the final hash using the EIP-712 standard's _hashTypedDataV4 function
        bytes32 hash = _hashTypedDataV4(structHash);
        // hash is now the final hash combining the structured data hash and the domain separator
        // This final hash is used to verify the EIP-712 signature
        // The domain separator includes contract name, version, chain ID, and contract address,
        // ensuring the signature is only valid for this specific contract and network
        address signer = ECDSA.recover(hash, signature);
        require(signer == msg.sender, "EIP712Voting: Invalid signature");
        voteCount[proposal]++;
        nonces[signer]++;
        emit VoteCast(signer, proposal);
    }
    function getVoteCount(uint256 proposal) external view returns (uint256) {
        return voteCount[proposal];
    }
}
This contract implements EIP-712 signature verification and voting functionality.
4. Write the Deployment Script
Create a scripts/deploy.js file:
const hre = require("hardhat");
async function main() {
  const EIP712Storage = await hre.ethers.getContractFactory("EIP712Voting");
  const eip712Storage = await EIP712Storage.deploy();
  // Wait for the contract to be deployed
  await eip712Storage.waitForDeployment();
  // Get the deployed contract address
  const address = await eip712Storage.getAddress();
  console.log("EIP712Storage deployed to:", address);
}
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
5. Deploy the Contract
Run the following command to deploy the contract:
npx hardhat run scripts/deploy.js --network eSpaceTestnet
Note down the output contract address.
6. Create the Signing Script
Create a scripts/sign.js file:
const hre = require("hardhat");
require("dotenv").config();
async function main() {
  const [signer] = await hre.ethers.getSigners();
  const contractAddress = "<YOUR_DEPLOYED_CONTRACT_ADDRESS>";
  const domain = {
    name: "EIP712Voting",
    version: "1",
    chainId: 71, // Conflux eSpace testnet
    verifyingContract: contractAddress,
  };
  const types = {
    Vote: [
      { name: "voter", type: "address" },
      { name: "proposal", type: "uint256" },
      { name: "nonce", type: "uint256" },
    ],
  };
  const EIP712Voting = await hre.ethers.getContractFactory("EIP712Voting");
  const contract = EIP712Voting.attach(contractAddress);
  const nonce = await contract.nonces(signer.address);
  const value = {
    voter: await signer.getAddress(),
    proposal: 1, // Assume we're voting for proposal 1
    nonce: nonce,
  };
  // Use the new signTypedData method
  const signature = await signer.signTypedData(domain, types, value);
  console.log("Signer:", await signer.getAddress());
  console.log("Proposal:", value.proposal);
  console.log("Nonce:", nonce.toString());
  console.log("Signature:", signature);
}
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
Remember to update contractAddress with your deployed contract address.
7. Generate the Signature
Run the signing script:
npx hardhat run scripts/sign.js --network eSpaceTestnet
This will output the signature information.
8. Create the Voting Script
Create a scripts/vote.js file:
const hre = require("hardhat");
async function main() {
  const contractAddress = "<YOUR_DEPLOYED_CONTRACT_ADDRESS>";
  const EIP712Voting = await hre.ethers.getContractFactory("EIP712Voting");
  const contract = EIP712Voting.attach(contractAddress);
  const proposal = 1; // Same as the proposal number used in the signature
  const signature = "<YOUR_SIGNATURE>";
  const tx = await contract.castVote(proposal, signature);
  await tx.wait();
  console.log("Vote cast successfully");
  const voteCount = await contract.getVoteCount(proposal);
  console.log("Vote count for proposal", proposal, ":", voteCount.toString());
}
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
Update contractAddress and signature with your actual values.
9. Execute the Vote
Run the voting script:
npx hardhat run scripts/vote.js --network eSpaceTestnet
This will cast a vote using the generated signature.
10. Create a Frontend Interface
Create a public/sign.html file:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>EIP-712 Voting with MetaMask</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.min.js"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
      }
      button {
        margin: 10px 0;
        padding: 10px;
      }
      #status,
      #result,
      #voteInfo {
        margin-top: 20px;
      }
    </style>
  </head>
  <body>
    <h1>EIP-712 Voting with MetaMask</h1>
    <button id="connectButton">Connect MetaMask</button>
    <div id="status"></div>
    <div id="votingSection" style="display:none;">
      <h2>Cast Your Vote</h2>
      <input
        type="number"
        id="proposalInput"
        placeholder="Enter proposal number"
      />
      <button id="voteButton">Vote</button>
    </div>
    <div id="result"></div>
    <div id="voteInfo"></div>
    <script type="module">
      import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.min.js";
      const contractAddress = "YOUR_DEPLOYED_CONTRACT_ADDRESS";
      const contractABI = [
        "function nonces(address owner) view returns (uint256)",
        "function castVote(uint256 proposal, bytes memory signature) external",
        "function getVoteCount(uint256 proposal) view returns (uint256)",
        "function getVoterProposal(address voter) view returns (uint256)", // Assuming this function exists in the contract
      ];
      let provider, signer, contract;
      const connectButton = document.getElementById("connectButton");
      const statusDiv = document.getElementById("status");
      const votingSection = document.getElementById("votingSection");
      const proposalInput = document.getElementById("proposalInput");
      const voteButton = document.getElementById("voteButton");
      const resultDiv = document.getElementById("result");
      const voteInfoDiv = document.getElementById("voteInfo");
      const checkVoteButton = document.getElementById("checkVoteButton");
      connectButton.addEventListener("click", async () => {
        if (typeof window.ethereum !== "undefined") {
          try {
            await window.ethereum.request({ method: "eth_requestAccounts" });
            provider = new ethers.BrowserProvider(window.ethereum);
            signer = await provider.getSigner();
            contract = new ethers.Contract(
              contractAddress,
              contractABI,
              signer
            );
            const address = await signer.getAddress();
            statusDiv.innerHTML = ``;
            votingSection.style.display = "block";
            checkVoteButton.style.display = "block";
          } catch (error) {
            console.error(error);
            statusDiv.innerHTML = "Failed to connect to MetaMask";
          }
        } else {
          statusDiv.innerHTML = "Please install MetaMask";
        }
      });
      voteButton.addEventListener("click", async () => {
        const proposal = proposalInput.value;
        if (!proposal) {
          alert("Please enter a proposal number");
          return;
        }
        try {
          const address = await signer.getAddress();
          const nonce = await contract.nonces(address);
          const domain = {
            name: "EIP712Voting",
            version: "1",
            chainId: Number((await provider.getNetwork()).chainId),
            verifyingContract: contractAddress,
          };
          const types = {
            Vote: [
              { name: "voter", type: "address" },
              { name: "proposal", type: "uint256" },
              { name: "nonce", type: "uint256" },
            ],
          };
          const value = {
            voter: address,
            proposal: BigInt(proposal),
            nonce: nonce,
          };
          const signature = await signer.signTypedData(domain, types, value);
          const tx = await contract.castVote(proposal, signature);
          await tx.wait();
          const voteCount = await contract.getVoteCount(proposal);
          resultDiv.innerHTML = ``;
        } catch (error) {
          console.error("Voting error:", error);
          let errorMessage = error.message;
          if (error.data && typeof error.data.message === "string") {
            const match = error.data.message.match(
              /execution reverted: (.*?)(?:\.?$)/
            );
            if (match) {
              errorMessage = match[1];
            }
          }
          resultDiv.innerHTML = "Failed to cast vote: " + errorMessage;
        }
      });
    </script>
  </body>
</html>
This HTML file provides a simple user interface for connecting MetaMask, casting votes, and checking voting results.
11. Run the Frontend
Use Live Server or another HTTP server to run public/sign.html. Make sure to update contractAddress with your deployed contract address.
结论
Through this tutorial, you've learned how to implement EIP-712 signatures on Conflux eSpace using Hardhat. This includes writing and deploying smart contracts, generating and verifying signatures, and creating a simple frontend interface to interact with the contract.
Remember to always protect your private keys and thoroughly test your application on testnets before conducting any real transactions.
