USDT改授权为什么要发两笔交易:approve先归零规则的来龙去脉 图 1
USDT改授权为什么要发两笔交易:approve先归零规则的来龙去脉 · 图 1

用支持任何代币的去中心化应用存币或授权时,几乎所有人都踩过同一个坑:换成 USDT 就失败,换成 USDC 就正常,提示往往是 revert。问题不在你的余额,也不在手续费,而在这份代币合约对“修改授权”这件事写了一条额外规则。理解它,你才能看懂为什么有些应用面对 USDT 必须发两笔授权交易。

approve 的本义:一个可以直接覆盖的数字

ERC-20 给每个“持有人—花费方”组合维护一个授权额度数字。你调用 approve(spender, value),就是把这笔额度直接设成新值;应用随后用 transferFrom 在额度内划转。标准写法里,从一百改到五十、从五十改到八十,都是一笔交易完成的事。

麻烦出在“改额度”与“花额度”交错的时候。以太坊改进提案 EIP-20 的讨论区里早在 2016 年就记录过这个竞态问题:如果你对同一花费方先发一笔“把额度改成 60”,紧跟一笔“改成 40”,而花费方在这两笔之间塞进一笔 transferFrom,它就可能先花掉旧额度下的 50,再花掉新额度下的 40,合计超出你只想给的 60。标准代币对此没有硬性防御,靠使用者自己控制顺序。

USDT 的做法:不许直接从非零改到非零

USDT 在以太坊上的合约把防御写进了 approve 本身:如果新值非零、且当前额度也非零,交易直接回退。翻译成用户语言:想把授权从五十改成六十,必须先做一笔“改成零”,确认生效后再发“改成六十”。两笔交易之间,旧额度已经归零,花费方没有可钻的空档。这正是社区常说的先归零规则。

用户侧的表现因此分成两类。写得规范的应用会先查你当前的 allowance,发现非零就先发一笔置零,再设新额度,你看到的是连着两笔授权确认;写得粗糙的应用直接试图非零改非零,交易在合约层回退,钱包里显示失败。此时反复点重试没有意义,失败原因是确定性的,换浏览器、换节点都绕不过这条 require

正确的操作顺序

第一步,查现状。在区块浏览器打开该代币合约的只读函数 allowance,输入你的地址和花费方地址,读当前额度;钱包的授权管理页本质也是读这个值。

第二步,归零。若额度非零且你要改数,先发一笔 approve 把额度设为零,等待它在链上确认。撤销全部授权时这一笔就是终点。

第三步,设新值。确认额度已经显示为零,再发第二笔设成目标数字。若两笔之间应用发起了划转,它能用的最多是零额度,这正是等待确认的意义。

边界与容易混淆的点

先归零不是所有代币的硬要求,对普通 ERC-20 只是多加一笔交易的保险做法;合约库 OpenZeppelin 的 SafeERC20 提供了自动处理这类代币的 forceApprove 与增量改额度的 safeIncreaseAllowance,所以由规范库支撑的合约通常不会在你面前翻车。另外要分清:USDT 转不出去的原因有很多,Gas 不足、地址被发行方列入冻结名单都在其他层,先用错误提示和 allowance 读数定位,不要把锅都扣给这条规则。

反方向操作与成本直觉

从大额度收回小额度同样受这条规则约束:想把一百万的授权降到一千,先归零再设一千,两笔交易之间花费方暂时什么都动不了,这是特性而不是 bug。费用上,一笔 approve 与一笔普通转账量级相当,两笔授权只是多付一笔 Gas,与被盗风险相比可以忽略。有个省事的替代路径常被忽略:多数情况下你可以直接撤销(设为零)后,让应用在下一次操作时按它的默认流程重新请求授权,不必自己算中间值。跨链场景还要注意,以太坊主网、各 Layer2 上部署的 USDT 是不同地址的合约,这条规则写在合约代码里,个别链上的部署实现可能有差异,操作前对照该链区块浏览器里的合约源码确认,不要用主网的经验硬套。

修改授权涉及额度大小,保留无限授权的习惯在哪个代币上都等于把额度钥匙长期交出去,按需要授权、事后撤销始终是更稳的做法。本文为机制与操作说明,不构成任何投资建议。

延伸阅读:授权请求逐项怎么读,见 钱包弹出授权请求时,你在批准什么?合约、额度与有效期逐项读;批量撤销与生效确认见 代币授权怎么批量撤销?撤销交易的手续费、顺序与生效确认