Background introduction
Recently, we monitored an on-chain attack on BNB Smart Chain, https://bscscan.com/tx/0xd4c7c11c46f81b6bf98284e4921a5b9f0ff97b4c71ebade206cb10507e4503b0
The attacked project is the DeFi project of Bankroll Network.
Attack and incident analysis
First, the attacker borrowed 16,000 WBNB using flash via pancakeSwap.
Subsequently, the buyfor function of Bankroll is called to use the 16,000 WBNB obtained from the flash loan to purchase Bankroll’s share, which is similar to staking.
Then, the attacker calls the buyfor function in large quantities, causing Bankroll’s own contract to purchase Bankroll’s share.
Let’s take a look at the definition of the buyfor function, as shown below:
First, the function transfers the token from _customerAddress to the current contract, where the token is WBNB. Then, purchaseTokens is called to purchase shares, and finally the distribute function is called.
Let’s take a look at the definition of this function:
In this function, profitPerShare_ is updated, which is the profit of owning one share. Then, all the shares initially transferred into WBNB using buyfor are sold through the sell function, and all WBNB is withdrawn through Withdraw.
Let’s take a look at the definition of the sell function:
As you can see, the relationship between payoutsTo_[_customerAddress] and profitPerShare_ is that the larger the profitPerShare_, the smaller the payoutsTo_[_customerAddress], and payoutsTo_[_customerAddress] is a negative number.
Next, let’s take a look at the definition of the withdraw function:
It can be seen that the amount withdrawn comes from the myDividends function. Let’s continue tracking;
From the above analysis, we can conclude that as long as profitPerShare_ is larger, more WBNB can be withdrawn under the same share. Then, since the attacker obtained a certain number of shares through buyfor at the beginning, the attacker only needs to manipulate profitPerShare_ and raise its value to withdraw the tokens in the contract. Going back to the conclusion obtained from the initial analysis of the buyfor function, profitPerShare_ can be raised by calling the buyfor function to buy shares. However, the buyfor function does not restrict _customerAddress to not be the contract itself, so the attacker can complete the attack by passing _customerAddress to the contract address of Bankroll without transferring WBNB to raise profitPerShare_. The attacker obtained 16,412 WBNB from the Bankroll contract, paid off the principal and interest of the flash loan of 16,008 WBNB, and made a profit of 403 WBNB, which is about 230k USD.
Summary
This vulnerability lies in the code of Bankroll. There is no input restriction, which allows the attacker to make the buyer the project owner. This causes the share of each unit to be greatly increased, allowing the attacker to withdraw all the assets of Bankroll in the form of share profits. It is recommended that the project owner conduct a full audit and cross-audit of the smart contract before the contract goes online to avoid such security issues.