All incidents

BRAToken self-transfer tax bug inflates pool and drains USDT

Share
Jan 10, 2023 04:40 UTCAttackLoss: 41,078.14 USDTManually checked2 exploit txWindow: 19m 22s
Estimated Impact
41,078.14 USDT
Label
Attack
Exploit Tx
2
Addresses
1
Attack Window
19m 22s
Jan 10, 2023 04:40 UTC → Jan 10, 2023 05:00 UTC

Exploit Transactions

TX 1BSC
0x6759db55a4edec4f6bedb5691fc42cf024be3a1a534ddcc7edd471ef205d4047
Jan 10, 2023 04:40 UTCExplorer
TX 2BSC
0x4e5b2efa90c62f2b62925ebd7c10c953dc73c710ef06695eac3f36fe0f6b9348
Jan 10, 2023 05:00 UTCExplorer

Victim Addresses

0x8f4ba1832611f0c364de7114bbff92ba676adf0eBSC

Loss Breakdown

41,078.14USDT

Similar Incidents

Root Cause Analysis

BRAToken self-transfer tax bug inflates pool and drains USDT

1. Incident Overview TL;DR

On BNB Smart Chain (BSC), an adversary-controlled EOA 0x67a909f2953fb1138bea4b60894b51291d2d0795 executed two flashloan-powered transactions that abused a bug in BRAToken’s tax logic to mint BRAToken directly into the BRAToken/USDT PancakeSwap pool. By repeatedly triggering BRAToken self-transfers where the AMM pair is both sender and recipient, the attacker inflated the pool’s BRAToken balance without increasing total supply, then swapped the inflated tokens for USDT and WBNB and repaid the flashloans.

Across the two exploit transactions (0x6759db55… and 0x4e5b2efa…), the BRAToken/USDT pool at 0x8f4ba1832611f0c364de7114bbff92ba676adf0e lost 41,078.140669802606214866 USDT, while the adversary cluster realized a net profit of approximately 820.161066222532481522 BNB-equivalent (WBNB inflows minus gas).

2. Key Background

  • BRAToken (BRA) is a BEP20 token on BSC whose buy and sell taxes are configured by a separate ConfigBRA contract at 0x0ed9cf2aF45916acefCd0390D32fEb5e88E74Ef6. Taxes are applied when transfers involve the uniswapV2Pair address and can be directed to configurable recipients.
  • The BRAToken/USDT AMM pool is a standard PancakeSwap pair contract at 0x8f4ba1832611f0c364de7114bbff92ba676adf0e, pairing BRAToken with BEP20USDT (0x55d398326f99059fF775485246999027B3197955). LPs own a proportional share of the pool’s BRAToken and USDT reserves.
  • DPPAdvanced is a DODO single-token liquidity pool on BSC; the WBNB pool at 0x0fe261aeE0d1C4DFdDee4102E82Dd425999065F4 exposes a public flashLoan function that allows any caller to borrow WBNB as long as the amount plus fee is repaid in the same transaction.
  • The observed ConfigBRA configuration for the BRAToken/USDT pair is:
    • BuyPer = SellPer = 300 (i.e., 3% each, on a 1e4 basis),
    • BuyTaxTo = SellTaxTo = the BRAToken/USDT pair address,
    • isAllow(pair) == false and isAllowSell(pair) == false.
  • The adversary deploys two borrower contracts (0x1FAe46B3… and 0x6066435e…) that orchestrate:
    • flashLoans from DPPAdvanced,
    • WBNB ↔ BNB unwrap/wrap,
    • PancakeRouter swaps through WBNB/USDT and BRAToken/USDT pairs,
    • BRAToken self-transfer loops via pair-level swap and skim operations,
    • final swaps back to WBNB and profit transfers to the EOA.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is a logic bug in BRAToken’s internal _transfer function, combined with the ConfigBRA deployment parameters. When both the sender and recipient are the BRAToken/USDT pair and the pair is not allowlisted, the function:

  • subtracts amount from the pair’s balance,
  • calculates a single taxAmount based on BuyPer or SellPer,
  • credits taxAmount twice (once to BuyTaxTo, once to SellTaxTo) and finalAmount = amount - taxAmount back to the recipient,
  • leaves _totalSupply unchanged except in the burn branch (recipient == 0).

Under the incident configuration where BuyTaxTo = SellTaxTo = pair and sender == recipient == pair, the net effect is to increase the pair’s BRAToken balance by taxAmount while preserving _totalSupply, violating basic token accounting invariants. An unprivileged adversary can drive this self-transfer path repeatedly via AMM swap and skim operations to mint BRAToken into the pool and then arbitrage it out for USDT and WBNB.

4. Detailed Root Cause Analysis

4.1 Invariant and Breakpoint

Two core invariants are expected:

  • Token supply invariant: The sum of all BRAToken balances must equal _totalSupply at all times.
  • AMM reserve consistency: For the BRAToken/USDT pair, the BRAToken balance held by the pair should only change via:
    • explicit debits from senders (incoming transfers),
    • LP mint/burn mechanics,
    • standard swap fee behavior, and remain consistent with the AMM’s constant-product pricing.

BRAToken’s _transfer implementation (from artifacts/root_cause/seed/56/0x449fea37d339a11efe1b181e5d5462464bba3752/src/Contract.sol) is:

function _transfer(address sender, address recipient, uint amount) internal {
    require(sender != address(0), "BEP20: transfer from the zero address");

    bool recipientAllow = ConfigBRA(BRA).isAllow(recipient);
    bool senderAllowSell = ConfigBRA(BRA).isAllowSell(sender);

    uint BuyPer = ConfigBRA(BRA).BuyPer();
    uint SellPer = ConfigBRA(BRA).SellPer();

    address BuyTaxTo_ = address(0);
    address SellTaxTo_ = address(0);

    _balances[sender] = _balances[sender].sub(amount, "BEP20: transfer amount exceeds balance");

    uint256 finalAmount = amount;
    uint256 taxAmount = 0;
  
    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();
    }

    finalAmount = finalAmount - taxAmount;

    if(BuyTaxTo_ != address(0)){
        _balances[BuyTaxTo_] = _balances[BuyTaxTo_].add(taxAmount);
        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);
    
    if (recipient == address(0) ) {
        totalBurn = totalBurn.add(amount);
        _totalSupply = _totalSupply.sub(amount);
        emit Burn(sender, address(0), amount);
    }
    emit Transfer(sender, recipient, finalAmount);
}

Under the exploit conditions:

  • sender == recipient == uniswapV2Pair,
  • recipientAllow == false and senderAllowSell == false,
  • BuyPer = SellPer = 300,
  • BuyTaxTo = SellTaxTo = uniswapV2Pair.

Execution proceeds as:

  1. _balances[pair] -= amount.
  2. First if branch sets taxAmount = amount * BuyPer / 10000 and BuyTaxTo_ = pair.
  3. Second if branch overwrites taxAmount = amount * SellPer / 10000 (identical value here) and sets SellTaxTo_ = pair.
  4. finalAmount = amount - taxAmount.
  5. Credits:
    • _balances[BuyTaxTo_] += taxAmount (pair),
    • _balances[SellTaxTo_] += taxAmount (pair again),
    • _balances[recipient] += finalAmount (pair).
  6. _totalSupply is unchanged because recipient != address(0).

The net change to the pair’s balance is: -amount + finalAmount + taxAmount + taxAmount = -amount + (amount - taxAmount) + 2 * taxAmount = +taxAmount.

Thus every such self-transfer mints taxAmount BRAToken into the pair without a matching burn or debit elsewhere, breaking both the global balance invariant and the AMM reserve consistency.

4.2 ConfigBRA Settings Enabling the Bug

The decompiled ConfigBRA contract (artifacts/root_cause/data_collector/iter_1/contract/56/0x0ed9cf2aF45916acefCd0390D32fEb5e88E74Ef6/decompile/...) exposes:

uint256 public Min;
address public owner;
uint256 public SellPer;
uint256 public BuyPer;
address public SellTaxTo;
address public BuyTaxTo;

function isAllow(address arg0) public view returns (bool);
function isAllowSell(address arg0) public view returns (bool);

From the traces for both exploit transactions, the following values are observed for the BRAToken/USDT pair:

ConfigBRA::isAllow(pair)      → 0
ConfigBRA::isAllowSell(pair)  → 0
ConfigBRA::BuyPer()           → 0x12c (300)
ConfigBRA::SellPer()          → 0x12c (300)
ConfigBRA::BuyTaxTo()         → pair address
ConfigBRA::SellTaxTo()        → pair address

This configuration makes the pair both taxable for buys and sells and routes both tax flows back to the pair itself, exactly satisfying the conditions that trigger minting on self-transfer.

4.3 Trace Evidence of Self-Transfer Minting

The trace.cast.log for the first exploit transaction (0x6759db55…) shows repeated BRAToken self-transfers within PancakePair::skim calls where the pair is both sender and recipient and ConfigBRA returns the above parameters. For example:

BRAToken::transfer(PancakePair: [0x8F4BA1832611f0c364dE7114bbff92ba676AdF0E], 10539350743918941916677)
  ConfigBRA::isAllow(pair)        → 0
  ConfigBRA::isAllowSell(pair)    → 0
  ConfigBRA::BuyPer()             → 0x12c
  ConfigBRA::SellPer()            → 0x12c
  ConfigBRA::BuyTaxTo()           → pair
  ConfigBRA::SellTaxTo()          → pair
  emit Transfer(from: pair, to: pair, value: 3.16e20)
  emit Transfer(from: pair, to: pair, value: 3.16e20)
  emit Transfer(from: pair, to: pair, value: 1.02e22)

The corresponding balance_diff.json for the first transaction shows a large positive BRAToken delta for the pair:

{
  "token": "0x449fea37d339a11efe1b181e5d5462464bba3752",
  "holder": "0x8f4ba1832611f0c364de7114bbff92ba676adf0e",
  "before": "19130492548708862134718",
  "after": "217219602167267832539918",
  "delta": "198089109618558970405200",
  "contract_name": "BRAToken"
}

The second exploit transaction (0x4e5b2efa…) shows the same pattern and an even larger BRAToken increase:

{
  "token": "0x449fea37d339a11efe1b181e5d5462464bba3752",
  "holder": "0x8f4ba1832611f0c364de7114bbff92ba676adf0e",
  "before": "99921194193661924936423",
  "after": "1666270349299319784421123",
  "delta": "1566349155105657859484700",
  "contract_name": "BRAToken"
}

These BRAToken inflows to the pair occur without any corresponding increase in _totalSupply and are driven purely by the self-transfer path described above.

4.4 Conditions for an ACT Opportunity

The exploit is an Anyone-Can-Take (ACT) opportunity under the following publicly verifiable conditions:

  • BRAToken is deployed with the _transfer implementation above and uses ConfigBRA for BuyPer, SellPer, BuyTaxTo, SellTaxTo, isAllow, and isAllowSell.
  • ConfigBRA is configured so that:
    • BuyPer > 0 and SellPer > 0 (300 in the incident),
    • BuyTaxTo = SellTaxTo = the BRAToken/USDT pair address,
    • the pair address is not allowlisted for buy or sell.
  • The BRAToken/USDT pair holds sufficient USDT liquidity such that swapping inflated BRAToken back to USDT yields a positive amount of USDT that can be routed to WBNB and withdrawn as profit.
  • The DPPAdvanced WBNB pool (or any equivalent public liquidity source) is available to provide the initial WBNB needed for BRAToken purchases.
  • An unprivileged account can deploy and call a borrower contract that sequences:
    • DPPAdvanced.flashLoan,
    • swaps through WBNB/USDT and BRAToken/USDT,
    • the BRAToken self-transfer loop via swap and skim,
    • final swaps back to WBNB,
    • flashloan repayment and profit transfer.

All of these conditions are satisfied in the observed incident, and no privileged roles or non-public data are required, so any searcher with comparable infrastructure could have realized the same strategy.

5. Adversary Flow Analysis

5.1 Adversary-Related Cluster

  • EOA (attacker): 0x67a909f2953fb1138bea4b60894b51291d2d0795

    • Sends both exploit transactions.
    • Pays gas (native balance deltas show 0.05702991938 BNB and 0.05691699671 BNB in the two transactions).
    • Receives the final WBNB profit transfers from borrower contracts after flashloan repayment.
  • Borrower contract 1: 0x1FAe46B350C4A5F5C397Dbf25Ad042D3b9a5cb07

    • Called by the first exploit transaction 0x6759db55….
    • Requests and manages the 1.4e21 WBNB flashloan, routes funds through PancakeRouter into BRAToken/USDT, drives the BRAToken self-transfer minting loop, swaps inflated BRAToken back to USDT and WBNB, repays DPPAdvanced, and forwards WBNB profit to the EOA.
  • Borrower contract 2: 0x6066435edcE9C2772F3F1184b33FC5F7826d03e7

    • Called by the second exploit transaction 0x4e5b2efa….
    • Repeats the pattern with a 1.0e21 WBNB flashloan, further inflating the BRAToken/USDT pair and draining additional USDT, then converts back to WBNB, repays the flashloan, and transfers WBNB profit to the EOA.

These accounts together form the adversary-related cluster that realizes the exploit and captures the profit.

5.2 Stage 1: First FlashLoan and Initial Minting (Tx 0x6759db55…)

  • Chain: BSC (chainid 56)
  • Block: 24,655,772
  • Tx: 0x6759db55a4edec4f6bedb5691fc42cf024be3a1a534ddcc7edd471ef205d4047

Flow:

  1. The attacker EOA calls borrower contract 0x1FAe46B3…, invoking its flashloan entrypoint.
  2. DPPAdvanced pool 0x0fe261aeE0d1C4DFdDee4102E82Dd425999065F4 issues a 1.4e21 WBNB flashloan to the borrower.
  3. The borrower unwraps WBNB to BNB and routes BNB through PancakeRouter 0x10ED… into:
    • the WBNB/USDT pair 0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE, then
    • the BRAToken/USDT pair 0x8f4ba1832611f0c364de7114bbff92ba676adf0e, buying BRAToken.
  4. With BRAToken in the pool, the borrower triggers repeated BRAToken transfers where the pair is both sender and recipient, via PancakePair::swap followed by multiple PancakePair::skim(pair) calls. The trace shows the pattern:
PancakePair::skim(pair)
  BRAToken::balanceOf(pair)
  BRAToken::transfer(pair, 10539350743918941916677)
    ConfigBRA::isAllow(pair)      → 0
    ConfigBRA::isAllowSell(pair)  → 0
    ConfigBRA::BuyPer()           → 0x12c
    ConfigBRA::SellPer()          → 0x12c
    ConfigBRA::BuyTaxTo()         → pair
    ConfigBRA::SellTaxTo()        → pair
    emit Transfer(pair → pair, …)  (three events per call)
  1. Each such loop mints additional BRAToken into the pair reserve as described in Section 4, increasing its BRAToken balance by taxAmount per iteration.
  2. The borrower then swaps a portion of the inflated BRAToken back to USDT and WBNB via PancakeRouter, repays the 1.4e21 WBNB flashloan plus fee to DPPAdvanced, and finally transfers 675724517850647356854 WBNB to the attacker EOA.
  3. Native balance deltas show the attacker EOA spends 57029919380000000 wei (0.05702991938 BNB) in gas for this transaction.

5.3 Stage 2: Second FlashLoan, Further Minting, and USDT Drain (Tx 0x4e5b2efa…)

  • Chain: BSC (chainid 56)
  • Block: 24,656,152
  • Tx: 0x4e5b2efa90c62f2b62925ebd7c10c953dc73c710ef06695eac3f36fe0f6b9348

Flow:

  1. The attacker EOA calls borrower contract 0x6066435e….
  2. DPPAdvanced again lends 1.0e21 WBNB via flashLoan.
  3. The borrower unwraps and routes WBNB through WBNB/USDT and into the BRAToken/USDT pair, acquiring BRAToken and setting up the same self-transfer minting loop.
  4. The trace shows the same ConfigBRA parameters and BRAToken self-transfers where the pair is both sender and recipient, confirming the same minting behavior.
  5. The second balance_diff.json quantifies the BRAToken and USDT changes for the BRAToken/USDT pair:
{
  "token": "0x449fea37d339a11efe1b181e5d5462464bba3752",
  "holder": "0x8f4ba1832611f0c364de7114bbff92ba676adf0e",
  "before": "99921194193661924936423",
  "after": "1666270349299319784421123",
  "delta": "1566349155105657859484700",
  "contract_name": "BRAToken"
}
{
  "token": "0x55d398326f99059ff775485246999027b3197955",
  "holder": "0x8f4ba1832611f0c364de7114bbff92ba676adf0e",
  "before": "44233396958720717570246",
  "after": "3155256288918111355380",
  "delta": "-41078140669802606214866",
  "contract_name": "BEP20USDT"
}
{
  "token": "0x55d398326f99059ff775485246999027b3197955",
  "holder": "0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae",
  "before": "63373319853144533241823651",
  "after": "63414397993814335848038517",
  "delta": "41078140669802606214866",
  "contract_name": "BEP20USDT"
}
  1. Thus, the BRAToken/USDT pair loses 41078140669802606214866 units of USDT (≈ 41,078.140669802606214866 USDT), which are transferred to the WBNB/USDT pair.
  2. The borrower swaps USDT from the WBNB/USDT pair back to WBNB, repays the 1.0e21 WBNB flashloan plus fee, and sends 144550495287975124668 WBNB to the attacker EOA.
  3. Native balance deltas show the attacker EOA spends 56916996710000000 wei (0.05691699671 BNB) in gas for this second transaction.

5.4 Profit Computation

Summing across both transactions:

  • WBNB inflows to the attacker EOA from borrower contracts:
    • 675724517850647356854 WBNB (tx 0x6759db55…),
    • 144550495287975124668 WBNB (tx 0x4e5b2efa…),
    • Total: 820275013138622481522 WBNB.
  • Gas cost paid by the attacker EOA (native BNB):
    • 57029919380000000 wei (0.05702991938 BNB),
    • 56916996710000000 wei (0.05691699671 BNB),
    • Total: 0.11394691609 BNB.

Treating WBNB as BNB-equivalent, the incremental portfolio delta attributable to the exploit sequence is:

  • 820.275013138622481522 BNB-equivalent of WBNB inflow,
  • minus 0.11394691609 BNB gas,
  • yielding a net gain of 820.161066222532481522 BNB-equivalent.

This profit is funded entirely by the BRAToken/USDT LP’s USDT reserves that were extracted by swapping inflated BRAToken into USDT.

6. Impact & Losses

  • The BRAToken/USDT PancakeSwap pool at 0x8f4ba1832611f0c364de7114bbff92ba676adf0e loses 41078140669802606214866 units of USDT, equivalent to 41,078.140669802606214866 USDT given USDT’s 18 decimals on BSC.
  • The same pool’s BRAToken balance increases sharply (by approximately 1.98e23 units in the first transaction and 1.57e24 units in the second), but this increase is purely the result of the minting bug and is not backed by real value; LPs are left holding a larger quantity of devalued BRAToken and much less USDT.
  • The adversary-related cluster gains 820275013138622481522 WBNB from borrower-to-EOA transfers and spends 0.11394691609 BNB in gas, for a net BNB-equivalent gain of 820.161066222532481522.
  • BRAToken holders as a whole are indirectly harmed because the exploit reduces the economic backing of BRAToken in the main liquidity pool and undermines trust in the token’s accounting.

7. References

  • Exploit Transactions

    • Tx 0x6759db55a4edec4f6bedb5691fc42cf024be3a1a534ddcc7edd471ef205d4047 (BSC, block 24,655,772): first DPPAdvanced WBNB flashloan and BRAToken self-transfer minting loop. Artifacts under artifacts/root_cause/seed/56/0x6759db55.../.
    • Tx 0x4e5b2efa90c62f2b62925ebd7c10c953dc73c710ef06695eac3f36fe0f6b9348 (BSC, block 24,656,152): second DPPAdvanced WBNB flashloan, further minting, and USDT drain. Artifacts under artifacts/root_cause/seed/56/0x4e5b2efa.../.
  • Victim and Stakeholder Contracts

    • BRAToken (BRA) implementation at 0x449fea37d339a11efe1b181e5d5462464bba3752: source in artifacts/root_cause/seed/56/0x449fea37.../src/Contract.sol.
    • ConfigBRA tax configuration contract at 0x0ed9cf2aF45916acefCd0390D32fEb5e88E74Ef6: decompiled code in artifacts/root_cause/data_collector/iter_1/contract/56/0x0ed9cf2a.../decompile/.
    • BRAToken/USDT PancakeSwap pair at 0x8f4ba1832611f0c364de7114bbff92ba676adf0e: source in artifacts/root_cause/data_collector/iter_1/contract/56/0x8f4ba183.../source/src/Contract.sol.
    • DPPAdvanced WBNB pool at 0x0fe261aeE0d1C4DFdDee4102E82Dd425999065F4: source/decompile in artifacts/root_cause/data_collector/iter_1/contract/56/0x0fe261ae.../.
    • Borrower contract 1 at 0x1FAe46B350C4A5F5C397Dbf25Ad042D3b9a5cb07: source in artifacts/root_cause/data_collector/iter_1/contract/56/0x1FAe46B3.../source/.
    • Borrower contract 2 at 0x6066435edcE9C2772F3F1184b33FC5F7826d03e7: source/decompile in artifacts/root_cause/data_collector/iter_1/contract/56/0x6066435e.../.
  • Analysis Artifacts

    • Root cause JSON: root_cause.json at the session root.
    • Analyzer reasoning: artifacts/root_cause/root_cause_analyzer/iter_2/current_analysis_result.json.
    • Data collection summary: artifacts/root_cause/data_collector/data_collection_summary.json.