# EIP-712笔记

By [0xY](https://paragraph.com/@0xy-2) · 2023-11-02

---

EIP-712使用详情
===========

利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单，一般就是给一串字符串，或者一串哈希签名，如果我们想为更复杂的数据签名就无法实现了。

EIP-712出现是为了解决这个问题，利用EIP-712，我们可以对更大的数据集，例如对结构体进行签名。在Uniswap PancakeSwap等DEX在移除LP流动性的时候，我们需要先签名，然后再发送一笔交易移除流动性。正常情况下，其实我们先调用LP代币的授权方法，授权DEX合约可以转移LP，然后再去移除流动性。这种二合一的实现正是应用了EIP-712。他帮助我们只需要签名一次就可以将两部交易合并为一步交易，从而节省gas费用。

基本结构
----

### EIP712Domain

顾名思义，是一个与`域` 相关的结构体，总共包含五个字段：

*   `name` 合约或者协议名称
    
*   `version` 合约的版本
    
*   `chainId` 合约部署的链Id，一般使用`block.chainid` 即当前链Id
    
*   `verifyingContract` 签名的合约地址，一般用address(this)
    
*   `salt` 随机数盐，一般不常用
    

DOMAIN\_SEPARATOR

EIP712Domain数据的哈希值，即：

    DOMAIN_SEPARATOR = keccak256 (
                abi.encode(
                        EIP712DOMAIN_TYPEHASH,
                        keccak256(name,合约名称),
                        keccak256(version,合约版本),
                        chainId,
                        verifyingContract
                )
    )
    

EIP712DOMAIN\_TYPEHASH

    bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
                "EIP712Domain(string name,string version,uint256 chainId,address verifying)"
    );
    

### 签名对象

这里我们以签名`Mail` 对象为例

    struct Mail{
            address from;
            address to;
            string contents;
    }
    

#### 签名对象类型哈希

    bytes32 internal constant TYPE_HASH = keccak256(
            "Mail(address from,address to,string contents)"
    );
    

注意对象名要首字母大写，结构体字段按照函数签名编写，这是规范，套用即可

如果结构体中还有其他结构体，例如：

    struct Transaction{
            Person from;
            Person to;
            Asset	tx;
    }
    
    struct Asset{
            address token;
            uint256 amount;
    }
    
    struct Person{
            address wallet;
            string name;
    }
    

需要写成（按照首字母排序，因此`Asset` 在`Person` 前面）：

    Transaction(Person from,Person to,Asset tx) Asset(address token,uint256 amount) Person(address wallet,string name)
    

#### 签名对象值哈希

    keccak256(
            abi.encode(
                    TYPE_HASH,
                    mail.from,
                    mail.to,
                    keccak256(bytes(mail.contents))
            )
    );
    

其中第一个参数为`TYPE_HASH` ，即签名对象类型的哈希。接下来依次是对象的各个字段，如果变长类型例如`string` ,`bytes` 则需要对其进行哈希。例如：这里的`mail.contents`是`srting` 类型，因此需要进行哈希。

代码
--

#### 合约

    // SPDX-License-Identifier: SEE LICENSE IN LICENSE
    pragma solidity ^0.8.0;
    
    contract EIP712Mail {
        //Mail 是带签名结构体
        struct Mail {
            address from;
            address to;
            string contents;
        }
    
        struct EIP712Domain {
            string name;
            string version;
            uint256 chainId;
            address verifyingContract;
        }
        bytes32 public immutable DOMAIN_SEPARATOR;
    
        //固定不变的
        bytes32 public constant EIP712DOMAIN_TYPEHASH =
            keccak256(
                "EIP712Domain(string name,strinf version,uint256 chainId,address verifyingContract)"
            );
    
        //签名对象哈希
        bytes32 internal constant TYPE_HASH =
            keccak256("Mail(address from,address to,string contents)");
    
        constructor() {
            //Domain的HASH
            DOMAIN_SEPARATOR = keccak256(
                //计算DMIAN_SEPARATOR哈希
                //这里的name为EIP712Mail，即合约名称
                abi.encode(
                    EIP712DOMAIN_TYPEHASH,
                    keccak256("EIP712Mail"),
                    keccak256("1"),
                    block.chainid,
                    address(this)
                )
            );
        }
    
        //计算待签名的结构体的哈希
        function hashStruct(Mail memory mail) public pure returns (bytes32) {
            return
                keccak256(
                    abi.encode(
                        TYPE_HASH,
                        mail.from,
                        mail.to,
                        keccak256(bytes(mail.contents))
                    )
                );
        }
    
        function verify(
            Mail memory mail,
            address signer,
            uint8 v,
            bytes32 r,
            bytes32 s
        ) public view returns (bool) {
            // Note:we need to use `encodePacked` here instead of `encode`
            //固定格式套用即可
            bytes32 digest = keccak256(
                abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, hashStruct(mail))
            );
            return ecrecover(digest, v, r, s) == signer;
        }
    }
    

`verify` 函数接收三个参数，分别是待签名结构体，签名地址，v，r，s。其中v，r，s是构成签名的三部分，签名一共有65个字节，前32个字节是r，接下来32个字节是s，最后一个字节是v，`ecrecover` 是Solidity内置函数，可以用于验证签名，他会根据digest以及签名内容v，r，s来计算出签名人的地址。如果结果等于传入的签名地址。则说明验证签名正确。

#### 使用JavaScript进行签名：

    const { ethers } = require('ethers')
    // 将合约部署在 hardhat node 本地链上
    const provider = new ethers.JsonRpcProvider()
    
    // 这里我们使用 hardhat node 自带的地址进行签名
    const privateKey = `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`
    const wallet = new ethers.Wallet(privateKey, provider)
    
    async function sign() {
      // 获取 chainId
      const { chainId } = await provider.getNetwork()
    
      // 构造 domain 结构体
      // 最后一个地址字段，由于我们在合约中使用了 address(this)
      // 因此需要在部署合约之后，将合约地址粘贴到这里
      const domain = {
        name: 'EIP712Mail',
        version: '1',
        chainId: 4,
        verifyingContract: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0',
      }
      // The named list of all type definitions
      // 构造签名结构体类型对象
      const types = {
        Mail: [
          { name: 'from', type: 'address' },
          { name: 'to', type: 'address' },
          { name: 'contents', type: 'string' },
        ],
      }
      // The data to sign
      // 自行构造一个结构体的值
      const value = {
        from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
        to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8',
        contents: 'xyyme',
      }
      const signature = await wallet.signTypedData(domain, types, value)
    
      // 将签名分割成 v r s 的格式
      let signParts = ethers.Signature.from(signature)
      console.log('>>> Signature:', signParts)
      // 打印签名本身
      console.log(signature)
    }
    
    sign()
    

我们将rsv签名，value值，以及签名地址传给`verify` 函数进行验证，结果为true，说明验证成功

### 应用

在Uniswap中运用了EIP-712，使得移除流动性的操作由两步变成一步，减少了Gas的使用。Dai合约中有一个`Permit`函数，用于第三方授权，同样也是应用了EIP-712标准。

在ERC20中，A可以调用`approve` 来对B进行授权，而Dai合约中的`Permit`函数的目的就是，A提前在联系啊对授权对象进行签名，这样第三方就可以拿着A的签名去调用`permit` 来实现A的授权操作，从而使A在不发送交易的情况下就能够完成授权操作。

#### Dai核心代码

    contract Dai is LibNote {
        // ERC20 信息，name 和 version 用于 domain 签名
        string  public constant name     = "Dai Stablecoin";
        string  public constant symbol   = "DAI";
        string  public constant version  = "1";
        uint8   public constant decimals = 18;
        uint256 public totalSupply;
    
        mapping (address => uint)                      public balanceOf;
        mapping (address => mapping (address => uint)) public allowance;
        // nonces 用于避免重放攻击
        mapping (address => uint)                      public nonces;
    
        // --- EIP712 niceties ---
        bytes32 public DOMAIN_SEPARATOR;
    
        // 计算签名结构体 Permit 的哈希
        // bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)");
    
        bytes32 public constant PERMIT_TYPEHASH = 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb;
    
        constructor(uint256 chainId_) public {
            wards[msg.sender] = 1;
            // 计算 domain 哈希
            DOMAIN_SEPARATOR = keccak256(abi.encode(
                keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
                keccak256(bytes(name)),
                keccak256(bytes(version)),
                chainId_,
                address(this)
            ));
        }
    
        // 常规授权方法
        function approve(address usr, uint wad) 
            external returns (bool) {
            allowance[msg.sender][usr] = wad;
            emit Approval(msg.sender, usr, wad);
            return true;
        }
    
        // --- Approve by signature ---
        // 重点是这里的 permit 函数
        function permit(address holder, address spender, uint256 nonce, uint256 expiry,
                        bool allowed, uint8 v, bytes32 r, bytes32 s) external
        {
            bytes32 digest =
                keccak256(abi.encodePacked(
                    "\x19\x01",
                    DOMAIN_SEPARATOR,
                    keccak256(abi.encode(PERMIT_TYPEHASH,
                                         holder,
                                         spender,
                                         nonce,
                                         expiry,
                                         allowed))
            ));
    
            require(holder != address(0), "Dai/invalid-address-0");
            //在这判断是否为持有者签名（验证签名）
            require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit");
            //判断是否过期
            require(expiry == 0 || now <= expiry, "Dai/permit-expired");
            // 用于防止重放攻击
            require(nonce == nonces[holder]++, "Dai/invalid-nonce");
            uint wad = allowed ? uint(-1) : 0;
            //进行授权
            allowance[holder][spender] = wad;
            emit Approval(holder, spender, wad);
        }
    }
    

`permit` 函数的参数中，`holder`地址需要在链下进行签名,`spender` 即被授权地址`nonce`用于防止重放攻击。

---

*Originally published on [0xY](https://paragraph.com/@0xy-2/eip-712)*
