Orion Pool Double-Count Exploit
Exploit Transactions
Victim Addresses
0xb5599f568d3f3e6113b286d010d2bca40a7745aaEthereum0xe9d1d2a27458378dd6c6f0b2c390807aed2217caBSCLoss Breakdown
Similar Incidents
NOON Pool Drain via Public transfer
33%Dexible selfSwap allowance drain
31%Hegic WBTC Pool Repeated Tranche Withdrawal Exploit
30%V3Utils Arbitrary Call Drain
30%TRU reserve mispricing attack drains WBNB from pool
29%TomInu Reflection Pair Inflation Flashloan Exploit
29%Root Cause Analysis
Orion Pool Double-Count Exploit
1. Incident Overview TL;DR
Orion Protocol was exploited on Ethereum in transaction 0xa6f63fcb6bec8818864d96a5b1bb19e8bd85ee37b2cc916412e720988440b2aa and on BNB Chain in transaction 0xfb153c572e304093023b4f9694ef39135b6ed5b2515453173e81ec02df2e2104. In both cases, the attacker flash-borrowed the output stablecoin, created an OrionPool path that used an attacker-controlled intermediate token, and reentered Orion Exchange during swapThroughOrionPool to deposit the same output token that Orion later counted again as swap proceeds.
The root cause is a double-counting bug in Orion's OrionPool settlement path. depositAsset -> generalDeposit credits the transferred output token to the attacker as an internal Orion balance, while PoolFunctionality._doSwapTokens separately measures the exchange contract's raw output-token balance delta and returns it as swap output. Because both ledgers consume the same transfer, the attacker obtains two claims for one injected transfer and can withdraw preexisting exchange inventory.
2. Key Background
Orion Exchange keeps user balances in 8-decimal internal units even when the underlying tokens use different base-unit decimals. For USDT on Ethereum, a 6-decimal token transfer is scaled into Orion's 8-decimal internal ledger.
When Orion executes swapThroughOrionPool from in-contract inventory, the pool swap is routed with the exchange contract itself as the receiving address. That design choice matters because the router then infers output by comparing the exchange contract's token balance before and after the swap.
OrionPool pair creation is permissionless. An unprivileged attacker can deploy a malicious ERC20-like token, create a [input token, malicious token, output token] path, seed both pairs with minimal liquidity, and cause arbitrary code to run from the malicious token's transfer path during swap execution.
The canonical ACT pre-state for validation is Ethereum block 16542147, immediately before the exploit transaction in block 16542148. The same flaw was independently realized again on BNB Chain, which confirms that the issue is in shared Orion logic rather than in one chain-specific market condition.
3. Vulnerability Analysis & Root Cause Summary
The vulnerability is an accounting error in Orion's pool settlement flow, not a pricing bug in the AMM math. Orion Exchange correctly records a real token transfer when depositAsset is called, but the pool router also treats that same transfer as swap output because it only looks at the exchange contract's ending token balance. The vulnerable breakpoint is the interaction between Exchange.generalDeposit, PoolFunctionality._doSwapTokens, and LibPool.doSwapThroughOrionPool. generalDeposit records the attacker's deposit in Orion's internal ledger, _doSwapTokens computes amountOut as balanceOf(toAuto) - curBalance with toAuto equal to the exchange contract, and LibPool.doSwapThroughOrionPool credits the attacker from that inflated result. The attacker-controlled intermediate token makes the balance mutation happen inside the swap before _doSwapTokens finishes measuring the delta. The exploit is permissionless because it only requires public liquidity, public contract code, and permissionless OrionPool entrypoints.
The vulnerable public components are the Ethereum Orion Exchange proxy 0xb5599f568d3f3e6113b286d010d2bca40a7745aa, the BNB Chain Orion Exchange proxy 0xe9d1d2a27458378dd6c6f0b2c390807aed2217ca, the Ethereum PoolFunctionality router 0x420a50a62b17c18b36c64478784536ba980feac8, the BNB Chain PoolFunctionality router 0xd2997f29b5285ab74bbca62d26c6723a74500183, and the settlement libraries behind LibPool.doSwapThroughOrionPool and LibExchange.creditUserAssets.
The verified Orion implementation exposes the two sides of the bug:
// Verified Orion Exchange implementation
function depositAsset(address assetAddress, uint112 amount) external {
...
generalDeposit(assetAddress, uint112(actualAmount));
}
// Verified Orion PoolFunctionality implementation
function _doSwapTokens(InternalSwapData memory swapData) internal returns (uint256 amountIn, uint256 amountOut) {
uint256 curBalance = IERC20(swapData.path[swapData.path.length - 1]).balanceOf(toAuto);
_swap(...);
amountOut = IERC20(swapData.path[swapData.path.length - 1]).balanceOf(toAuto) - curBalance;
}
And Orion later settles against the returned router amount:
// Verified Orion settlement path
(uint amountOut, uint amountIn) = IPoolFunctionality(d.orionpool_router).doSwapThroughOrionPool(...);
LibExchange.creditUserAssets(d.isFromWallet ? 1 : 0, msg.sender, int(amountIn), d.path[d.path.length - 1], assetBalances, liabilities);
4. Detailed Root Cause Analysis
The Ethereum seed trace shows the exploit at the exact point where the malicious intermediate token transfers into the second OrionPool pair and reenters attacker code. During that callback, the helper contract 0x5061f7e6dfc1a867d945d0ec39ea2a33f772380a calls depositAsset(USDT, 2844766426326) on the Orion Exchange proxy 0xb5599f568d3f3e6113b286d010d2bca40a7745aa.
Ethereum seed trace, attacker helper reentry
0x64Acd987...::transfer(..., 99680123783317)
0x5061F7e6...::deposit()
0xb5599f56...::depositAsset(USDT, 2844766426326)
TetherToken::transferFrom(0x5061F7e6..., 0xb5599f56..., 2844766426326)
emit NewAssetTransaction(..., isDeposit: true, amount: 284476642632600)
At that moment, the exchange's USDT balance rises from 2844766426325 to 5689532852651, and Orion records an internal attacker deposit of 284476642632600 internal units. After the swap resumes, the final OrionPool hop contributes only 99 more USDT to the exchange, but _doSwapTokens measures the full post-swap delta against the earlier curBalance snapshot. The OrionPool event therefore reports a return amount dominated by the injected deposit, not by genuine pool output:
Ethereum seed trace, inflated OrionPool settlement
TetherToken::balanceOf(orion_exchange) -> 5689532852750
emit OrionPoolSwap(..., rt_a: 2844766426425, ...)
0xb5599f56...::getBalance(USDT, 0x5061F7e6...) -> 0x000...20575c2766ddc
0xb5599f56...::withdraw(USDT, 5689532852749)
The same mechanism is present on BNB Chain. The helper contract 0x84452042cb7be650be4eb641025ac3c8a0079b67 deposits 191606635567219518304671 USDT into the exchange during the swap, Orion records it as a deposit, and the later settlement again counts the exchange balance delta as swap proceeds:
BNB Chain seed trace, attacker helper reentry
0xC4da120a...::transfer(..., 6659986639946559786)
0x84452042...::deposit()
0xe9d1d2a2...::depositAsset(USDT, 191606635567219518304671)
BEP20USDT::transferFrom(0x84452042..., 0xe9d1d2a2..., 191606635567219518304671)
emit NewAssetTransaction(..., isDeposit: true, amount: 19160663556721)
This violates the core invariant for pool settlement: the user's claimable output after swapThroughOrionPool must equal the real output produced by the OrionPool route, and independent deposits during execution must not be treated as swap proceeds. Because Orion violates that invariant, one attacker-supplied transfer becomes two spendable claims: one internal deposit balance and one swap credit. The attacker repays the flash loan with the first copy and keeps the second.
The exploit conditions are deterministic from the traces and code: the attacker needs a malicious ERC20-like intermediate token that can execute code during transfer, permissionless OrionPool pairs for [input, malicious, output], a victim exchange with a non-zero output-token inventory, and a source of temporary output-token liquidity such as a public flash swap. Those conditions are all satisfied in the observed transactions.
5. Adversary Flow Analysis
The adversary cluster identified in the evidence consists of sender EOA 0x837962b686fd5a407fb4e5f92e8be86a230484bd, Ethereum helper 0x5061f7e6dfc1a867d945d0ec39ea2a33f772380a, BNB Chain helper 0x84452042cb7be650be4eb641025ac3c8a0079b67, and profit recipient 0x3dabf5e36df28f6064a7c5638d0c4e01539e35f1.
The end-to-end sequence is:
- The helper flash-borrows the output stablecoin from a public AMM pair.
- The helper seeds attacker-created OrionPool pairs that route from the input token through a malicious intermediate token to the output token.
- The helper starts
swapThroughOrionPoolon Orion Exchange. - The malicious path token's transfer callback reenters attacker code and calls
depositAssetfor the output token while Orion is still inside the swap. - OrionPool finishes, computes output from the exchange balance delta, and returns an amount that already includes the reentrant deposit.
- Orion credits or pays that inflated amount, the helper withdraws the doubled claim, repays the flash loan, and forwards the remainder to the profit recipient.
Both observed transactions follow that structure. On Ethereum, the attack culminates in a USDT withdrawal from the exchange and a later WETH profit transfer to 0x3dabf5e36df28f6064a7c5638d0c4e01539e35f1. On BNB Chain, the attack culminates in a large USDT transfer to the same profit-recipient address.
6. Impact & Losses
The directly observed victim contracts are the Orion Exchange proxies:
- Ethereum:
0xb5599f568d3f3e6113b286d010d2bca40a7745aa - BNB Chain:
0xe9d1d2a27458378dd6c6f0b2c390807aed2217ca
The balance diffs show deterministic protocol losses:
- Ethereum exchange loss:
2844766426324USDT base units, token decimals6 - BNB Chain exchange loss:
191606635567209086337063USDT base units, token decimals18
The attacker also realized observable downstream profit:
- Ethereum profit recipient
0x3dabf5e36df28f6064a7c5638d0c4e01539e35f1gained1651247397448511042353WETH - BNB Chain profit recipient
0x3dabf5e36df28f6064a7c5638d0c4e01539e35f1gained191030285531192719854894USDT
For the canonical Ethereum ACT framing, the validator confirmed that the direct profit recipient starts with zero WETH and ends with 1651247397448511042353 WETH in the seed balance diff, so the profit predicate is fully deterministic.
7. References
- Ethereum exploit transaction:
0xa6f63fcb6bec8818864d96a5b1bb19e8bd85ee37b2cc916412e720988440b2aa - BNB Chain exploit transaction:
0xfb153c572e304093023b4f9694ef39135b6ed5b2515453173e81ec02df2e2104 - Ethereum Orion Exchange implementation:
0x98a877bb507f19eb43130b688f522a13885cf604 - Ethereum Orion PoolFunctionality:
0x420a50a62b17c18b36c64478784536ba980feac8 - BNB Chain Orion Exchange implementation:
0x34233e37717451562edd72dc7edc4d0a7128c010 - BNB Chain Orion PoolFunctionality:
0xd2997f29b5285ab74bbca62d26c6723a74500183 - Ethereum balance diff reviewed by the validator: exchange USDT delta
-2844766426324, profit-recipient WETH delta+1651247397448511042353 - BNB Chain balance diff reviewed by the validator: exchange USDT delta
-191606635567209086337063, profit-recipient USDT delta+191030285531192719854894