Analysis2023-01-11

Security Analysis of BRA Flash loan attack

6 Minutes Read

BradMoon

BradMoon

Security Operation / Audit

Summary

This blog post analyzes the BRA flash loan attack, which involved a series of transactions on the Binance Smart Chain. The attacker used a flash loan to borrow 1000 WBNB, which was then used to purchase and sell BRA tokens, resulting in an increase in circulation and a profit of approximately $310,000. The post also suggests using MetaTrust's Prover engine to troubleshoot ERC20 tokens for vulnerabilities and provides tips for preventing similar attacks.

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}
  1. 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 }
  2. 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

  1. 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.
  2. 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.

Share this article