All incidents

Elephant Treasury Sweep Sandwich

Share
Dec 06, 2023 10:27 UTCAttackLoss: 114,393.96 BUSDPending manual check1 exploit txWindow: Atomic
Estimated Impact
114,393.96 BUSD
Label
Attack
Exploit Tx
1
Addresses
3
Attack Window
Atomic
Dec 06, 2023 10:27 UTC → Dec 06, 2023 10:27 UTC

Exploit Transactions

TX 1BSC
0xd423ae0e95e9d6c8a89dcfed243573867e4aad29ee99a9055728cbbe0a523439
Dec 06, 2023 10:27 UTCExplorer

Victim Addresses

0x8cf0a553ab3896e4832ebcc519a7a60828ab5740BSC
0xcb5a02bb3a38e92e591d323d6824586608ce8ce4BSC
0xe283d0e3b8c102badf5e8166b73e02d96d92f688BSC

Loss Breakdown

114,393.96BUSD

Similar Incidents

Root Cause Analysis

Elephant Treasury Sweep Sandwich

1. Incident Overview TL;DR

On BNB Smart Chain block 34114761, transaction 0xd423ae0e95e9d6c8a89dcfed243573867e4aad29ee99a9055728cbbe0a523439 used four public Pancake V3 flash loans to distort the BUSD/WBNB price immediately before calling Elephant’s public sweep contract at 0x8cf0a553ab3896e4832ebcc519a7a60828ab5740. The sweep spent treasury BUSD from 0xcb5a02bb3a38e92e591d323d6824586608ce8ce4 through PancakeRouter 0x10ed43c718714eb63d5aa57b78b54704e256024e and accepted any output because the router call used amountOutMin = 0. The manipulated trade delivered only 195809938538532916514 raw ELEPHANT units to the graveyard 0xaf0980a0f52954777c491166e7f40db2b6fbb4fc, while a clean pre-state execution of the same public sweep would have delivered 592479600269094285940.

The root cause is a permissionless treasury-buy path that trusted live AMM spot quotes and executed with zero slippage protection. Any unprivileged adversary could sandwich the sweep with transient reserve manipulation, force the treasury to buy at a bad price, then unwind the manipulation and retain the difference as profit.

2. Key Background

Elephant uses an unverified sweep contract at 0x8cf0a553ab3896e4832ebcc519a7a60828ab5740 to pull BUSD from its treasury and buy ELEPHANT for the graveyard address. The on-chain bytecode reconstruction shows the contract exposes public selectors for sweep(), available(), bestPath(uint256), collateralTreasury(), coreTreasury(), and collateralRouter(). Direct block-34114760 RPC calls confirm:

  • available() = 592479600269094285940
  • collateralTreasury() = 0xCb5a02BB3a38e92E591d323d6824586608cE8cE4
  • coreTreasury() = 0xAF0980A0f52954777C491166E7F40DB2B6fBb4Fc
  • collateralRouter() = 0x10ED43C718714eb63d5aA57B78B54704E256024E
  • whitelist(0x1111111111111111111111111111111111111111) = false

The verified Treasury contract at 0xcb5a02bb3a38e92e591d323d6824586608ce8ce4 is whitelist-gated. Its source proves that only whitelisted callers can withdraw treasury BUSD, and a pre-state call confirmed the sweep contract itself was whitelisted, so the public sweep entrypoint could legally pull treasury funds.

Verified Treasury source:

modifier onlyWhitelisted() {
    require(whitelist[msg.sender], "not whitelisted");
    _;
}

function withdraw(uint256 _amount) public onlyWhitelisted {
    require(token.transfer(_msgSender(), _amount));
}

The public liquidity used for manipulation was available from four Pancake V3 pools whose token1 BUSD balances at the pre-state summed to 5439162800956148738427381 raw BUSD units.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK, not benign MEV. The vulnerable invariant is that treasury collateral sweeps must not execute at a caller-manipulable AMM price and must not accept arbitrary output. Elephant violated that invariant in two ways. First, sweep() was callable by an unprivileged external address even though the sweep contract itself had treasury withdrawal rights. Second, the contract derived its route from live Pancake getAmountsOut quotes and then called the router with amountOutMin = 0. That combination let an attacker transiently distort the BUSD/WBNB pair, force the treasury to buy ELEPHANT at the manipulated price, and realize deterministic profit when the manipulation was unwound.

The reconstructed sweep logic is:

function sweep() external {
    require(!isPaused());
    uint256 amount = available();
    if (amount > liquidityThreshold) {
        Treasury(collateralTreasury).withdraw(amount);
        router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            amount,
            0,
            bestPath(amount),
            coreTreasury,
            block.timestamp
        );
        lastSweep = block.timestamp;
    }
}

The decisive breakpoint is the zero-slippage router call. Once the attacker distorted the BUSD/WBNB reserve ratio, the treasury buy became value-destructive by construction.

4. Detailed Root Cause Analysis

The bytecode reconstruction of 0x8cf0... identifies selector 0x35faa416 as sweep() and shows that the function body begins by checking the paused flag but does not enforce caller authorization. The same reconstruction maps storage slot 11 to treasury 0xcb5a..., slot 12 to graveyard 0xaf09..., and slot 14 to PancakeRouter 0x10ed.... Internal available() reads the treasury BUSD balance, applies daily_apr and elapsed-time logic, and produces the spendable amount. Internal bestPath(uint256) queries live router prices over direct and WBNB-intermediated paths.

Sweep reconstruction from on-chain bytecode:

0x0e9c-0x0f00  CALL Treasury.withdraw(amount)
0x1db9-0x1ed6  approve(BUSD, router) and approve(ELEPHANT, router)
0x1ed7-0x1f8b  call selector 0x5c11d795 with:
               (amountIn, 0, bestPath(amountIn), graveyard, block.timestamp)

The clean pre-state sweep trace shows what should have happened absent manipulation. A random non-whitelisted caller successfully invoked sweep(), the treasury transferred 193615310121918999528294 raw BUSD to the sweep contract, and PancakeRouter would have bought 592479600269094285940 raw ELEPHANT for the graveyard over the [BUSD, WBNB, ELEPHANT] path.

In the incident transaction, the attacker EOA 0xbbcc139933d1580e7c40442e09263e90e6f1d66d called helper contract 0x69bd13f775505989883768ebd23d528c708d6bcf, which nested four Pancake V3 flash loans. The helper borrowed all available BUSD from pools 0x22536030..., 0x4f3126d5..., 0x85faac65..., and 0x369482c7..., then swapped the full 5439162800956148738427381 raw BUSD into 13300112326793611701704 raw WBNB on the manipulated BUSD/WBNB pair 0x58f876857a02d6762e0101bb5c46a8c1ed44dc16.

The incident trace then records the vulnerable sweep path:

0x8Cf0...::sweep()
  PancakeRouter::getAmountsOut(193627110417113639371428, [BUSD, WBNB, ELEPHANT])
  Treasury::withdraw(193627110417113639371428)
  PancakeRouter::swapExactTokensForTokensSupportingFeeOnTransferTokens(
      193627110417113639371428,
      0,
      [BUSD, WBNB, ELEPHANT],
      0xAF0980A0f52954777C491166E7F40DB2B6fBb4Fc,
      1701858478
  )

Because the BUSD/WBNB reserves had already been skewed, the treasury spent 193627110417113639371428 raw BUSD but the graveyard received only 195809938538532916514 raw ELEPHANT. After the sweep completed, the attacker sold WBNB back into BUSD, repaid the flash pools plus fees, and transferred the residual 114393958203315523088130 raw BUSD units to the originating EOA. Valuing gas at the pre-state BUSD/WBNB price yields a gas cost of 4099702450846898449 raw BUSD units and a net post-gas profit of 114389858500864676189681.

5. Adversary Flow Analysis

The adversary execution was a single-transaction ACT sequence:

  1. The EOA 0xbbcc139933d1580e7c40442e09263e90e6f1d66d sent one normal transaction to helper contract 0x69bd13f775505989883768ebd23d528c708d6bcf.
  2. The helper nested four Pancake V3 flash loans and accumulated 5439162800956148738427381 raw BUSD.
  3. The helper swapped that BUSD into WBNB to move the BUSD/WBNB price against the treasury.
  4. The helper called the public sweep() function on 0x8cf0....
  5. The sweep contract, already whitelisted by the treasury, withdrew treasury BUSD and executed the zero-slippage router swap along the manipulated path.
  6. The helper swapped the WBNB back into BUSD after the treasury trade had moved through the pool.
  7. The helper repaid all four flash pools and forwarded the remaining BUSD to the EOA.

The balance diff and trace identify the attacker-controlled addresses and the profit realization path without requiring any attacker-private artifact, calldata template, or privileged state.

6. Impact & Losses

The measurable loss is treasury BUSD value transferred to the attacker through manipulated execution quality. The attacker received 114393958203315523088130 raw BUSD units in the final transfer and remained net positive after gas. The treasury at 0xcb5a02bb3a38e92e591d323d6824586608ce8ce4 spent 193627110417113639371428 raw BUSD in the manipulated sweep, while the graveyard received only 195809938538532916514 raw ELEPHANT instead of the 592479600269094285940 raw ELEPHANT obtainable from the clean pre-state.

Affected public protocol components:

  • Sweep contract: 0x8cf0a553ab3896e4832ebcc519a7a60828ab5740
  • Treasury: 0xcb5a02bb3a38e92e591d323d6824586608ce8ce4
  • Elephant token: 0xe283d0e3b8c102badf5e8166b73e02d96d92f688

7. References

  1. Incident transaction: 0xd423ae0e95e9d6c8a89dcfed243573867e4aad29ee99a9055728cbbe0a523439
  2. Incident execution trace for the attacker helper and sweep path
  3. Incident balance diff showing treasury loss and final attacker BUSD receipt
  4. Clean pre-state sweep() execution trace from block 34114760
  5. Pre-state pool and permission context, including available() and random-caller whitelist checks
  6. Sweep contract bytecode reconstruction and disassembly for selectors, storage slots, and router call shape
  7. Verified Treasury source proving withdraw(uint256) is whitelist-gated
  8. Verified Elephant token source for protocol context