Permit2 风险分析

前言

Uniswap 新发布了新的代币授权标准 Permit2,区别于传统的 ERC20 permitPermit2 可以让用户无需在和不同的 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 协议都是依赖这个函数进行所谓的授权管理。首先,我们观察参数结构。参数中存在变量 ownerpermitSingle 结构体变量及一个签名参数 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 和 nonceexpiration 顾名思义,就是指的是本次授权的有效时间范围,而由于签名验证中验证的是 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 完整的流程应该是 

  1. 用户将 ERC20 代币的最大授权给到 Permit2 合约

  2. 用户通过 permit 函数对 Permit2 合约中的具体授权进行管理

  3. 第三方协议和用户可以根据 Permit2 中已有的授权信息通过 Permit2 合约作为中间人实现代币的转移。

所以 Permit2 是更好的授权模型吗?

在理解了授权原理之后,我们可以发现 Permit2 协议具有的优点

  1. 统一的代币管理

  2. 可控制的授权时间

  3. 不用每次都多发一笔交易来进行授权

但其实问题也是存在

  1. 号称解决了 infinity approval 的问题,但实际只是把授权对象从需要交互的 DApp 转变成了 Permit2 合约,只是风险转移。用户还是需要无限授权。第三方接入 Permit2 协议的时候,可能无法保证安全

  2. 虽然说代币授权有过期时间,但是这个时间依旧可以无限大,导致其实和无限授权的结果是差不多的,显得很鸡肋

  3. 由于调用 permit 函数的过程可以不发送交易而是只提供签名给第三方代发,如果是做钓鱼的话可以做得更加隐蔽,用户检查签名消息的成本提升,某些第三方钱包可能不会对签名信息进行解码展示,导致用户被攻击的风险更高

总结

总结来看, Permit2 协议看起来很好,也好像优化了用户体验,但是从实际代码分析下来看,只是一种风险转嫁,并且用户需要更加消息消息签名的内容,无形中增加了用户成本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值