Transit Router V5 Drain
Exploit Transactions
0x93ae5f0a121d5e1aadae052c36bc5ecf2d406d35222f4c6a5d63fef1d6de10810x45929e8a20c5a2295914f115b9b66f53eccacbd99ad386b55095e08b34be4f900x17d81b602b78617a7801dde1918980e9711dc872728c74e7092e5eb0f30a85120x67da40bbe1912dbd0992895b6de86712231d44ec3649e532960b71be6f749e820x363a1949ada5ff295a3ca41cf907f3ba5ac9b2a6b75445a881194cbc2e30620a0xae61dafcca3cd21c1cafe680e2f37b9183e4e81a709d2ed5e81ba5491af518f40xc05d2c2baddbd8821e10259dc06c98e9428ccf6b1dc428eae3d07e8c809deaad0x575d1aa68fcf21823b2a5eee3b418352d6b6fb81e15a8d186ae807741965a1b2Victim Addresses
0x00000047bb99ea4d791bb749d970de71ee0b1a34BSCLoss Breakdown
Similar Incidents
KEKESANTA Pair-to-Router Double-Credit Liquidity Drain
36%GGGTOKEN Treasury Drain via receive()
35%LinkdaoDex USDT Pair Drain
35%DBW Static-Income LP Drain
35%Sheep Burn Reserve Drain
34%SwapX Arbitrary transferFrom Approval Drain on BNB Chain
34%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...: USDT43841867959016089190183 -> 0 - real Pancake V3 pool
0x36696169...: USDT9485011174029608430218334 -> 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...and0x17d81b60...call PancakeSwap Router v2removeLiquidityETH, converting LP into Collector plus BNB.0x67da40bb...approves Collector to PancakeSwap Router v2.0x363a1949...,0xae61dafc...,0xc05d2c2b..., and0x575d1aa6...callswapExactTokensForETH, 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:
-
0x93ae5f0a121d5e1aadae052c36bc5ecf2d406d35222f4c6a5d63fef1d6de1081(block34506417) The attacker sends0.01 BNBto0x7d758372.... That call triggers Transit Router v5, forges the first V3 hop, drains the router's full USDT balance into the real Pancake V3 pool, receives173907186477338745776wei of WBNB into pair0xece3f264..., and callssync()on the pair. -
0x45929e8a20c5a2295914f115b9b66f53eccacbd99ad386b55095e08b34be4f90(block34506662) PancakeSwap Router v2removeLiquidityETHreturns118685695334069069wei of BNB to the attacker. -
0x17d81b602b78617a7801dde1918980e9711dc872728c74e7092e5eb0f30a8512(block34506699) A secondremoveLiquidityETHreturns118567009638735000649wei of BNB. -
0x67da40bbe1912dbd0992895b6de86712231d44ec3649e532960b71be6f749e82(block34506928) The attacker approves PancakeSwap Router v2 to spend Collector token. -
0x363a1949ada5ff295a3ca41cf907f3ba5ac9b2a6b75445a881194cbc2e30620a(block34506934)swapExactTokensForETHreturns33744840020043670406wei of BNB. -
0xae61dafcca3cd21c1cafe680e2f37b9183e4e81a709d2ed5e81ba5491af518f4(block34506981) A second successfulswapExactTokensForETHreturns2843285233093763036wei of BNB. -
0xc05d2c2baddbd8821e10259dc06c98e9428ccf6b1dc428eae3d07e8c809deaad(block34507035) A third successfulswapExactTokensForETHreturns829485654209850115wei of BNB. -
0x575d1aa68fcf21823b2a5eee3b418352d6b6fb81e15a8d186ae807741965a1b2(block34507147) A fourth successfulswapExactTokensForETHreturns231107568308683003wei of BNB. -
0x7531af1907940ea7c4419a02b62048b76d8956a4534a7496f91e4cfbfdc4ef15(block34508074, related) The attacker sends100 BNBto0x0d5550d52428e7e3175bfc9550207e4ad3859b17, 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:
0x93ae5f0a121d5e1aadae052c36bc5ecf2d406d35222f4c6a5d63fef1d6de10810x45929e8a20c5a2295914f115b9b66f53eccacbd99ad386b55095e08b34be4f900x17d81b602b78617a7801dde1918980e9711dc872728c74e7092e5eb0f30a85120x67da40bbe1912dbd0992895b6de86712231d44ec3649e532960b71be6f749e820x363a1949ada5ff295a3ca41cf907f3ba5ac9b2a6b75445a881194cbc2e30620a0xae61dafcca3cd21c1cafe680e2f37b9183e4e81a709d2ed5e81ba5491af518f40xc05d2c2baddbd8821e10259dc06c98e9428ccf6b1dc428eae3d07e8c809deaad0x575d1aa68fcf21823b2a5eee3b418352d6b6fb81e15a8d186ae807741965a1b2- related:
0x7531af1907940ea7c4419a02b62048b76d8956a4534a7496f91e4cfbfdc4ef15