一句话版本
在以太坊上,让一个 DeFi 应用"使用"你的代币,需要先签一笔 approve,授权它从你的地址转走最多 X 个代币。为了省事,绝大多数应用默认请求无限额授权,而且这份授权永不过期。
只要那个合约日后出了漏洞、被升级成恶意版本、或者你当初授权的根本就是钓鱼合约,你钱包里的这种代币就可以被随时转走,不需要你再签任何东西。所以要做两件事:授权时只给精确额度,以及定期检查并撤销不用的授权。
全文分三层:
- 通俗理解:签了一张不限额、不过期的委托书
- 实际用处:真实事件,以及怎样检查和撤销
- 原理与工程要点:
approve/transferFrom的机制、calldata 解码、精确额度
第一层:通俗理解
1.1 一张不限额、不过期的委托书
你想用一个交易所兑换 100 USDC。流程是两步:
- 授权:告诉 USDC 合约"允许这个交易所合约从我这里转走 USDC";
- 兑换:交易所合约把你的 100 USDC 转走,换给你别的代币。
问题在第 1 步。为了让你下次兑换时不用再授权一次,应用通常请求的额度是无限。这相当于签了一张委托书:"此人可以随时从我的账户里转走任意数量的 USDC,永久有效。"
1.2 什么时候出事
| 情况 | 结果 |
|---|---|
| 被授权的合约有漏洞 | 攻击者利用漏洞,以合约身份把所有授权过的用户的钱转走 |
| 合约是可升级的,管理员私钥被盗 | 升级成恶意版本,同上 |
| 你当初授权的就是钓鱼合约 | 攻击者想什么时候转就什么时候转 |
可怕之处在于:授权可能是几年前签的,你早就忘了。
第二层:实际用处
- 跨链聚合器事件:2024 年,LI.FI、Socket 等跨链聚合器的合约先后出现漏洞,攻击者利用用户此前给它们的无限额授权,直接从用户钱包里转走了数百万到上千万美元。受害者在事发当天什么都没做。
- Badger DAO(2021):前端被注入恶意脚本,诱导用户对攻击者地址做授权,之后批量转走,损失约 1.2 亿美元。
- NFT 的
setApprovalForAll:一签就是"我这个系列的所有 NFT 都交给你处置"。大量 NFT 钓鱼就是诱导用户签这个。
怎么检查和撤销:
- Etherscan 的 Token Approval Checker、revoke.cash 等工具,可以列出你地址上所有仍然有效的授权;
- 撤销就是再签一笔交易,把额度设为 0(需要花一点 gas);
- 养成习惯:用完一个不常用的应用,就撤销它的授权。
Caloogy Wallet 也提供授权检查和一键撤销。
第三层:原理与工程要点
3.1 ERC-20 的授权机制
ERC-20 代币合约内部维护一张表 allowance[owner][spender]:
function approve(address spender, uint256 amount) returns (bool); // owner 调用:设置额度
function transferFrom(address from, address to, uint256 amount); // spender 调用:在额度内转走 from 的代币
approve之后,spender 可以多次调用transferFrom,每次从额度里扣;- "无限额"就是把 amount 设成
uint256的最大值 。很多代币合约对这个值做了特殊处理:转账时不扣减额度,真正意义上的无限。
3.2 从 calldata 认出一笔授权
钱包发出的交易里,data 字段的前 4 个字节是函数选择器,等于函数签名 Keccak-256 哈希的前 4 字节:
| 函数 | 选择器 |
|---|---|
approve(address,uint256) |
0x095ea7b3 |
transfer(address,uint256) |
0xa9059cbb |
一笔无限额授权的 calldata 长这样:
0x095ea7b3
000000000000000000000000 <spender 地址 20 字节>
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
看到 095ea7b3 后面跟着一整串 f,就是无限额授权。(选择器已用 Python 验算。)
3.3 工程上的防护
| 措施 | 说明 |
|---|---|
| 默认精确额度 | 应用请求无限额时,钱包把额度改成本次实际需要的数量(MetaMask 已经支持用户手动修改) |
| 识别所有授权类调用 | approve、increaseAllowance、setApprovalForAll,以及 Permit / Permit2 签名(见《盲签与所见即所签》) |
| 授权对象检查 | spender 是不是已知合约?是不是普通地址(普通地址作为 spender 几乎一定是钓鱼)? |
| 定期扫描 | 列出地址上所有仍然有效的授权,标出"无限额""对象未知""很久没用" |
| 撤销是低风险操作 | 撤销只会让账户更安全,风控上可以给最低风险等级,减少用户撤销的阻力 |
| 上链核对 | 授权列表若来自第三方索引服务,展示前用 allowance(owner, spender) 直接在链上读一次当前额度 |
3.4 新的方向
- Permit2 的过期时间:授权可以带过期时间,过期自动失效;
- 账户抽象 / 智能合约钱包:可以把"每次只授权本次所需"甚至"批量交易里授权和使用一起完成"做成默认行为(见《EOA与智能合约钱包》)。
总结
| 层次 | 要点 |
|---|---|
| 直觉 | 无限额授权 = 不限额、不过期的委托书 |
| 风险 | 被授权合约出问题,你什么都不做也会被转走 |
| 识别 | 0x095ea7b3 + 一串 f;还有 setApprovalForAll 和 Permit 签名 |
| 防护 | 默认精确额度;定期检查;用完就撤销 |