# ERC20的Permit **Published by:** [r4yyy.eth](https://paragraph.com/@r4yyy/) **Published on:** 2022-05-08 **URL:** https://paragraph.com/@r4yyy/erc20-permit ## Content 前 之前学习的时候遇到了 permit 这个ERC20 方法,可以实现无gas 转账和离线交易。 听起来很神奇,但是实际上的原理并不复杂,在这里来进行一个总结 正文 什么是 Permit permit 在 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的额度。 那么由此过程我们可以衍生出下面的用途 离线支付 场景是A在离线的情况下用钱包签名 approval的方法,在签名的时候,带有 token/数量/收款人/deadline。签名之后把签出的内容给到B(可公开),B在有网络的情况下使用 permit的方法获取对应额度完成转账操作。 但是值得注意的是:这里的转账过程不是可靠的,因为你拿到的只是对应的 allowence 的额度,实际上需要B进行permit 获得 allowance之后,再进行transferFrom 才可以得到对应的数量token。如果A在B执行这个权利之前把账号资产转走,那么B只是得到这个授权额度但是无法获得任何资产。这里可以类比于开出了一张空头支票。 无gas转账 不过需要提前明确的是,这里的无gas 不是指的没有gas 消耗,而是A方不需要为授权和转账来付出gas。一般的ERC20 的转账形式是,调用方调用 approval 和 transfer 来进行转账。在使用了 permit 的情况下,A 可以签名 Approval 函数,之后发送给B,b在获得签名之后去调用 Permit 来获得 allowance 使用 transferFrom 来进行转账。全部过程中不需要A支付任何GAS 实现原理 合约验签逻辑 在实现这里,直接找到 EIP-2612的commit 这里实现了 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? 签名与校验 Solidity 中的 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签名 这里有两个点 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 的方法。 研究底层的实现还算有趣。 ## Publication Information - [r4yyy.eth](https://paragraph.com/@r4yyy/): Publication homepage - [All Posts](https://paragraph.com/@r4yyy/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@r4yyy): Subscribe to updates - [Twitter](https://twitter.com/_R4y_opz): Follow on Twitter