All incidents

Annex Liquidator WBNB Drain

Share
Nov 18, 2022 15:43 UTCAttackLoss: 7.23 WBNBPending manual check2 exploit txWindow: 6m 6s
Estimated Impact
7.23 WBNB
Label
Attack
Exploit Tx
2
Addresses
1
Attack Window
6m 6s
Nov 18, 2022 15:43 UTC → Nov 18, 2022 15:49 UTC

Exploit Transactions

TX 1BSC
0x3b8e62a3ac6bdaec8688abc6941009b7efc7ab6aed6571c4cf61b710836a2ee3
Nov 18, 2022 15:43 UTCExplorer
TX 2BSC
0x3757d177482171dcfad7066c5e88d6f0f0fe74b28f32e41dd77137cad859c777
Nov 18, 2022 15:49 UTCExplorer

Victim Addresses

0xe65e970f065643ba80e5822edff483a1d75263e3BSC

Loss Breakdown

7.23WBNB

Similar Incidents

Root Cause Analysis

Annex Liquidator WBNB Drain

1. Incident Overview TL;DR

An unprivileged BSC attacker exploited Annex's public Liquidator contract at 0xe65e970f065643ba80e5822edff483a1d75263e3 and drained its stored WBNB in transaction 0x3757d177482171dcfad7066c5e88d6f0f0fe74b28f32e41dd77137cad859c777 at block 23165447. The attacker first deployed helper contract 0xb08efc0933a4552d4294c729fed5176c5e3e63fd in transaction 0x3b8e62a3ac6bdaec8688abc6941009b7efc7ab6aed6571c4cf61b710836a2ee3, then used that helper to borrow WBNB from public Pancake pair 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae, create a fresh TOK/WBNB pair 0x29dabb363f1366969bd339aee8b1c291b62cf82a, and route a flash-swap callback into the victim.

The root cause was an authentication failure in Liquidator.pancakeCall(address,uint256,uint256,bytes). The function verified only that msg.sender was the Pancake factory pair for (token0, token1), but it never verified that the callback originated from the Liquidator's own liquidate() flow and never verified that attacker-supplied repayAToken and seizeAToken were genuine Annex markets. That let the attacker make the victim approve WBNB to the attacker helper, call attacker-controlled liquidateBorrow and redeem functions, repay the attacker-created pair with victim funds, and pull the victim's remaining WBNB by allowance.

2. Key Background

Annex deployed a public liquidation helper called Liquidator to automate Pancake flash-swap liquidations. The relevant callback surface is pancakeCall(address sender,uint256 amount0,uint256 amount1,bytes data), which Pancake pairs invoke on the swap recipient whenever swap(..., data) uses non-empty data.

The victim contract already held realized inventory on its own balance sheet before the exploit. At block 23165446, immediately before the exploit block, the Liquidator held exactly 7225851763293057027 wei of WBNB. The exploit therefore did not need to seize value from a borrower position first; it only needed to force the Liquidator into a privileged callback branch that could touch its stored WBNB.

The relevant public protocol components were all permissionless: Pancake factory 0xca143ce32fe78f1f7019d7d551a6402fc5350c73, flash source pair 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae, and the factory's pair creation flow. Any unprivileged user could deploy a helper contract, create a new token, create a new Pancake pair, and trigger a pancakeCall callback with attacker-chosen calldata.

3. Vulnerability Analysis & Root Cause Summary

The incident is an on-chain attack, not a pricing anomaly or privileged compromise. Annex's Liquidator exposed a public callback intended for its own flash-swap workflow, but the authentication was too weak: the code only checked whether msg.sender matched PancakeFactory.getPair(token0, token1) for the tokens reported by the caller pair. That check does not distinguish a legitimate pair used by Annex from an attacker-created pair that also exists in the factory.

The callback also trusted attacker-decoded repayAToken and seizeAToken values. In the exploit, both fields were set to the attacker helper contract, which implemented the small subset of selectors that the Liquidator expected. Once execution entered the repayAToken == seizeAToken branch, the victim approved WBNB to the attacker helper, called liquidateBorrow on that helper, called redeem on that helper, and finally transferred WBNB back to the attacker-controlled pair as if a legitimate flash-swap debt existed.

The violated invariant is straightforward: only a callback tied to a liquidation initiated by the Liquidator itself, and only for genuine Annex markets, may execute code paths that approve or transfer the Liquidator's assets. The code-level breakpoint is the beginning of pancakeCall, where the victim accepts any canonical pair and immediately performs privileged external calls on untrusted addresses.

require(msg.sender == IPancakeFactory(FACTORY).getPair(token0, token1));
IERC20(source).safeApprove(repayAToken, amount);
ABep20(repayAToken).liquidateBorrow(borrower, amount, seizeAToken);
ABep20(seizeAToken).redeem(IERC20(seizeAToken).balanceOf(address(this)));

The function never enforced sender == address(this) and never checked that repayAToken and seizeAToken belonged to Annex. Those omissions made the drain deterministic.

4. Detailed Root Cause Analysis

The exploit transaction was sent by EOA 0x40fb8d322cc4887d6c2bf886755300ea79f6a874 to helper 0xb08efc0933a4552d4294c729fed5176c5e3e63fd. Metadata for 0x3757d177482171dcfad7066c5e88d6f0f0fe74b28f32e41dd77137cad859c777 shows the helper call targeted the victim 0xe65e970f065643ba80e5822edff483a1d75263e3, WBNB 0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c, and flash source pair 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae.

Inside the transaction, the helper borrowed WBNB from the public flash source, created a new TOK/WBNB pair, seeded it with liquidity, and then called that pair's swap function so the pair itself would invoke the victim callback. The decisive trace segment is below:

PancakePair::swap(..., 0xe65E970F065643bA80E5822edfF483A1d75263E3, data)
  WBNB::transfer(victim, 7225851763293057027)
  Liquidator::pancakeCall(attacker_helper, 0, 7225851763293057027, data)
    PancakeFactory::getPair(fakeToken, WBNB) => attacker_created_pair
    WBNB::approve(attacker_helper, 7225851763293057027)
    attacker_helper::liquidateBorrow(...)
    attacker_helper::redeem(0)
    WBNB::transfer(attacker_created_pair, 7247594546933858603)

That trace proves three critical facts. First, the callback reached the victim from the attacker-created pair, not from any Annex-controlled action. Second, the victim approved the attacker helper to spend WBNB. Third, the victim repaid the attacker-created pair with its own WBNB.

The attacker then used the newly granted allowance to pull the victim's residual WBNB. Receipt evidence described by the auditor records a second WBNB outflow of 7204108979652255451 wei from the victim to the attacker helper. Because the pair itself was also attacker-controlled, the helper later burned the LP position and recovered the WBNB that the victim had transferred into that pair as purported flash-swap repayment.

This sequence matches the root-cause statement in full: unauthenticated callback origin plus unvalidated market addresses let the attacker convert the Liquidator's stored WBNB into attacker profit. The exploit conditions were minimal and public: the Liquidator needed a positive WBNB balance, Pancake pair creation had to remain public, and the attacker needed only a helper contract implementing the selectors the victim invoked.

5. Adversary Flow Analysis

The attacker lifecycle had two transactions. In 0x3b8e62a3ac6bdaec8688abc6941009b7efc7ab6aed6571c4cf61b710836a2ee3, the adversary EOA deployed helper 0xb08efc0933a4552d4294c729fed5176c5e3e63fd. That helper later served three roles at once: exploit orchestrator, fake market contract, and temporary profit-holding contract.

In the seed exploit transaction 0x3757d177482171dcfad7066c5e88d6f0f0fe74b28f32e41dd77137cad859c777, the helper executed the following sequence:

  1. Borrow WBNB from public Pancake pair 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae.
  2. Create attacker-controlled TOK/WBNB pair 0x29dabb363f1366969bd339aee8b1c291b62cf82a through the public factory.
  3. Add liquidity so that the new pair becomes a canonical factory pair and can satisfy the victim's lone getPair check.
  4. Call the new pair's swap with non-empty data, causing the pair to send WBNB to the victim and invoke Liquidator.pancakeCall.
  5. Provide callback data that decodes to (borrower, repayAToken, seizeAToken) = (attacker_helper, attacker_helper, attacker_helper).
  6. Let the victim approve WBNB to the helper and make attacker-controlled liquidateBorrow and redeem calls.
  7. Pull the victim's remaining WBNB by allowance.
  8. Burn the LP position to recover WBNB that the victim sent to the attacker pair.
  9. Repay the original flash source pair.
  10. Transfer the remaining WBNB to the attacker EOA as profit.

This is ACT because every step used permissionless public surfaces. No privileged key, private orderflow, or hidden state was required. The adversary-related accounts are also defensible from the evidence set: the EOA funded and sent the exploit, the helper was created by that EOA, and the pair was created inside the exploit transaction for the attack path.

6. Impact & Losses

The victim Liquidator lost its entire pre-state WBNB inventory:

{
  "token_symbol": "WBNB",
  "amount": "7225851763293057027",
  "decimal": 18
}

The attacker EOA's native BNB balance decreased by 22926264000000000 wei in gas, but the attacker cluster realized 7198525231262800914 wei of WBNB profit by the end of the exploit. The root cause therefore had direct asset-loss impact on Annex's liquidation infrastructure, not merely a transient accounting inconsistency.

7. References

  • Exploit tx: 0x3757d177482171dcfad7066c5e88d6f0f0fe74b28f32e41dd77137cad859c777
  • Helper deployment tx: 0x3b8e62a3ac6bdaec8688abc6941009b7efc7ab6aed6571c4cf61b710836a2ee3
  • Victim contract: 0xe65e970f065643ba80e5822edff483a1d75263e3
  • Attacker EOA: 0x40fb8d322cc4887d6c2bf886755300ea79f6a874
  • Attacker helper: 0xb08efc0933a4552d4294c729fed5176c5e3e63fd
  • Attacker-created pair: 0x29dabb363f1366969bd339aee8b1c291b62cf82a
  • Flash source pair: 0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae
  • Pancake factory: 0xca143ce32fe78f1f7019d7d551a6402fc5350c73
  • Seed metadata: /workspace/session/artifacts/collector/seed/56/0x3757d177482171dcfad7066c5e88d6f0f0fe74b28f32e41dd77137cad859c777/metadata.json
  • Seed trace: /workspace/session/artifacts/collector/seed/56/0x3757d177482171dcfad7066c5e88d6f0f0fe74b28f32e41dd77137cad859c777/trace.cast.log
  • Seed balance diff: /workspace/session/artifacts/collector/seed/56/0x3757d177482171dcfad7066c5e88d6f0f0fe74b28f32e41dd77137cad859c777/balance_diff.json
  • Verified Liquidator source: https://api.etherscan.io/v2/api?chainid=56&module=contract&action=getsourcecode&address=0xe65e970f065643ba80e5822edff483a1d75263e3&apikey=QCKY4E9UXVMQFIKPAH9DSKFX4TK2H6TUXG
  • Helper creation record: https://api.etherscan.io/v2/api?chainid=56&module=contract&action=getcontractcreation&contractaddresses=0xb08efc0933a4552d4294c729fed5176c5e3e63fd&apikey=QCKY4E9UXVMQFIKPAH9DSKFX4TK2H6TUXG