在本文的第一部分,我描述了一个案例:交换服务 rabbit.io 试图向客户发送少量 SOL,但交易不断被拒绝——尽管根据 Solana 区块链的已知规则,所有内容看起来都完全有效。
第一部分还涵盖了在 XRP Ledger、Stellar 和 Lightning 网络(比特币的二层)等网络中,技术上正确的交易仍可能被拒绝的情况。我还解释了 USDC 智能合约中黑名单的一个实现细节,这可能导致 USDC 交易意外失败,即使在发送方看来没有任何问题。
如果你错过了第 I 部分,可以在 这里 阅读。
今天我想看看几个涉及其他网络和代币的类似示例。在每一个示例中,乍一看一切都可能正确——但交易仍然失败。
这个案例针对 DeFi 协议的开发者和运营者,不过普通用户也会感受到其后果。
想象以下情形。你尝试向某个协议存入 USDC、DAI 和 USDT。使用 USDC 和 DAI 的存款正常,但 USDT 转账被回滚。地址正确、网络正确、gas 足够。原因是什么?
原因在于以太坊上的 USDT 并未完全遵守 ERC-20 标准。该标准要求函数 transfer 和 transferFrom 返回一个布尔值:成功返回 true,失败返回 false。而 USDT 不返回任何值。
如果某个智能合约严格按 ERC-20 标准编写,并期望在调用 transfer 时收到布尔值响应,而 USDT 不返回值,那么 EVM 虚拟机(使用 Solidity 从 0.4.22 版本起)会将此解释为错误并回滚该转账。技术上该交易本可以成功,但被强制回滚。发送方既不会从网络也不会从智能合约那里得到明确的错误原因。
这个例子与 rabbit.io 的兑换并无关系。我们的系统以最简单且最可靠的方式组织:你会收到一个地址并手动向其发送代币,作为回报你从我们这里收到所需代币。DeFi 智能合约并不涉及其中。
不过,对于那些不仅在 rabbit.io 上进行兑换还积极参与 DeFi 的读者——包括开发者——我可以建议一个简单的解决方案:使用 OpenZeppelin 的 SafeERC20 库,它可以正确处理非标准代币。
大多数专业编写的 DeFi 协议已经采用了这一做法。然而,较旧或业余的智能合约在与 USDT 交互时仍会出错。这个问题如此普遍,以至于被收录在已知代币怪癖的 weird-erc20 数据库中,你也可以在那里找到其他行为类似的代币。
以太坊上的 USDT 还有另一个特性,会定期导致与智能合约的交易失败。
在 ERC-20 标准中,如果你想允许智能合约花费你的代币,你会调用函数 approve(spender, amount)。例如,如果你想把授权额度从 100 增加到 200 USDC,只需调用 approve(spender, 200)。
USDT 的工作方式不同。它的代码明确规定:如果接收地址已经有非零的授权额度,则不允许直接设置为新的非零值。对几乎任何其他代币来说完全有效的一笔交易,在 USDT 上会被拒绝。
这种行为被引入以防止涉及授权额度的双重支付攻击。开发者担心攻击者可能会瞬间同时花掉旧的授权额度和新的授权额度。
因此,更改 USDT 授权额度的强制模式如下:
approve(spender, 0) 将授权重置为零approve(spender, new_value)对于去中心化应用的用户来说这可能很困惑。用户之前已经授予了某个应用授权,想设置不同的额度,签名了交易——但什么也没变,而且没有明显的解释。
这个例子同样与 rabbit.io 的兑换无关。此类被拒绝的交易不会在我们平台上发生,因为 rabbit.io 不要求连接钱包,也不请求代币支出的权限。
然而,如果在某个 dApp 中,你针对 USDT 的 approve 交易失败,通常的解决办法很简单:检查当前的授权额度。如果不是零,先将其重置为零,然后再设置新值。
在比特币网络中存在一个名为“尘埃”(dust)的概念。它指的是交易输出的数额小于日后花费该输出所需的费用。换言之,硬币在技术上存在于你的钱包中,但发送它们所需的手续费将大于这些硬币本身。
理解一个经常被混淆的根本区别很重要:共识规则与节点策略之间的差别。
根据比特币共识规则,输出低于尘埃限额的交易技术上可能是有效的。然而,大多数节点默认会拒绝转发此类交易,且大多数矿工会拒绝将其打包进区块。
原因很简单:存储极小的硬币会给数据库带来负担,却没有任何真实的经济收益。
尘埃限额取决于地址类型和最小中继费。历史上,尘埃限额被视为 546 satoshi。如果最小费率为 1 sat/vByte,低于此数的输出在手续费上会比发送金额更贵。但这只适用于以“1”开头的旧地址(legacy)。
更现代的地址类型在区块链中占用的空间更小,因此它们的尘埃限额可以更低。例如,对于常规的 bc1q... 地址,在费用率为 1 sat/vByte 时,尘埃限额为 294 satoshi;而在当前应用的最小中继费(0.1 sat/vByte)下,尘埃限额会小十倍。

想象以下场景。
你有 100,000 satoshi(0.001 BTC)。你想给某人发送 99,000 satoshi。你构造了一笔交易,网络手续费为 980 satoshi。剩下的 20 satoshi 应该作为找零返回给你。
但那不会发生。你既不会收到找零,主要的付款也不会到达接收方。中继节点网络会静默拒绝该交易,因为它的某个输出低于尘埃限额。
还有一个相关问题:日后花费尘埃。如果在费用较低时你收到许多小于 294 satoshi 的输出,而后来费用上升,任何试图花费这些硬币的交易可能会变得如此昂贵,以至于转出的金额甚至无法覆盖所需的费用。
因此,在将其他加密资产兑换为比特币时,请确保所创建的输出超过尘埃限额。Rabbit.io 允许进行高于当前尘埃限额数倍的兑换。然而,如果网络费用上升,这类小额可能会变得难以继续发送。
在以太坊以及任何兼容 EVM 的区块链(如 BSC、HyperEVM 等)中,每个从特定地址发送的交易都有一个序号,称为 nonce。
网络严格按顺序处理交易:先处理 nonce = 0 的交易,然后是 nonce = 1,接着是 2,以此类推。不能跳过某个序号。
如果某笔交易由于 gas 费用过低而在 mempool 中卡住,则来自同一地址的所有后续更高 nonce 的交易也会保持待处理状态,即使它们的费用正常。
这里有一个非常现实的场景。几周前你发送了一笔 gas 费较低的交易。它卡住了。你忘了这件事。现在你尝试发送一笔新交易。你填写了正确的地址并设置了足够的费用,但交易一直无法被处理。
问题并不在当前交易,而是在那笔未确认的旧交易。
解决方法是发送一笔与旧交易相同 nonce 但 gas 费更高的交易。这要么会加速原交易(加速操作),要么会取消它。后者发生在你用相同 nonce 向自己发送一笔转账并设置更高 gas 费的情况。
但请注意,并非所有钱包都允许用户控制 nonce 值。例如,像 Trust Wallet 和 Exodus 这样的流行钱包就不提供此功能。与此同时,许多人认为过时且不便的 MetaMask 实际上是支持该功能的。
TRON 网络有一种不同寻常的资源模型。交易消耗两种类型的资源,而不像其他网络那样只收取单一交易费用:
能量可以通过冻结 TRX 获得。如果不这么做,网络会自动消耗(销毁)TRX 来覆盖成本。
这就出现了一个意想不到的费用问题:
如果发送方只有 15 TRX 可用于手续费,但转账是发往一个新地址,那么交易可能会以 OUT OF ENERGY 错误失败。本次尝试中消耗的 TRX 不会退还。
换言之,情况再次类似于本文的其他示例:地址正确、网络正确、发送方持有 USDT、TRX 也有——但交易仍然失败。
结论很简单:为了稳定地处理 USDT TRC-20,建议余额中至少保留 27 TRX 来覆盖可能的交易成本。
那次 SOL 转账失败的故事给了我一个宝贵的教训。不是说我误解了区块链,而是认识到每条区块链都是其自身的生态系统,具有隐藏的规则,这些规则并不总是有清晰的文档,有时只有在不寻常的情况下才会显现出来。
每一次令人困惑的失败都不是沮丧的理由,而是学习新东西的机会。我学到的东西越多,继续从事加密货币工作对我来说就越有趣。
如果你也有同感,请记录不寻常的案例并分享你的发现。当实践经验传播得比官方文档更新更快时,加密行业会变得更加可靠。