漏洞解析:
如果是某个合约采用了ZRX代币作为交易的代币,例如某个交易所合约实现借贷转账ZRX代币,那么使用ZRX代币的transfer则会返回false或者是true。(ZRX代币没有严格按照ERC20规范)
这个时候就需要交易所合约自己判断以决定后续的动作(例如更新余额),可能会有疏漏或者考虑不周的点。
因此如果使用旧版ERC20规范的代币就需要在合约里进行特殊处理,但是如果遵循了ERC20规范的代币,在遇到transfer fail的时候,则会自动回滚,而不是返回false或者true让处理。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/*
Name: No Revert on Failure
Description:
Some tokens do not revert on failure, but instead return false (e.g. ZRX).
ZRX transfer return false:
function transfer(address _to, uint _value) returns (bool) {
//Default assumes totalSupply can't be over max (2^256 - 1).
if (balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]) {
balances[msg.sender] -= _value;
balances[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;
} else { return false; }
}
Mitigation:
Use OpenZeppelin’s SafeERC20 library and change transfer to safeTransfer.
*/
contract ContractTest is Test {
using SafeERC20 for IERC20;
IERC20 constant zrx = IERC20(0xE41d2489571d322189246DaFA5ebDe1F4699F498);
function setUp() public {
vm.createSelectFork("mainnet", 16138254);
}
function testTransfer() public {
vm.startPrank(0xef0DCc839c1490cEbC7209BAa11f46cfe83805ab);
zrx.transfer(address(this), 123); //return false, do not revert
vm.stopPrank();
}
function testSafeTransferFail() public {
vm.startPrank(0xef0DCc839c1490cEbC7209BAa11f46cfe83805ab);
// https://github.com/foundry-rs/foundry/issues/5367 can't vm.expectRevert
// vm.expectRevert("SafeERC20: ERC20 operation did not succeed");
zrx.safeTransfer(address(this), 123); //revert
vm.stopPrank();
}
receive() external payable {}
}
可以看到zrx代币里只实现了transfer方法,但是引入了SafeERC20这个库,所有遵循ERC20标准的代币都可以使用 SafeERC20 库。
SafeERC20 库的设计目的是为了增强和安全化对ERC20代币的操作,特别是处理转账操作
functiontransfer(address _to, uint _value) returns (bool) {
//Default assumes totalSupply can't be over max (2^256 - 1).
if(balances[msg.sender] >= _value && balances[_to] + _value >= balances[_to]) {
balances[msg.sender] -= _value;
balances[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;
} else { return false; }
} //返回了false
利用_callOptionalReturn来对交易失败时进行回滚
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value)));
}
可以看到一个返回了revert 一个返回了False
// 危险版本(使用transfer)
function withdraw(address user, uint amount) public {
zrx.transfer(user, amount); // 如果失败返回false但继续执行
balances[user] -= amount; // 即使转账失败也会扣余额!
}
// 安全版本(使用safeTransfer)
function safeWithdraw(address user, uint amount) public {
zrx.safeTransfer(user, amount); // 失败则完全回滚
balances[user] -= amount; // 只有成功时才执行
}
原文始发于微信公众号(Ice ThirdSpace):DeFiVulnLabs靶场全系列详解(二十六)交易失败时不回滚
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论