All incidents

BankX/XSD Router Burn Reentrancy

Share
Sep 26, 2023 18:37 UTCAttackLoss: 9.84 BNBPending manual check1 exploit txWindow: Atomic
Estimated Impact
9.84 BNB
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
Sep 26, 2023 18:37 UTC → Sep 26, 2023 18:37 UTC

Exploit Transactions

TX 1BSC
0xbdf76f22c41fe212f07e24ca7266d436ef4517dc1395077fabf8125ebe304442
Sep 26, 2023 18:37 UTCExplorer

Victim Addresses

0xfADDa925e10d07430f5d7461689fd90d3D81bB48BSC
0x39400e67820c88a9d67f4f9c1fbf86f3d688e9f6BSC
0xbfbcb8bde20cc6886877dd551b337833f3e0d96dBSC

Loss Breakdown

9.84BNB

Similar Incidents

Root Cause Analysis

BankX/XSD Router Burn Reentrancy

1. Incident Overview TL;DR

On BNB Chain block 32086901, transaction 0xbdf76f22c41fe212f07e24ca7266d436ef4517dc1395077fabf8125ebe304442 exploited BankX/XSD through a public router path that let an unprivileged attacker use nested DODO flash loans, a helper contract, and router reentrancy to drain the XSD/WBNB pool. The attacker helper contract 0x202e059a16d29a2f6ae0307ae3d574746b2b6305 finished the transaction with 10.889999 BNB after starting with 1.05 BNB, while the originating EOA paid 0.002283925 BNB in gas, yielding a tx-local adversary-cluster net delta of 9.837715975 BNB.

The root cause is an exploitable exact-output swap path in Router.swapXSDForETH(uint amountOut, uint amountInMax). The router computes the quoted input but transfers amountInMax instead, pays native BNB to the caller before the protocol burn completes, and then burns amountInMax / 10 from the pool via XSDStablecoin.burnpoolXSD. That sequence lets an attacker reenter between payout and burn, reshape pool reserves, and force a burn sized from attacker-controlled input.

2. Key Background

BankX’s router is not a plain AMM router. Its XSD-to-BNB path performs a pool swap and then conditionally burns XSD directly from the XSD/WBNB pool. The relevant public components are:

  • Router 0xfADDa925e10d07430f5d7461689fd90d3D81bB48
  • XSD token 0x39400e67820c88A9D67F4F9c1fbf86f3D688e9F6
  • XSD/WBNB pool 0xbfBcB8BDE20cc6886877DD551b337833F3e0d96d

The helper contract matters because swapXSDForETH sends native BNB to msg.sender. If msg.sender is a contract, its receive() path can run arbitrary logic before the router reaches the burn. The collected tx history shows the attacker EOA 0x506eebd8d6061202a8e8fc600bb3d5d41f475ee1 deployed the helper in tx 0xed0e5a6d6e9cbe61c523b842474d78b92369a856f0c7b5c4a230555c7ae4df8e and funded it in tx 0x12219a35ea2753fb5443387a6728c4f055254c0c18039b6c6e5e7f9c52d8ff83.

3. Vulnerability Analysis & Root Cause Summary

The vulnerability is an attack-class protocol bug, not a flash-loan issue. In Router.swapXSDForETH, the router first computes the required input with BankXLibrary.quote, but then debits amountInMax from the caller instead of the quoted amount. That already lets a caller stuff an oversized amount of XSD into the pool while only asking for a fixed amountOut of BNB.

The second defect is the interaction order. After the pool swap, the router unwraps WBNB and transfers native BNB to the caller before it evaluates the burn branch. Because there is no reentrancy guard and the payout is an external call, the caller can reenter during this window and manipulate the XSD/WBNB reserves.

The third defect is the burn basis itself. If the protocol-side predicate passes, the router calls XSD.burnpoolXSD(amountInMax / 10), so the burn amount is based on attacker-controlled amountInMax, not the actual quoted input. burnpoolXSD then burns directly from the pool balance and syncs reserves. The violated invariant is straightforward: an exact-output router must debit only the quoted amount, and protocol-side burns must not depend on caller-controlled maxima after an external interaction.

4. Detailed Root Cause Analysis

The relevant victim-side code in the verified Router source is:

function swapXSDForETH(uint amountOut, uint amountInMax) external override {
    (uint reserveA, uint reserveB, ) = IXSDWETHpool(XSDWETH_pool_address).getReserves();
    uint amounts = BankXLibrary.quote(amountOut, reserveB, reserveA);
    require(amounts <= amountInMax, "BankXRouter: EXCESSIVE_INPUT_AMOUNT");
    TransferHelper.safeTransferFrom(xsd_address, msg.sender, XSDWETH_pool_address, amountInMax);
    XSDWETHpool(XSDWETH_pool_address).swap(0, amountOut, address(this));
    IWBNB(WETH).withdraw(amountOut);
    TransferHelper.safeTransferETH(msg.sender, amountOut);
    if (XSD.totalSupply() - CollateralPool(payable(collateral_pool_address)).collat_XSD() > amountOut / 10 && !pid_controller.bucket1()) {
        XSD.burnpoolXSD(amountInMax / 10);
    }
    refreshPID();
}

The burn primitive in the verified XSD source is:

function burnpoolXSD(uint _xsdamount) public {
    require(msg.sender == router, "Only the router can access this function");
    require(totalSupply() - CollateralPool(payable(collateral_pool_address)).collat_XSD() > _xsdamount, "uXSD has to be positive");
    super._burn(address(xsdEthPool), _xsdamount);
    xsdEthPool.sync();
    emit XSDBurned(msg.sender, address(this), _xsdamount);
}

The seed trace shows the exploit sequence exactly:

0xfADDa925e10d07430f5d7461689fd90d3D81bB48::swapXSDForETH(9840000000000000000, 39566238265722260955438)
XSDStablecoin::transferFrom(attacker, XSDWETHpool, 39566238265722260955438)
0x202E059a16D29a2F6aE0307AE3D574746b2B6305::fallback{value: 9840000000000000000}()
0xfADDa925e10d07430f5d7461689fd90d3D81bB48::swapETHForBankX{value: 1000000000000}(100)
XSDStablecoin::burnpoolXSD(3956623826572226095543)
emit Sync(reserve0: 7313572418829758249, reserve1: 3857071215838228063464)

The same trace records the reentrant reserve manipulation before the burn. After the router pays out 9.84 BNB, the helper contract sends 3800 WBNB into the pool and buys out 263932735529288914857295 XSD, leaving the pool with only 3963937398991055853792 XSD before the burn. The router then burns 3956623826572226095543 XSD from the pool, collapsing the XSD reserve to 7313572418829758249, which is effectively near-zero at pool scale.

Once the pool is almost empty on the XSD side, the attacker holds nearly all of the pool’s XSD inventory and can dump it back for nearly all remaining WBNB. That is why the exploit is not just a transient reserve distortion: the reserve collapse becomes the mechanism for the final extraction.

5. Adversary Flow Analysis

The adversary flow is a single transaction preceded by public setup:

  1. Deploy helper contract 0x202e059a16d29a2f6ae0307ae3d574746b2b6305 from EOA 0x506eebd8d6061202a8e8fc600bb3d5d41f475ee1.
  2. Fund the helper with 11 BNB.
  3. Prime the helper with XSD inventory through the public router path.
  4. Borrow 3000 WBNB and then 1000 WBNB from the two public DODO flash-loan contracts.
  5. Call swapXSDForETH(9.84 BNB, 39566238265722260955438) on the public router.
  6. Reenter during the router’s native-BNB payout, buy out most remaining XSD from the pool, and make a tiny reentrant swapETHForBankX call that refreshes the router’s PID timestamp path.
  7. Let the original router call resume and burn amountInMax / 10 from the pool.
  8. Sell the attacker-held XSD back into the now-depleted pool, repay the flash loans, and keep the residual WBNB/BNB.

The collected balance diff confirms the realized profit:

{
  "native_balance_deltas": [
    {
      "address": "0x202e059a16d29a2f6ae0307ae3d574746b2b6305",
      "before_wei": "1050000000000000000",
      "after_wei": "10889999000000000000",
      "delta_wei": "9839999000000000000"
    },
    {
      "address": "0x506eebd8d6061202a8e8fc600bb3d5d41f475ee1",
      "delta_wei": "-2283925000000000"
    }
  ]
}

6. Impact & Losses

The exploit extracted 9839999000000000000 wei of BNB-equivalent value from the XSD/WBNB pool, or roughly 9.84 BNB. The pool’s XSD reserve was burned down from 3963937398991055853792 to 7313572418829758249 during the exploit path, and the remaining WBNB liquidity was then drained via the attacker’s final dump.

The directly affected public components are the BankX Router, the XSD token’s router-authorized burn function, and the XSD/WBNB pool. The economic effect is both immediate loss and pool-state corruption.

7. References

  • Seed exploit transaction: 0xbdf76f22c41fe212f07e24ca7266d436ef4517dc1395077fabf8125ebe304442
  • Attacker setup transaction: 0xed0e5a6d6e9cbe61c523b842474d78b92369a856f0c7b5c4a230555c7ae4df8e
  • Helper funding transaction: 0x12219a35ea2753fb5443387a6728c4f055254c0c18039b6c6e5e7f9c52d8ff83
  • Verified Router: 0xfADDa925e10d07430f5d7461689fd90d3D81bB48
  • Verified XSD token / PID controller bundle: 0x82a6405B9C38Eb1d012c7B06642dcb3D7792981B
  • XSD/WBNB pool: 0xbfBcB8BDE20cc6886877DD551b337833F3e0d96d
  • Supporting evidence: seed metadata, trace, balance diff, and collected tx histories under /workspace/session/artifacts/collector/