前言
Uniswap
新发布了新的代币授权标准 Permit2
,区别于传统的 ERC20 permit
, Permit2
可以让用户无需在和不同的 DApp
交互前都需要首先进行一笔 approve
操作,让 DApp
协议首先获取你的代币授权。根据描述,新的Permit2
协议具有节省 gas
、可批量操作授权/转账且比传统 ERC20 approve
更安全,并且支持一站式的授权管理。说得很花,但具体怎样,还是要从代码出发
授权基础
根据官方介绍,Permit2
衍生于 EIP 2612
,属于一种拓展的 EIP 20
协议,所以归根到底,Permit2
只是 ERC20
的一种补充,而不是取代。毕竟 Permit2
并没有可以直接继承现有所有的 ERC20
数据,所谓的一站式管理,本质上还是需要调用 ERC20
合约的 approve
操作来完成一些初始的操作。为了探究 Permit2
的授权原理,我们需要先从最核心的 permit
函数出发,代码如下:
function permit(address owner, PermitSingle memory permitSingle, bytes calldata signature) external {
if (block.timestamp > permitSingle.sigDeadline) revert SignatureExpired(permitSingle.sigDeadline);
// Verify the signer address from the signature.
signature.verify(_hashTypedData(permitSingle.hash()), owner);
_updateApproval(permitSingle.details, owner, permitSingle.spender);
}
以上便是位于核心合约 AllowanceTransfer
中的 permit
函数,整个 Permit2
协议都是依赖这个函数进行所谓的授权管理。首先,我们观察参数结构。参数中存在变量 owner
、permitSingle
结构体变量及一个签名参数 signature
。通过结合下面的代码逻辑不难分析,owner
变量实际上就是用于结合 signature
变量进行一波验签。在签名验证通过后,根据 permitSingle
变量的信息对授权数据进行更新。到这里,逻辑都很简单,但是有一个疑问,结合上文,我们得知 Permit2
协议实际上是无法操控别的代币的授权的。也就是说就是在验签通过后,Permit2
协议只能更改用户的在 Permit2
协议里的数据,没有办法更新到别的地方的数据。那 Permit2
协议又是如何管理用户的授权数据呢?留着这个疑问,我们先看 permitSingle
结构体的定义,如下:
struct PermitDetails {
address token;
uint160 amount;
uint48 expiration;
uint48 nonce;
}
struct PermitSingle {
PermitDetails details;
address spender;
uint256 sigDeadline;
}
通过函数定义,不难发现,PermitSingle
结构体里头其实定义了授权的基本信息,包含 spender
授权人、PermitDetails
变量及sigDeadline
变量。其中根据上文的 permit
函数逻辑,很容易推断出 sigDeadline
指定的是每次 permit
签名的有效时间,而 PermitDetails
变量则是关于具体的代币授权信息的细节,除了常规的 token
和 amount
这些 ERC20
授权中会用到的变量之外,还添加了 expiration
和 nonce
。expiration
顾名思义,就是指的是本次授权的有效时间范围,而由于签名验证中验证的是 PermitSingle
变量,所以需要 nonce
变量来防止签名被重复使用。
OK,到这里我们基本分析完核心函数的基本数据结构,我们知道一次 permit
授权需要一个授权信息,其中必须指定授权的代币、数量及授权的有效期。同时,我们还要一个针对授权信息的签名,并且指定一个签名的过期时间,方式签名被无限期使用。有了以上信息,那么接下来我们分析 permit
函数的最后一个逻辑 _updateApproval
,代码如下:
struct PackedAllowance {
// amount allowed
uint160 amount;
// permission expiry
uint48 expiration;
// an incrementing value indexed per owner,token,and spender for each signature
uint48 nonce;
}
function _updateApproval(PermitDetails memory details, address owner, address spender) private {
uint48 nonce = details.nonce;
address token = details.token;
uint160 amount = details.amount;
uint48 expiration = details.expiration;
PackedAllowance storage allowed = allowance[owner][token][spender];
if (allowed.nonce != nonce) revert InvalidNonce();
allowed.updateAll(amount, expiration, nonce);
emit Permit(owner, token, spender, amount, expiration, nonce);
}
在签名验证通过后,最后调用的 _updateApproval
函数,其实只是将先前签名的 PermitSingle
变量的信息保存到一个全局的 PackedAllowance
变量里。这个变量通过 allowance[owner][token][spender]
来进行索引。
那么到了这里,其实我们还没有看到任何和 ERC20
相关的操作,只是对 Permit2
合约数据的更改,而且分析完那么多,我们很清晰的发现,其实所谓的授权管理,只是管理在 Permit2
合约里的 allowance
授权罢了,那么如何通过 Permit2
函数真正的管理代币的授权呢?答案在 _transfer
函数里,代码如下:
function transferFrom(address from, address to, uint160 amount, address token) external {
_transfer(from, to, amount, token);
}
function _transfer(address from, address to, uint160 amount, address token) private {
PackedAllowance storage allowed = allowance[from][token][msg.sender];
if (block.timestamp > allowed.expiration) revert AllowanceExpired(allowed.expiration);
uint256 maxAmount = allowed.amount;
if (maxAmount != type(uint160).max) {
if (amount > maxAmount) {
revert InsufficientAllowance(maxAmount);
} else {
unchecked {
allowed.amount = uint160(maxAmount) - amount;
}
}
}
// Transfer the tokens from the from address to the recipient.
ERC20(token).safeTransferFrom(from, to, amount);
}
如果光看 Permit2
的 transferFrom
函数,不难发现这个函数貌似可以把任意地址的资金转给另一个地址,接下来分析 _transfer
函数,发现内部函数逻辑其实检查了 allowance
变量,以 msg.sender
作为索引。在得到变量信息后检查授权的额度和过期时间,并在最后调用了 ERC20
的 transferFrom
函数来实现从 from
到 to
的代币转移。到这里,我们终于发现重点了,由于 transferFrom
函数调用者是 Permit2
合约,所以肯定需要用户对 Permit2
合约进行一个授权。同时,我们再串联前面的知识,发现其实授权管理,管理的是别人可以从 Permit2
合约中能转移你多少代币,而不是 Permit2
合约接管了其他 ERC20
然后进行额度管理。
同时结合 _transfer
函数的逻辑,我们不难发现,其实 Permit2
完整的流程应该是
-
用户将
ERC20
代币的最大授权给到Permit2
合约 -
用户通过
permit
函数对Permit2
合约中的具体授权进行管理 -
第三方协议和用户可以根据
Permit2
中已有的授权信息通过Permit2
合约作为中间人实现代币的转移。
所以 Permit2 是更好的授权模型吗?
在理解了授权原理之后,我们可以发现 Permit2
协议具有的优点
-
统一的代币管理
-
可控制的授权时间
-
不用每次都多发一笔交易来进行授权
但其实问题也是存在
-
号称解决了
infinity approval
的问题,但实际只是把授权对象从需要交互的DApp
转变成了Permit2
合约,只是风险转移。用户还是需要无限授权。第三方接入Permit2
协议的时候,可能无法保证安全 -
虽然说代币授权有过期时间,但是这个时间依旧可以无限大,导致其实和无限授权的结果是差不多的,显得很鸡肋
-
由于调用
permit
函数的过程可以不发送交易而是只提供签名给第三方代发,如果是做钓鱼的话可以做得更加隐蔽,用户检查签名消息的成本提升,某些第三方钱包可能不会对签名信息进行解码展示,导致用户被攻击的风险更高
总结
总结来看, Permit2
协议看起来很好,也好像优化了用户体验,但是从实际代码分析下来看,只是一种风险转嫁,并且用户需要更加消息消息签名的内容,无形中增加了用户成本。