All incidents

Transit Router V5 Drain

Share
Dec 20, 2023 02:01 UTCAttackLoss: 43,841.87 USDTPending manual check8 exploit txWindow: 36m 30s
Estimated Impact
43,841.87 USDT
Label
Attack
Exploit Tx
8
Addresses
1
Attack Window
36m 30s
Dec 20, 2023 02:01 UTC → Dec 20, 2023 02:38 UTC

Exploit Transactions

TX 1BSC
0x93ae5f0a121d5e1aadae052c36bc5ecf2d406d35222f4c6a5d63fef1d6de1081
Dec 20, 2023 02:01 UTCExplorer
TX 2BSC
0x45929e8a20c5a2295914f115b9b66f53eccacbd99ad386b55095e08b34be4f90
Dec 20, 2023 02:14 UTCExplorer
TX 3BSC
0x17d81b602b78617a7801dde1918980e9711dc872728c74e7092e5eb0f30a8512
Dec 20, 2023 02:15 UTCExplorer
TX 4BSC
0x67da40bbe1912dbd0992895b6de86712231d44ec3649e532960b71be6f749e82
Dec 20, 2023 02:27 UTCExplorer
TX 5BSC
0x363a1949ada5ff295a3ca41cf907f3ba5ac9b2a6b75445a881194cbc2e30620a
Dec 20, 2023 02:27 UTCExplorer
TX 6BSC
0xae61dafcca3cd21c1cafe680e2f37b9183e4e81a709d2ed5e81ba5491af518f4
Dec 20, 2023 02:30 UTCExplorer
TX 7BSC
0xc05d2c2baddbd8821e10259dc06c98e9428ccf6b1dc428eae3d07e8c809deaad
Dec 20, 2023 02:32 UTCExplorer
TX 8BSC
0x575d1aa68fcf21823b2a5eee3b418352d6b6fb81e15a8d186ae807741965a1b2
Dec 20, 2023 02:38 UTCExplorer

Victim Addresses

0x00000047bb99ea4d791bb749d970de71ee0b1a34BSC

Loss Breakdown

43,841.87USDT

Similar Incidents

Root Cause Analysis

Transit Router V5 Drain

1. Incident Overview TL;DR

Transit Finance Router v5 on BNB Smart Chain was drained in block 34506417 because its V3 routing path accepted an arbitrary first-hop pool address and trusted that address's self-reported metadata plus returned signed deltas as if they came from a canonical Pancake V3 pool. In transaction 0x93ae5f0a121d5e1aadae052c36bc5ecf2d406d35222f4c6a5d63fef1d6de1081, attacker EOA 0xf7552ba0ee5bed0f306658f4a1201f421d703898 sent 0.01 BNB to helper contract 0x7d7583724245eeebb745ebcb1cee0091ff43082b, which in turn drove TransitSwapRouterV5::exactInputV3Swap on router 0x00000047bb99ea4d791bb749d970de71ee0b1a34.

The fake first hop reported token0=WBNB, token1=USDT, fee=0, and a forged negative amount1 equal to the router's entire pre-existing USDT custody. The router then treated 43841867959016089190183 units of BEP20USDT as legitimate hop output, sent that exact amount into the real Pancake V3 USDT/WBNB pool 0x36696169c63e42cd08ce11f5deebbcebae652050, and the real pool returned 173907186477338745776 wei of WBNB into attacker-controlled PancakePair 0xece3f2645ed0910d4a10f4e262e9fe47c481d9de. The attacker later removed liquidity and sold the attacker token back into WBNB across transactions 0x45929e8a..., 0x17d81b60..., 0x363a1949..., 0xae61dafc..., 0xc05d2c2b..., and 0x575d1aa6..., realizing at least 156.30 BNB of profit.

The root cause is an application-level accounting flaw, not a liquidity or pricing anomaly in PancakeSwap. Transit's V3 router authenticated only callback senders and never authenticated the pool address used for hop accounting. That broke the invariant that each hop's amountOut must come from a canonical, authorized pool that actually transferred tokens to the router before the router spends those tokens in the next hop.

2. Key Background

Transit Router v5 encodes each V3 hop as a uint256 whose low 160 bits are the pool address and whose upper bits carry flags such as the pool index used by callback verification. The router stores allowed factory and init-code-hash pairs by pool index, but _verifyPool does not look up the address in that allowlist. Instead, it calls token0(), token1(), and fee() on whatever address appears in params.pools[i].

The real second hop in the incident is Pancake V3 USDT/WBNB pool 0x36696169c63e42cd08ce11f5deebbcebae652050. Pancake V3's swap implementation transfers the output token first and then invokes IPancakeV3SwapCallback(msg.sender).pancakeV3SwapCallback(amount0, amount1, data), which forces the caller to pay the positive delta before the swap completes. Transit Router v5 implements that callback and, after checking that msg.sender matches the allowed factory/init-code-hash tuple for the selected pool index, pays tokenIn from its own balance.

The attacker monetized the drain through Collector/WBNB PancakePair 0xece3f2645ed0910d4a10f4e262e9fe47c481d9de. The pair held the full 1e24 supply of Collector token 0x1f790e7eb953b3f7ead89e5a100ffc3b8d2d2b28 plus 0.01 WBNB before the seed transaction. PancakePair's reserve accounting is balance-based: sync() writes current balances into reserves, and burn() later redeems pro-rata balances to the LP owner.

function burn(address to) external lock returns (uint amount0, uint amount1) {
    uint balance0 = IERC20(_token0).balanceOf(address(this));
    uint balance1 = IERC20(_token1).balanceOf(address(this));
    amount0 = liquidity.mul(balance0) / _totalSupply;
    amount1 = liquidity.mul(balance1) / _totalSupply;
    _safeTransfer(_token0, to, amount0);
    _safeTransfer(_token1, to, amount1);
}

function sync() external lock {
    _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}

Those mechanics matter because once the real Pancake V3 pool sent WBNB into the pair and the helper called sync(), that stolen WBNB became withdrawable or sellable through standard Pancake V2 flows. No privileged role was required at any stage.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK-category ACT opportunity caused by Transit Router v5's V3 hop accounting. The vulnerable invariant is: for every exactInputV3Swap hop, the amount forwarded into hop i+1 must equal tokens actually received from an authenticated hop i, and the router must never spend a pre-existing intermediate-token balance unless the immediately preceding authenticated hop delivered it.

Transit breaks that invariant in three places. First, _verifyPool(address tokenIn, address tokenOut, uint256 pool) accepts any pool address and trusts that address's token0(), token1(), and fee() responses to decide whether the hop is valid and what the next hop token should be. Second, _swap(address recipient, uint256 pool, bytes memory tokenInAndPoolSalt, uint256 amount) trusts the callee's returned signed deltas and converts the negative output delta directly into amountOut, even if no canonical V3 callback/payment occurred. Third, _executeCallback(int256 amount0Delta, int256 amount1Delta, bytes memory _data) authenticates only the callback sender and then pays tokenIn from router custody to the pool.

The verified Transit router source shows the precise issue:

function _swap(address recipient, uint256 pool, bytes memory tokenInAndPoolSalt, uint256 amount)
    internal
    returns (uint256 amountOut)
{
    bool zeroForOne = pool & _ZERO_FOR_ONE_MASK == 0;
    if (zeroForOne) {
        (, int256 amount1) = IUniswapV3Pool(address(uint160(pool))).swap(
            recipient,
            zeroForOne,
            amount.toInt256(),
            MIN_SQRT_RATIO + 1,
            abi.encode(pool, tokenInAndPoolSalt)
        );
        amountOut = SafeMath.toUint256(-amount1);
    }
}

function _verifyPool(address tokenIn, address tokenOut, uint256 pool)
    internal
    view
    returns (address nextTokenIn, bytes memory tokenInAndPoolSalt)
{
    IUniswapV3Pool iPool = IUniswapV3Pool(address(uint160(pool)));
    address token0 = iPool.token0();
    address token1 = iPool.token1();
    uint24 fee = iPool.fee();
    ...
}

function _executeCallback(int256 amount0Delta, int256 amount1Delta, bytes memory _data) internal {
    ...
    _verifyCallback(pool, poolSalt, msg.sender);
    ...
    TransferHelper.safeTransfer(tokenIn, msg.sender, amountToPay);
}

The security principles violated are straightforward:

  • Authenticate external counterparties before trusting their metadata or return values.
  • Do not derive accounting transitions from untrusted callee return data alone.
  • Do not mix router custody balances with per-swap obligations.

4. Detailed Root Cause Analysis

4.1 Pre-state and ACT conditions

The exploit was realizable from publicly reconstructible BNB Smart Chain state immediately before transaction index 0x10 in block 34506417. At that point, Transit Router v5 already held 43841867959016089190183 units of BEP20USDT, and the attacker-controlled Collector/WBNB pair already held the entire Collector supply (1000000000000000000000000) plus 10000000000000000 wei of WBNB. Those balances are visible in the collected seed trace and seed balance diff.

The ACT conditions were:

  • the router had to custody a nonzero intermediate-token balance that could be named as fake-hop output;
  • an unprivileged actor had to be able to submit a route whose first hop was an arbitrary contract and whose second hop was a real allowed V3 pool;
  • the attacker needed a venue it controlled to monetize the real pool's output, here the Collector/WBNB V2 pair.

All three conditions were public and permissionless at block 34506417.

4.2 Fake first hop and forged accounting

The seed trace shows the exact exploit entrypoint:

0x00000047...::exactInputV3Swap{value: 10000000000000000}(...)
  0x7d758372...::token0() -> WBNB
  0x7d758372...::token1() -> BEP20USDT
  0x7d758372...::fee()    -> 0
  0x7d758372...::swap(...)
    BEP20USDT::balanceOf(0x00000047...) -> 43841867959016089190183
    <- Return 0, -43841867959016089190183

This is the decisive accounting break. The fake first hop never transfers USDT to the router, never proves that it is a canonical Pancake V3 pool, and never executes a callback into Transit. It simply returns a negative amount1 equal to the router's live USDT balance. Because Transit converts -amount1 directly into amountOut, the router now believes it received 43841867959016089190183 USDT from the first hop.

4.3 Real second hop and router-funded callback

After trusting that forged output, Transit immediately uses the same amount as the input to the real Pancake V3 USDT/WBNB pool:

0x36696169...::swap(
  recipient = 0xEce3F2645Ed0910D4a10F4e262e9FE47C481D9DE,
  zeroForOne = true,
  amountSpecified = 43841867959016089190183,
  ...
)
  WBNB::transfer(0xEce3F264..., 173907186477338745776)
  Transit Router V5::pancakeV3SwapCallback(
    43841867959016089190183,
    -173907186477338745776,
    ...
  )
    USDT::transfer(0x36696169..., 43841867959016089190183)

That trace matches the router source exactly. Pancake V3 transfers WBNB to the recipient pair first, then invokes pancakeV3SwapCallback. _executeCallback verifies only that msg.sender matches the allowed factory configuration for pool index 1; it does not prove that the previous hop was canonical or that the amount being paid came from a real preceding hop. Because the second hop's tokenIn is USDT, Transit pays the real pool from its own pre-existing USDT custody.

The seed balance diff confirms the transfer economically:

  • router 0x00000047...: USDT 43841867959016089190183 -> 0
  • real Pancake V3 pool 0x36696169...: USDT 9485011174029608430218334 -> 9528853041988624519408517

The same artifact shows that the attacker EOA only funded 0.01 BNB plus gas into the seed transaction, so the drained USDT came from the router, not from attacker capital.

4.4 Pair synchronization and cash-out

The seed transaction then calls sync() on Collector/WBNB pair 0xece3f264..., writing the pair's new WBNB balance into reserves. That makes the WBNB returned by the real Pancake V3 pool claimable through ordinary V2 liquidity removal and token selling.

The attacker's later transactions follow that path exactly:

  • 0x45929e8a... and 0x17d81b60... call PancakeSwap Router v2 removeLiquidityETH, converting LP into Collector plus BNB.
  • 0x67da40bb... approves Collector to PancakeSwap Router v2.
  • 0x363a1949..., 0xae61dafc..., 0xc05d2c2b..., and 0x575d1aa6... call swapExactTokensForETH, dumping Collector into WBNB/BNB.

Summing the six successful internal BNB transfers to the attacker EOA yields 156334413809725050000 wei (156.33441380972505 BNB). That is consistent with the root-cause lower bound of >=156.30 BNB after subtracting the visible 0.02 BNB of attack-path capital and gas.

5. Adversary Flow Analysis

The adversary cluster consists of:

  • EOA 0xf7552ba0ee5bed0f306658f4a1201f421d703898, which sent the seed drain transaction and every later unwind transaction.
  • Helper contract 0x7d7583724245eeebb745ebcb1cee0091ff43082b, which exposed the fake V3 surface consumed by Transit.
  • Collector token 0x1f790e7eb953b3f7ead89e5a100ffc3b8d2d2b28, whose supply and approvals are used to monetize the injected WBNB.

The end-to-end on-chain flow is:

  1. 0x93ae5f0a121d5e1aadae052c36bc5ecf2d406d35222f4c6a5d63fef1d6de1081 (block 34506417) The attacker sends 0.01 BNB to 0x7d758372.... That call triggers Transit Router v5, forges the first V3 hop, drains the router's full USDT balance into the real Pancake V3 pool, receives 173907186477338745776 wei of WBNB into pair 0xece3f264..., and calls sync() on the pair.

  2. 0x45929e8a20c5a2295914f115b9b66f53eccacbd99ad386b55095e08b34be4f90 (block 34506662) PancakeSwap Router v2 removeLiquidityETH returns 118685695334069069 wei of BNB to the attacker.

  3. 0x17d81b602b78617a7801dde1918980e9711dc872728c74e7092e5eb0f30a8512 (block 34506699) A second removeLiquidityETH returns 118567009638735000649 wei of BNB.

  4. 0x67da40bbe1912dbd0992895b6de86712231d44ec3649e532960b71be6f749e82 (block 34506928) The attacker approves PancakeSwap Router v2 to spend Collector token.

  5. 0x363a1949ada5ff295a3ca41cf907f3ba5ac9b2a6b75445a881194cbc2e30620a (block 34506934) swapExactTokensForETH returns 33744840020043670406 wei of BNB.

  6. 0xae61dafcca3cd21c1cafe680e2f37b9183e4e81a709d2ed5e81ba5491af518f4 (block 34506981) A second successful swapExactTokensForETH returns 2843285233093763036 wei of BNB.

  7. 0xc05d2c2baddbd8821e10259dc06c98e9428ccf6b1dc428eae3d07e8c809deaad (block 34507035) A third successful swapExactTokensForETH returns 829485654209850115 wei of BNB.

  8. 0x575d1aa68fcf21823b2a5eee3b418352d6b6fb81e15a8d186ae807741965a1b2 (block 34507147) A fourth successful swapExactTokensForETH returns 231107568308683003 wei of BNB.

  9. 0x7531af1907940ea7c4419a02b62048b76d8956a4534a7496f91e4cfbfdc4ef15 (block 34508074, related) The attacker sends 100 BNB to 0x0d5550d52428e7e3175bfc9550207e4ad3859b17, which is the post-exploit disposal transaction referenced in the analysis.

This is a single-cluster, multi-transaction ACT exploit. Every transaction is permissionless, and the only attacker-controlled contract logic needed for the critical break is the fake V3 metadata/return-value surface exposed by 0x7d758372....

6. Impact & Losses

Transit Router v5 lost its full observed BEP20USDT custody in the seed transaction:

  • token: USDT
  • raw amount: 43841867959016089190183
  • decimal: 18
  • display amount: 43,841.867959016089190183 USDT

The real Pancake V3 pool converted that USDT into 173907186477338745776 wei of WBNB inside the attacker-controlled Collector/WBNB pair. The attacker's later liquidity removals and token sales transferred 156.33441380972505 BNB back to the attacker EOA across six successful unwind transactions. After subtracting the visible 0.02 BNB of attack-path capital and gas, the attack remains strictly profitable and comfortably above the root-cause predicate of >=156.30 BNB.

The directly affected victim is Transit Finance Router v5 at 0x00000047bb99ea4d791bb749d970de71ee0b1a34. Pancake V3 and Pancake V2 behaved according to their normal invariants; they were venues used to settle and monetize the stolen value, not the source of the accounting flaw.

7. References

  • Seed trace artifact: /workspace/session/artifacts/collector/seed/56/0x93ae5f0a121d5e1aadae052c36bc5ecf2d406d35222f4c6a5d63fef1d6de1081/trace.cast.log
  • Seed balance diff artifact: /workspace/session/artifacts/collector/seed/56/0x93ae5f0a121d5e1aadae052c36bc5ecf2d406d35222f4c6a5d63fef1d6de1081/balance_diff.json
  • Transit Finance Router v5 verified source: https://bscscan.com/address/0x00000047bb99ea4d791bb749d970de71ee0b1a34#code
  • Pancake V3 USDT/WBNB pool verified source: https://bscscan.com/address/0x36696169c63e42cd08ce11f5deebbcebae652050#code
  • Collector/WBNB PancakePair verified source: https://bscscan.com/address/0xece3f2645ed0910d4a10f4e262e9fe47c481d9de#code
  • Attacker EOA history: https://bscscan.com/address/0xf7552ba0ee5bed0f306658f4a1201f421d703898
  • Collector token tracker: https://bscscan.com/address/0x1f790e7eb953b3f7ead89e5a100ffc3b8d2d2b28
  • Relevant exploit transactions:
    • 0x93ae5f0a121d5e1aadae052c36bc5ecf2d406d35222f4c6a5d63fef1d6de1081
    • 0x45929e8a20c5a2295914f115b9b66f53eccacbd99ad386b55095e08b34be4f90
    • 0x17d81b602b78617a7801dde1918980e9711dc872728c74e7092e5eb0f30a8512
    • 0x67da40bbe1912dbd0992895b6de86712231d44ec3649e532960b71be6f749e82
    • 0x363a1949ada5ff295a3ca41cf907f3ba5ac9b2a6b75445a881194cbc2e30620a
    • 0xae61dafcca3cd21c1cafe680e2f37b9183e4e81a709d2ed5e81ba5491af518f4
    • 0xc05d2c2baddbd8821e10259dc06c98e9428ccf6b1dc428eae3d07e8c809deaad
    • 0x575d1aa68fcf21823b2a5eee3b418352d6b6fb81e15a8d186ae807741965a1b2
    • related: 0x7531af1907940ea7c4419a02b62048b76d8956a4534a7496f91e4cfbfdc4ef15