授权方式的更迭:从ERC20到Permit2

admin 2026-02-10 14:37:11 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文解析了代币授权机制的演进,对比传统ERC20的高风险与EIP-2612的兼容性局限,重点阐述了UniswapPermit2方案。Permit2通过全链统一的中间层合约实现旧资产兼容、引入授权过期机制并利用位图技术优化Gas,提升了体验与安全性,但需警惕链下签名钓鱼等新型风险。 综合评分: 90 文章分类: 区块链安全,漏洞分析,解决方案


cover_image

授权方式的更迭: 从 ERC20 到 Permit2

原创

ChainSecLabs ChainSecLabs

ChainSecLabs

2026年2月8日 17:02 四川

区块链安全相关的内容

我们将在这里分享一些

Hi,这里是ChainSecLab

区块链安全相关的内容

我们将在这里分享一些

Hi,这里是ChainSec

区块链安全相关的内容

我们将在这里分享一些

Hi,这里是ChainSecLabs!

1

传统授权方式: ERC20

相信每一位 DeFi 用户都经历过:当你希望在一个 DEX 上交换两种代币的时候,第一个要进行的操作并不是交易,而是 授权(approve)。这就是传统的ERC20标准的解决方案,即让代币合约维护如下的一个二维映射:

mapping(address => mapping(address => uint256)) private _allowances;//       Owner     =>    Spender   =>  Amount

这种方式使得交易按照如下步骤进行:

  1. 用户期望进行交易。Owner 调用token.approve(…) ,授权 Spender(通常是资金池或 DApp) 一定数额的代币。

这个数额往往难以精确计算,相信你在靶场练习时都是直接授权type(uint256).max 的,大部分 DEX/DApp 也会这么请求,这就导致导致了合约一旦被攻破,进行无限授权的用户会蒙受巨大的损失。一个经典的案例为 LI.FI 协议攻击。

2. Onwer 与 Spender 交互(可以是调用 swap 相关函数或 DApp 上的一些操作)。

3. Spender 调用 token.transferFrom(…) 将代币转过来。

4. Spender 进行剩余操作。

显然,这样很麻烦。如果用户不在交易完成后进行token.approve(spender, 0),那么无限额度的授权将一直存在;如果用户每次都收回授权,那再次交易又要重新授权,gas 消耗蹭蹭往上涨。

2

并不完美的改变: EIP-2612

      为了解决 ERC20 的上述问题,EIP-2612 诞生了,它在 ERC20 的基础上要求代币合约新增一个 permit() 函数及两个验证用的函数:

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;function nonces(address owner) external view returns (uint);function DOMAIN_SEPARATOR() external view returns (bytes32);

这使得用户不再需要进行链上的 approve(),而是进行链下签名,并且这份签名是有时效性的。

注意:deadline 仅仅意味着签名的时效性,而非 allowance 的时效性。

这使得整个交易流程逻辑变成了:

1.

  • 用户期望进行交易,网站前端向钱包发送一次签名请求,并得到签名:
digest = keccak256(    "\\x19\\x01" ||    DOMAIN_SEPARATOR ||    keccak256(encode(Permit)))

其中,Permit 如下:

Permit(    address owner,    address spender,    uint256 value,    uint256 nonce,    uint256 deadline)

DOMAIN_SEPARATOR 是 EIP-712 规定的:

EIP712Domain(    string  name,    string  version,    uint256 chainId,    address verifyingContract)

随后网站解析签名得到 (v, r, s)。整个过程是完全链下的,主要由网站实现,用户只需要许可签名即可。

  • Spender 调用 token.permit(…),由该函数进行验证及授权。

2. Spender 调用 token.transferFrom(…) 进行转账,并进行剩余操作。

显而易见,这样做极大地简化了用户的操作逻辑,gas 费也几乎不需要用户承担。这看起来是一个完美的方案,应该会被广泛运用…对吗?事实上,这个方案有一个巨大的缺陷——它必须对原有代币合约进行升级。而以太坊上流动性最好的资产——USDT, WETH 等,都是在 EIP-2612 诞生前部署的。因此这些主流资产无法支持 EIP-2612,它也就无法成为一个彻底的解决方案。

3

统一解决方案的建立:Permit2

在两位“ 前辈 ”的基础上,Uniswap  推出了  Permit2 ,一个独立于代币合约之外的、全链统一的规范合约。老的代币不需要升级,用户也不需要对各个交互对象多次授权。现在,用户只需要将额度授权给Permit2 合约,它会在需要时向用户请求签名以验证身份,并将对应数额的代币转账给接收方。

规范合约意味着 Permit2 在所有主流链上的地址是一致的。(0x000000000022D473030F116dDEE9F6B43aC78BA3)

4

Permit2 的两种授权方式

Permit2提供两种授权方式:AllowanceTransfer和SignatureTransfer。

  • AllowanceTransfer 类似于改进版的 ERC20 Approve,允许用户设置一个带有过期时间的授权。虽然你授权给 Permit2 合约的额度不会消失,但是它会阻止超出过期时间的转账。

  • SignatureTransfer 是一种原子化签名模式,使得一次签名只能 Spender 被用来提取指定数额的代币一次。

5

Permit2 的交互逻辑

虽然授权方式有两种,但是整体的交互逻辑一样:

1.

  • 用户期望交易,并及进行签名。

  • Owner 调用 token.approve(…) ,授权 Permit2 合约一定数额的代币(只要数额还够,这一步就不用重复执行)。

  1. 用户与Spender方交易,Spender  调用Permit2.permitTransferFrom(…)。

3. Permit2 验证,并调用以下转账代码实现转账。

token.transferFrom(…)

4. Spender 进行剩余操作。

6

Permit2 的核心实现

Permit2合约继承了两个核心抽象AllowanceTransfer.sol和SignatureTransfer.sol 这里以单笔交易部分为例。

1. AllowanceTransfer.sol

 该合约定义了两个核心的交易相关的、一个存储用结构体:

// 链上存储的授权状态,与 PermitDetails 对应struct PackedAllowance {    uint160 amount;    uint48 expiration;    uint48 nonce;}struct PermitDetails {    address token;             // ERC-20 合约地址    uint160 amount;            // 授权最大额度    uint48 expiration;         // 授权过期时间     uint48 nonce;}struct PermitSingle {    PermitDetails details;    address spender;            // 接收人    uint256 sigDeadline;        // 签名过期时间}

以及一个核心映射:

mapping(address => mapping(address => mapping(address => PackedAllowance))) public allowance;//       owner  =>      token      =>      spender    =>  PackedAllowance

其主要函数方面与 EIP-2612 基本对应,如:

function approve(address token, address spender, uint160 amount, uint48 expiration) external;function permit(address owner, PermitSingle memory permitSingle, bytes calldata signature) external;function transferFrom(address from, address to, uint160 amount, address token) external;

此外还提供了:

  • function   lockdown  (TokenSpenderPair[] calldata approvals) external; 用于批量撤销授权。其中,TokenSpenderPair 如下:
struct TokenSpenderPair {    address token;    address spender;}
  • function   invalidateNonces  (address token, address spender, uint48 newNonce) external; 用于作废并设置新的 Nonce(必须大于等于旧的 Nonce)。

2. SignatureTransfer.sol

该合约定义了三个核心的交易相关的结构体:

// 单笔交易的授权信息struct TokenPermissions {    address token;                           // ERC-20 合约地址    uint256 amount;                          // 授权最大额度}// 单笔授权信息struct PermitTransferFrom {    TokenPermissions permitted;    uint256 nonce;    uint256 deadline;                        // 签名有效期}// 接收方信息    struct SignatureTransferDetails {    address to;                              // 接收人    uint256 requestedAmount;                 // spender 实际请求转账数量}

以及一个核心 Nonce 位图:

mapping(address => mapping(uint256 => uint256)) public nonceBitmap;//       Owner    =>    wordIndex  =>  bitIndex

位图核心逻辑(有修改)为:

function&nbsp;_useUnorderedNonce(address from, uint256 nonce)&nbsp;internal {&nbsp; &nbsp;&nbsp;uint256&nbsp;wordPos&nbsp;=&nbsp;nonce >>&nbsp;8; &nbsp; &nbsp; &nbsp;&nbsp;// 取高 248 位&nbsp; &nbsp;&nbsp;uint256&nbsp;bitPos&nbsp;=&nbsp;uint8(nonce); &nbsp; &nbsp; &nbsp;// 取低 8 位&nbsp; &nbsp;&nbsp;uint256&nbsp;bit&nbsp;=&nbsp;1&nbsp;<< bitPos; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 相当于 2 ** bitPos&nbsp;&nbsp; &nbsp;&nbsp;uint256&nbsp;flipped&nbsp;=&nbsp;nonceBitmap[from][wordPos] ^= bit;&nbsp; &nbsp;&nbsp;if&nbsp;(flipped & bit ==&nbsp;0) revert&nbsp;InvalidNonce();}

      在更新nonceBitmap[from][wordPos] 的同时,计算nonceBitmap[from][wordPos] ^ bit & bit,如果 nonceBitmap[from][wordPos] 是 0,说明当前位没用过,结果为 bit;如果是 1,说明当前位已经用过,结果为 0。

     相比于 AllowanceTransfer 的 Nonce,这里的 Nonce 事实上可以无序使用,即用户可以生成 nonce 1 和 nonce 100 的签名,先执行哪个都可以。而如果使用简单的 mapping(address => mapping(uint256 => bool)) usedNonce; 会导致一个 nonce 对应一个 bool,占用一个 slot,增加 gas 消耗。

回到转账本身,它的转账入口为:

function&nbsp;permitTransferFrom(&nbsp; &nbsp; PermitTransferFrom memory permit, &nbsp;&nbsp;&nbsp; &nbsp; TransferDetails memory transferDetails,&nbsp; &nbsp; address owner, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// 签名者&nbsp; &nbsp; bytes calldata signature &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// EIP-712 签名) external;

       除开金额、nonce、签名的验证,它最后会调用以下的转账代码进行转账。

token.transferFrom (owner,transferDetails.to,transferDetails.requestedAmount)

7

Permit2 的安全注意

由于 Permit2 合约经过审计且被普遍认为是安全的,可以认为它解决了过去 approve 带来的一系列安全隐患,但同时也带来了新的风险。这种风险不再来源于链上,而更多来源于链下,如伪造假 DEX 网站,将 Spender 指向攻击者钱包地址而非 router 合约,等等。

8

总结

     从传统 ERC20 的频繁链上操作(导致高 Gas 成本与无限授权风险),到 EIP-2612 尝试引入签名授权(却受限于对旧代币的兼容性),直到演进至 Uniswap Permit2,授权方式几经更迭。最终,Permit2 通过建立一个全链统一的中间层合约,实现了对旧资产的向下兼容、引入了授权过期机制,并采用位图技术优化 gas 效率,它在显著提升用户交互体验与安全性的同时,也将安全防范的核心从合约漏洞审计转向了对链下签名钓鱼攻击的警惕。

作者:victifa

编辑:Legend

审核:Chloe


参考文献:

  • ERC20

https://eips.ethereum.org/EIPS/eip-20

  • EIP-712

https://eips.ethereum.org/EIPS/eip-712

  • EIP-2612

https://eips.ethereum.org/EIPS/eip-2612

  • Uniswap Permit2

https://github.com/Uniswap/permit2/tree/main

往期推荐

01>>   主流跨链协议攻击面


02>>   风起于青萍之末


03>>   惊了!Paxos 竟意外铸出 300 万亿稳定币!


04>>  跨链mev解析


搜索公众号  关注我们

「ChainSecLabs」

免责声明

      本文章旨在分享安全技术相关知识与经验,内容仅供学习与研究参考。文中所提及的技术手段、工具或操作流程,均基于公开资料与作者个人理解,不代表任何官方立场,也不构成对读者的具体操作建议。

      请勿将本文所述技术用于任何非法用途,否则后果自负。作者严禁并坚决反对一切网络攻击、非法入侵、数据窃取等违法行为,且不承担因读者不当使用文章内容而引发的任何直接或间接责任。

      若文章中引用了第三方工具或资料,版权归原作者所有,若有侵权或不妥之处,请及时联系,我们将第一时间予以处理。

      网络安全关乎法律与伦理,请读者在合法合规的前提下,自主学习、合理应用。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:ChainSecLabs ChainSecLabs ChainSecLabs《授权方式的更迭: 从 ERC20 到 Permit2》

评论:0   参与:  4