Analysis2023-01-11
Security Analysis of BRA Flash loan attack

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
- Attack transaction https://bscscan.com/tx/0x4e5b2efa90c62f2b62925ebd7c10c953dc73c710ef06695eac3f36fe0f6b9348
- Victim contract https://bscscan.com/token/0x449fea37d339a11efe1b181e5d5462464bba3752
- Victim liquidity pool BUSD-BRA https://bscscan.com/address/0x8f4ba1832611f0c364de7114bbff92ba676adf0e
- Attack contract https://bscscan.com/address/0x6066435edce9c2772f3f1184b33fc5f7826d03e7
- Attacker https://bscscan.com/address/0x67a909f2953fb1138bea4b60894b51291d2d0795
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:
function _transfer(address sender, address recipient, uint amount) internal { // sender=BUSD-BRA // recipient=attack contract // amount=85915 BRA require(sender != address(0), "BEP20: transfer from the zero address"); bool recipientAllow = ConfigBRA(BRA).isAllow(recipient); // recipientAllow=0 bool senderAllowSell = ConfigBRA(BRA).isAllowSell(sender); // senderAllowSell=0 uint BuyPer = ConfigBRA(BRA).BuyPer(); // BuyPer=300 uint SellPer = ConfigBRA(BRA).SellPer(); // SellPer=300 address BuyTaxTo_ = address(0); address SellTaxTo_ = address(0); _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance"); // _balance[BUSD-BRA] = _balance[BUSD-BRA](99921)-85915=14006 uint256 finalAmount = amount; uint256 taxAmount = 0; if(sender==uniswapV2Pair&&!recipientAllow){ taxAmount = amount.div(10000).mul(BuyPer); // taxAmount=(85915/10000)*300=85915*3%=2577 BuyTaxTo_ = ConfigBRA(BRA).BuyTaxTo(); // BuyTaxTo_=BUSD-BRA } if(recipient==uniswapV2Pair&&!senderAllowSell){ taxAmount = amount.div(10000).mul(SellPer); SellTaxTo_ = ConfigBRA(BRA).SellTaxTo(); } finalAmount = finalAmount - taxAmount; if(BuyTaxTo_ != address(0)){ _balances[BuyTaxTo_] = _balances[BuyTaxTo_].add(taxAmount); // _balance[BUSD-BRA] = _balance[BUSD-BRA](14006)+2577=16583 emit Transfer(sender, BuyTaxTo_, taxAmount); } if(SellTaxTo_ != address(0)){ _balances[SellTaxTo_] = _balances[SellTaxTo_].add(taxAmount); emit Transfer(sender, SellTaxTo_, taxAmount); } _balances[recipient] = _balances[recipient].add(finalAmount); // _balance[attack contract]=_balances[recipient](0)+83337 if (recipient == address(0) ) { totalBurn = totalBurn.add(amount); _totalSupply = _totalSupply.sub(amount); emit Burn(sender, address(0), amount); } emit Transfer(sender, recipient, finalAmount); }
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.
function skim(address to) external lock { address _token0 = token0; // gas savings address _token1 = token1; // gas savings _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0)); _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1)); }
-
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:function _transfer(address sender, address recipient, uint amount) internal { // sender=BUSD-BRA // recipient=BUSD-BRA // amount=83337 BRA require(sender != address(0), "BEP20: transfer from the zero address"); bool recipientAllow = ConfigBRA(BRA).isAllow(recipient); // recipientAllow=0 bool senderAllowSell = ConfigBRA(BRA).isAllowSell(sender); // senderAllowSell=0 uint BuyPer = ConfigBRA(BRA).BuyPer(); // BuyPer=300 uint SellPer = ConfigBRA(BRA).SellPer(); // SellPer=300 address BuyTaxTo_ = address(0); address SellTaxTo_ = address(0); _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance"); // _balance[BUSD-BRA] = _balance[BUSD-BRA](99921)-83337=16583 uint256 finalAmount = amount; uint256 taxAmount = 0; if(sender==uniswapV2Pair&&!recipientAllow){ taxAmount = amount.div(10000).mul(BuyPer); // taxAmount=(83337/10000)*300=83337*3%=2500 BuyTaxTo_ = ConfigBRA(BRA).BuyTaxTo(); // BuyTaxTo_=BUSD-BRA } if(recipient==uniswapV2Pair&&!senderAllowSell){ taxAmount = amount.div(10000).mul(SellPer); // taxAmount=(83337/10000)*300=83337*3%=2500 SellTaxTo_ = ConfigBRA(BRA).SellTaxTo(); // SellTaxTo_=BUSD-BRA } finalAmount = finalAmount - taxAmount; //finalAmount = 83337-2500=80837 if(BuyTaxTo_ != address(0)){ _balances[BuyTaxTo_] = _balances[BuyTaxTo_].add(taxAmount); // _balance[BUSD-BRA] = _balance[BUSD-BRA](16583)+2500=19083 emit Transfer(sender, BuyTaxTo_, taxAmount); } if(SellTaxTo_ != address(0)){ _balances[SellTaxTo_] = _balances[SellTaxTo_].add(taxAmount); // _balance[BUSD-BRA] = _balance[BUSD-BRA](19083)+2500=21583 emit Transfer(sender, SellTaxTo_, taxAmount); } _balances[recipient] = _balances[recipient].add(finalAmount); // _balance[BUSD-BRA]=_balances[BUSD-BRA](21583)+80837=102421 if (recipient == address(0) ) { totalBurn = totalBurn.add(amount); _totalSupply = _totalSupply.sub(amount); emit Burn(sender, address(0), amount); } emit Transfer(sender, recipient, finalAmount); }
-
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:
select * 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.

BradMoon
Security Operation / Audit
Share this article
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.
@2023 by MetaTrust Labs Pte. Ltd. All Rights Reserved