跳到主要内容

ERC20 转账问题

ERC20 转账问题是智能合约中常见的漏洞来源。 这些问题源于 ERC20 标准实现的不一致性,特别是不同代币如何处理转账函数的返回值。

ERC20 规范规定,当转换成功时,ERC20 代币必须返回 true。 然而,并非所有 ERC20 代币都始终遵循这个规则,从而导致潜在的安全风险。

ERC20 transfer issues can occur when a contract assumes all ERC20 tokens behave identically. 一些代币在转账时不返回数值,一些总是返回 true,而另一些在失败时会回滚(revert),而不是返回 false。 如果不适当处理,这种行为上的差异可能会导致意外的结果。

更复杂的是,一些 ERC20 代币并不遵循返回 true 的协议。 值得注意的是,Tether (USDT)和其他一些代币在转换失败时会回滚,导致回滚传递到调用者。 为了解决这个问题,一些库封装了 ERC20 代币的转账调用,以拦截回滚并返回一个布尔值。 以下是Openzeppelin SafeTransfer 和Solady SafeTransfer的实现方式。

Consider a simplified TokenExchange contract that swaps one ERC20 token for another:

contract TokenExchange {
function swapTokens(IERC20 tokenA, IERC20 tokenB, uint256 amount) external {
require(tokenA.transferFrom(msg.sender, address(this), amount), "Transfer of token A failed");
require(tokenB.transfer(msg.sender, amount), "Transfer of token B failed");
}
}

在这个合约中,swapTokens方法存在ERC20转账问题漏洞。 It assumes that both transferFrom and transfer will return a boolean value, which isn't always the case.

防御机制​

安全转账库​

Using safe transfer libraries is an effective way to handle ERC20 transfer inconsistencies. These libraries wrap the transfer calls and handle different token behaviors. 以下是使用OpenZeppelin's的SafeERC20示例:

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract SafeTokenExchange {
using SafeERC20 for IERC20;

function safeSwapTokens(IERC20 tokenA, IERC20 tokenB, uint256 amount) external {
tokenA.safeTransferFrom(msg.sender, address(this), amount);
tokenB.safeTransfer(msg.sender, amount);
}
}

Solady 还提供了一种更节省 gas 的安全转账实现方式:

import "solady/src/utils/SafeTransferLib.sol";

contract GasEfficientTokenExchange {
function safeSwapTokens(address tokenA, address tokenB, uint256 amount) external {
SafeTransferLib.safeTransferFrom(tokenA, msg.sender, address(this), amount);
SafeTransferLib.safeTransfer(tokenB, msg.sender, amount);
}
}

低级调用与返回值检查​

对于无法使用外部库的合约,可以实现低级调用与返回值检查:

function saferTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
(bool success, bytes memory data) = address(token).call(
abi.encodeWithSelector(IERC20.transfer.selector, to, value)
);
return success && (data.length == 0 || abi.decode(data, (bool)));
}

通过遵循这些实践,智能合约开发者可以显著降低 ERC20 转账问题的风险,并确保在与各种 ERC20 代币交互时合约的安全性。