Introduction
At around 13:00 on January 10, the token project BRA (https://bscscan.com/token/0x449fea37d339a11efe1b181e5d5462464bba3752) was attacked. The attacker exploited the tax-sharing loophole in the token transfer process and increased circulation 1.6 million BRA tokens out of thin air and sold them, with a total profit of about 310,000 US dollars.
Background
Analysis
Take the attack transaction https://bscscan.com/tx/0x4e5b2efa90c62f2b62925ebd7c10c953dc73c710ef06695eac3f36fe0f6b9348 as an example:
Flash Loan — Get WBNB
The attacker first calls the attack contract (0x6066435edce9c2772f3f1184b33fc5f7826d03e7
), and the attack contract calls the flashLoan
function of DPPAdvanced(0x0fe261aee0d1c4dfddee4102e82dd425999065f4) to perform a flash loan from DPPAdvanced and borrow 1000WBNB
https://miro.medium.com/v2/resize:fit:1400/0*w4FQ_XKvVM714H7v
Token Swap — BNB to BRA
The attacker starts the flash loan attack through DPPFlashLoanCall. The attack contract holds 1000BNB, first withdraws from the WBNB contract, and swap WBNB to BNB
https://miro.medium.com/v2/resize:fit:1400/0*5b_dLd8uYh3wJuzt
The attack contract calls swapExactETHforTokens of pancakeswap to exchange 1000 BNB for BRA
https://miro.medium.com/v2/resize:fit:1400/0*2RUs2DvoQo5H65uN
1.Among them, the exchange path is WBNB=>BUSD=>BRA, which is 0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c=>0x55d398326f99059ff775485246999027b3197955=>0x449fea37d35639a11efe1b482
2.In this process, the BUSD-BRA pool data before the transaction is:
44233 BUSD-99921 BRA.
The user exchanged 272015 BUSD for BRA through 1000BNB.
Normally, he should get 85915 BRA, but in fact, the attack contract only got 83337 BRA, because the BRA token will be taxed during the transfer process:
1function _transfer(address sender, address recipient, uint amount) internal {
2 // sender=BUSD-BRA
3 // recipient=attack contract
4 // amount=85915 BRA
5 require(sender != address(0), "BEP20: transfer from the zero address");
6
7 bool recipientAllow = ConfigBRA(BRA).isAllow(recipient);
8 // recipientAllow=0
9 bool senderAllowSell = ConfigBRA(BRA).isAllowSell(sender);
10 // senderAllowSell=0
11 uint BuyPer = ConfigBRA(BRA).BuyPer();
12 // BuyPer=300
13 uint SellPer = ConfigBRA(BRA).SellPer();
14 // SellPer=300
15 address BuyTaxTo_ = address(0);
16 address SellTaxTo_ = address(0);
17
18 _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance");
19 // _balance[BUSD-BRA] = _balance[BUSD-BRA](99921)-85915=14006
20 uint256 finalAmount = amount;
21 uint256 taxAmount = 0;
22
23 if(sender==uniswapV2Pair&&!recipientAllow){
24 taxAmount = amount.div(10000).mul(BuyPer);
25 // taxAmount=(85915/10000)*300=85915*3%=2577
26 BuyTaxTo_ = ConfigBRA(BRA).BuyTaxTo();
27 // BuyTaxTo_=BUSD-BRA
28 }
29
30 if(recipient==uniswapV2Pair&&!senderAllowSell){
31 taxAmount = amount.div(10000).mul(SellPer);
32 SellTaxTo_ = ConfigBRA(BRA).SellTaxTo();
33 }
34
35 finalAmount = finalAmount - taxAmount;
36
37 if(BuyTaxTo_ != address(0)){
38 _balances[BuyTaxTo_] = _balances[BuyTaxTo_].add(taxAmount);
39 // _balance[BUSD-BRA] = _balance[BUSD-BRA](14006)+2577=16583
40 emit Transfer(sender, BuyTaxTo_, taxAmount);
41 }
42
43 if(SellTaxTo_ != address(0)){
44 _balances[SellTaxTo_] = _balances[SellTaxTo_].add(taxAmount);
45 emit Transfer(sender, SellTaxTo_, taxAmount);
46 }
47
48 _balances[recipient] = _balances[recipient].add(finalAmount);
49 // _balance[attack contract]=_balances[recipient](0)+83337
50
51 if (recipient == address(0) ) {
52 totalBurn = totalBurn.add(amount);
53 _totalSupply = _totalSupply.sub(amount);
54 emit Burn(sender, address(0), amount);
55 }
56 emit Transfer(sender, recipient, finalAmount);
57
58 }
1function _transfer(address sender, address recipient, uint amount) internal {
2 // sender=BUSD-BRA
3 // recipient=attack contract
4 // amount=85915 BRA
5 require(sender != address(0), "BEP20: transfer from the zero address");
6
7 bool recipientAllow = ConfigBRA(BRA).isAllow(recipient);
8 // recipientAllow=0
9 bool senderAllowSell = ConfigBRA(BRA).isAllowSell(sender);
10 // senderAllowSell=0
11 uint BuyPer = ConfigBRA(BRA).BuyPer();
12 // BuyPer=300
13 uint SellPer = ConfigBRA(BRA).SellPer();
14 // SellPer=300
15 address BuyTaxTo_ = address(0);
16 address SellTaxTo_ = address(0);
17
18 _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance");
19 // _balance[BUSD-BRA] = _balance[BUSD-BRA](99921)-85915=14006
20 uint256 finalAmount = amount;
21 uint256 taxAmount = 0;
22
23 if(sender==uniswapV2Pair&&!recipientAllow){
24 taxAmount = amount.div(10000).mul(BuyPer);
25 // taxAmount=(85915/10000)*300=85915*3%=2577
26 BuyTaxTo_ = ConfigBRA(BRA).BuyTaxTo();
27 // BuyTaxTo_=BUSD-BRA
28 }
29
30 if(recipient==uniswapV2Pair&&!senderAllowSell){
31 taxAmount = amount.div(10000).mul(SellPer);
32 SellTaxTo_ = ConfigBRA(BRA).SellTaxTo();
33 }
34
35 finalAmount = finalAmount - taxAmount;
36
37 if(BuyTaxTo_ != address(0)){
38 _balances[BuyTaxTo_] = _balances[BuyTaxTo_].add(taxAmount);
39 // _balance[BUSD-BRA] = _balance[BUSD-BRA](14006)+2577=16583
40 emit Transfer(sender, BuyTaxTo_, taxAmount);
41 }
42
43 if(SellTaxTo_ != address(0)){
44 _balances[SellTaxTo_] = _balances[SellTaxTo_].add(taxAmount);
45 emit Transfer(sender, SellTaxTo_, taxAmount);
46 }
47
48 _balances[recipient] = _balances[recipient].add(finalAmount);
49 // _balance[attack contract]=_balances[recipient](0)+83337
50
51 if (recipient == address(0) ) {
52 totalBurn = totalBurn.add(amount);
53 _totalSupply = _totalSupply.sub(amount);
54 emit Burn(sender, address(0), amount);
55 }
56 emit Transfer(sender, recipient, finalAmount);
57
58 }
3.In this way, after the exchange is completed, the pool changes from 44233 BUSD-99921 BRA to 316249 BUSD-16583 BRA, and the attacker has 1,000 BNB in his hand, which becomes 272015 BUSD, and finally becomes 83337 BRA. Some state variables are as follows:
balance0=16583,balance1=316249,reserve0=16583,reserve1=316249
The core attack point — transfer to the liquidity pool
The attack contract directly transfers the 83337 BRA it holds to the liquidity pool through transfer. Of course, this transfer process also needs to collect taxes, but after the tax is collected, the tax flow is also the liquidity pool, so in fact, 83337 BRA was transferred to the liquidity pool in full.
The core attack point — call skim to the liquidity pool itself, resulting in increased circulation
The attack contract calls the skim
function to carry out the core step of the attack. The to
parameter input of the attacker in the skim function is the liquidity pool itself, so this skim is actually a transfer from the liquidity pool to the liquidity pool itself.
1function skim(address to) external lock {
2 address _token0 = token0; // gas savings
3 address _token1 = token1; // gas savings
4 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
5 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
6}
1function skim(address to) external lock {
2 address _token0 = token0; // gas savings
3 address _token1 = token1; // gas savings
4 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
5 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
6}
-
Since in the skim function at this time, IERC20(_token0).balanceOf (address(this)). sub(reserve0)) is not 0 (the reason is that the attacker transferred 83337 to the liquidity pool), therefore, the liquidity pool will call safeTransfer
to the liquidity pool itself, but this behavior, from the perspective of BRA’s transfer process, is both buying and selling tokens. Therefore, the current transfer execution process is as follows:
1function _transfer(address sender, address recipient, uint amount) internal {
2 // sender=BUSD-BRA
3 // recipient=BUSD-BRA
4 // amount=83337 BRA
5 require(sender != address(0), "BEP20: transfer from the zero address");
6
7 bool recipientAllow = ConfigBRA(BRA).isAllow(recipient);
8 // recipientAllow=0
9 bool senderAllowSell = ConfigBRA(BRA).isAllowSell(sender);
10 // senderAllowSell=0
11 uint BuyPer = ConfigBRA(BRA).BuyPer();
12 // BuyPer=300
13 uint SellPer = ConfigBRA(BRA).SellPer();
14 // SellPer=300
15 address BuyTaxTo_ = address(0);
16 address SellTaxTo_ = address(0);
17
18 _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance");
19 // _balance[BUSD-BRA] = _balance[BUSD-BRA](99921)-83337=16583
20 uint256 finalAmount = amount;
21 uint256 taxAmount = 0;
22
23 if(sender==uniswapV2Pair&&!recipientAllow){
24 taxAmount = amount.div(10000).mul(BuyPer);
25 // taxAmount=(83337/10000)*300=83337*3%=2500
26 BuyTaxTo_ = ConfigBRA(BRA).BuyTaxTo();
27 // BuyTaxTo_=BUSD-BRA
28 }
29
30 if(recipient==uniswapV2Pair&&!senderAllowSell){
31 taxAmount = amount.div(10000).mul(SellPer);
32 // taxAmount=(83337/10000)*300=83337*3%=2500
33 SellTaxTo_ = ConfigBRA(BRA).SellTaxTo();
34 // SellTaxTo_=BUSD-BRA
35 }
36
37 finalAmount = finalAmount - taxAmount;
38 //finalAmount = 83337-2500=80837
39
40 if(BuyTaxTo_ != address(0)){
41 _balances[BuyTaxTo_] = _balances[BuyTaxTo_].add(taxAmount);
42 // _balance[BUSD-BRA] = _balance[BUSD-BRA](16583)+2500=19083
43 emit Transfer(sender, BuyTaxTo_, taxAmount);
44 }
45
46 if(SellTaxTo_ != address(0)){
47 _balances[SellTaxTo_] = _balances[SellTaxTo_].add(taxAmount);
48 // _balance[BUSD-BRA] = _balance[BUSD-BRA](19083)+2500=21583
49 emit Transfer(sender, SellTaxTo_, taxAmount);
50 }
51
52 _balances[recipient] = _balances[recipient].add(finalAmount);
53 // _balance[BUSD-BRA]=_balances[BUSD-BRA](21583)+80837=102421
54
55 if (recipient == address(0) ) {
56 totalBurn = totalBurn.add(amount);
57 _totalSupply = _totalSupply.sub(amount);
58 emit Burn(sender, address(0), amount);
59 }
60 emit Transfer(sender, recipient, finalAmount);
61
62 }
1function _transfer(address sender, address recipient, uint amount) internal {
2 // sender=BUSD-BRA
3 // recipient=BUSD-BRA
4 // amount=83337 BRA
5 require(sender != address(0), "BEP20: transfer from the zero address");
6
7 bool recipientAllow = ConfigBRA(BRA).isAllow(recipient);
8 // recipientAllow=0
9 bool senderAllowSell = ConfigBRA(BRA).isAllowSell(sender);
10 // senderAllowSell=0
11 uint BuyPer = ConfigBRA(BRA).BuyPer();
12 // BuyPer=300
13 uint SellPer = ConfigBRA(BRA).SellPer();
14 // SellPer=300
15 address BuyTaxTo_ = address(0);
16 address SellTaxTo_ = address(0);
17
18 _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance");
19 // _balance[BUSD-BRA] = _balance[BUSD-BRA](99921)-83337=16583
20 uint256 finalAmount = amount;
21 uint256 taxAmount = 0;
22
23 if(sender==uniswapV2Pair&&!recipientAllow){
24 taxAmount = amount.div(10000).mul(BuyPer);
25 // taxAmount=(83337/10000)*300=83337*3%=2500
26 BuyTaxTo_ = ConfigBRA(BRA).BuyTaxTo();
27 // BuyTaxTo_=BUSD-BRA
28 }
29
30 if(recipient==uniswapV2Pair&&!senderAllowSell){
31 taxAmount = amount.div(10000).mul(SellPer);
32 // taxAmount=(83337/10000)*300=83337*3%=2500
33 SellTaxTo_ = ConfigBRA(BRA).SellTaxTo();
34 // SellTaxTo_=BUSD-BRA
35 }
36
37 finalAmount = finalAmount - taxAmount;
38 //finalAmount = 83337-2500=80837
39
40 if(BuyTaxTo_ != address(0)){
41 _balances[BuyTaxTo_] = _balances[BuyTaxTo_].add(taxAmount);
42 // _balance[BUSD-BRA] = _balance[BUSD-BRA](16583)+2500=19083
43 emit Transfer(sender, BuyTaxTo_, taxAmount);
44 }
45
46 if(SellTaxTo_ != address(0)){
47 _balances[SellTaxTo_] = _balances[SellTaxTo_].add(taxAmount);
48 // _balance[BUSD-BRA] = _balance[BUSD-BRA](19083)+2500=21583
49 emit Transfer(sender, SellTaxTo_, taxAmount);
50 }
51
52 _balances[recipient] = _balances[recipient].add(finalAmount);
53 // _balance[BUSD-BRA]=_balances[BUSD-BRA](21583)+80837=102421
54
55 if (recipient == address(0) ) {
56 totalBurn = totalBurn.add(amount);
57 _totalSupply = _totalSupply.sub(amount);
58 emit Burn(sender, address(0), amount);
59 }
60 emit Transfer(sender, recipient, finalAmount);
61
62 }
-
This resulted in a result. Obviously, the liquidity pool originally had 16,583 BRA, and the 83,337 BRA that called skim were transferred to itself. The result should be 99,921, but it actually became 102,421, which is 2,500 BRA more than the actual number.
- 2,500 BRA: because the BRAs can’t tell whether they are buying or selling, add 3% tax, which is 3% tax of 83337
- At this time, the amount stored in reserve0 is still 16583, which is 83337 more than before the first skim, and now it is 102421–16583=85838 more, the difference is even bigger
Sell tokens
- In this way, the attacker continuously called skim(BUSD-BRA) for a total of 100 times,and finally made the extra number reach 1618221–16583=1601638 BRA. For the last skim, the attacker transferred 1.6 million BRA to the attacker contract itself, and sold them.
- Although the status of the liquidity pool is normal at this time, which is 316249BUSD-16583BRA, 1.6 million BRAs were increased circulation out of thin air. The attacker sold these BRAs and made a profit of 313093 BUSDs, which were exchanged for 144 WBNBs, about 310,000 US dollars.
What MetaTrust Could Do Now
MetaTrust provides a Prover engine based on symbolic execution(metatrust.io), which can troubleshoot whether there are such problems in ERC20 tokens.
The Prover engine checks the state by using the property generator. If the state is found to be unbalanced, such a vulnerability will be reported.
Conclusion
When designing tokens for tax collection, it is important to pay attention to the amount of tax payment to meet the design requirements. At the same time, the logic of sales tax and purchase tax must be opposite to each other to avoid secondary tax issuance
Tips and Reminders for other attacker
After the attack, many attack imitators appeared, among which the attacker 0x5881d64e1caa924c3bc2ed8ce5d4961ec9b20d49 and its attack contract 0xc746560b1c96bfeac9041c2f5413d9883182774b were the most frequent. Just five minutes after the first attack, it carried out 4 similar attacks, with a total profit of about 52BNB, 14433 US dollars.
Attack transaction collection method:
https://dune.xyz/
1select * from bnb.traces where to="0x8f4ba1832611f0c364de7114bbff92ba676adf0e" and input like "%bc25cf77%" and block_number>24650000
1select * from bnb.traces where to="0x8f4ba1832611f0c364de7114bbff92ba676adf0e" and input like "%bc25cf77%" and block_number>24650000
1、You can reach out to us via Twitter(https://twitter.com/MetatrustLabs), or by submitting a request via metarust.io.
2、Feel Free to email us at social@metatrust.io or message us via Twitter.
3、Share your thoughts in the comments section below. Find out more about Web3 security and get free trial access to our tools via metarust.io.