# ERC20的Permit

By [r4yyy.eth](https://paragraph.com/@r4yyy) · 2022-05-08

---

前
=

之前学习的时候遇到了 `permit` 这个ERC20 方法，可以实现无gas 转账和离线交易。

听起来很神奇，但是实际上的原理并不复杂，在这里来进行一个总结

正文
==

什么是 Permit
----------

permit 在 [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) 中被引入到 ERC20 的协议中。子标题是 **signed approvals** ，另外从字面意思上看`permit` 在文字含义上是许可的意思，也正是与经典的erc20 协议里面 `allowance` 有关。

其功能从简单来讲，就是A在需要给B进行授权的时候，不需要主动的去调用 `approval`函数来给B进行授权。而是给这个approval 函数一个合法的签名，得到的签名提供给B，B用这个签名来调用 `permit`来获得对应额度的 allowance，从而可以对指定金额来进行消费或者转账。

这里用伪代码的approve函数以及 permit 函数进行对比，可见其核心的功能就是set一个对应的allowance

    function approve(address usr, uint wad) external returns (bool)
    {
      allowance[msg.sender][usr] = wad;
      …
    }
    function permit(
      address holder, address spender,
      uint256 nonce, uint256 expiry, bool allowed,
      uint8 v, bytes32 r, bytes32 s
    ) external {
      …
      allowance[holder][spender] = wad;
      …
    }
    

permit应用场景
----------

前面讲到permit，使用对approval的调用来进行签名，给到收款方来获得一个预先设置的 allowance的额度。

那么由此过程我们可以衍生出下面的用途

1.  离线支付
    
    场景是A在离线的情况下用钱包签名 approval的方法，在签名的时候，带有 token/数量/收款人/deadline。签名之后把签出的内容给到B（可公开），B在有网络的情况下使用 `permit`的方法获取对应额度完成转账操作。
    
    但是值得注意的是：这里的转账过程不是可靠的，因为你拿到的只是对应的 allowence 的额度，实际上需要B进行permit 获得 allowance之后，再进行transferFrom 才可以得到对应的数量token。如果A在B执行这个权利之前把账号资产转走，那么B只是得到这个授权额度但是无法获得任何资产。这里可以类比于开出了一张**空头支票**。
    
2.  无gas转账
    
    不过需要提前明确的是，这里的无gas 不是指的没有gas 消耗，而是A方不需要为授权和转账来付出gas。一般的ERC20 的转账形式是，调用方调用 approval 和 transfer 来进行转账。在使用了 permit 的情况下，A 可以签名 Approval 函数，之后发送给B，b在获得签名之后去调用 Permit 来获得 allowance 使用 transferFrom 来进行转账。全部过程中不需要A支付任何GAS
    

实现原理
----

### 合约验签逻辑

在实现这里，直接找到 [EIP-2612的commit](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2237/files/f410734f41f632b42ab4fc4e8695f47d800de709#diff-184791c357255d1de23b57e5660cecb2d01409238f14b76f87de3c5d89bfee77) 这里实现了 permit 函数

这里把参数分开

*   address owner // A地址
    
*   address spender // B地址
    
*   uint256 amount // 总额度
    
*   uint256 deadline // 过期时间
    
*   uint8 v, bytes32 r, bytes32 s // secp256k1（ECDSA） 的恢复ID 以及 RS 的签名输出。
    

> *   [What does v, r, s in eth\_getTransactionByHash mean?](https://ethereum.stackexchange.com/questions/15766/what-does-v-r-s-in-eth-gettransactionbyhash-mean)
>     
> *   [签名与校验](https://learnblockchain.cn/books/geth/part3/sign-and-valid.html)
>     
> *   [Solidity 中的 ecrecover 是什么？](https://soliditydeveloper.com/ecrecover)
>     

        function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public virtual override {
            // solhint-disable-next-line not-rely-on-time
            require(block.timestamp <= deadline, "ERC20Permit: expired deadline");
            // 对全部提供的内容按格式进行打包之后进行hash
            bytes32 structHash = keccak256(
                abi.encode(
                        // 这里限制了签名的类型，避免任意签名。
                    _PERMIT_TYPEHASH,
                    owner,
                    spender,
                    amount,
                    _nonces[owner].current(),
                    deadline
                )
            );
               // 这里进行hash结构的转化
            bytes32 hash = _hashTypedDataV4(structHash);
                    // 恢复对原始的签名数据来进行验签，看此内容验签之后的 signer 是不是传入数据的 owner
            address signer = ECDSA.recover(hash, v, r, s);
            require(signer == owner, "ERC20Permit: invalid signature");
                    // 如果是
            _nonces[owner].increment();
            // 给permit调用者对应的授权额度
            _approve(owner, spender, amount);
        }
    

### 客户端签名逻辑

参考链接： [Permit-712签名](https://blog.csdn.net/weixin_43840202/article/details/122957126)

这里有两个点

*   DOMAIN\_SEPARATOR // 定义域分隔符
    
*   \_PERMIT\_TYPEHASH // Permit 的参数格式的hash
    

\_PERMIT\_TYPEHASH 定义见下：

    
    bytes32 private immutable _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
    

* * *

在客户端中使用，下面代码来获取合约的PERMIT\_TYPEHASH 和 DOMAIN\_SEPARATOR

    const contract = new ClientContract(abi, '0x6b175474e89094c44da98b954eedeac495271d0f', 1)
        const calls = [
            contract.PERMIT_TYPEHASH(),
            contract.DOMAIN_SEPARATOR(),
        ]
        const [PERMIT_TYPEHASH, DOMAIN_SEPARATOR] = await multicallClient(calls)
        //DOMAIN_SEPARATOR: 0xdbb8cf42e1ecb028be3f3dbc922e1d878b963f411dc388ced501601c60f7c6f7
        //PERMIT_TYPEHASH: 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb
    

之后构造签名合约里用到的structureHash

    const digestHash = web3.utils.keccak256(web3.eth.abi.encodeParameters(['bytes32', 'address', 'address', 'uint256', 'uint256', 'uint256'], [
              PERMIT_TYPEHASH,
              holder,//你的地址
              spender,//授权给目标地址
                  amount,//你要授权的数量
              nonce,//你在DAI里面的nonce
              expiry,//授权到期时间
              ]
          ))
    

对这个结构进行签名，之后发送给B，就完成了这个 permit 的签发。

    const signatureHash = await web3.eth.sign(digest, account);
    

后
=

对Permit 原理上有大概的理解，些许有些复杂，不过好在应用上 openzeppelin 做了很大程度的封装。

在调用前端有 EIP712 的helper 直接引用即可，合约部分默认的 ERC20的合约已经包含了Permit 的方法。

研究底层的实现还算有趣。

---

*Originally published on [r4yyy.eth](https://paragraph.com/@r4yyy/erc20-permit)*
