All incidents

EFLeverVault Whole-Balance Withdrawal

Share
Oct 14, 2022 11:58 UTCAttackLoss: 480.01 WETH, 18.72 ETHPending manual check1 exploit txWindow: Atomic
Estimated Impact
480.01 WETH, 18.72 ETH
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Oct 14, 2022 11:58 UTC → Oct 14, 2022 11:58 UTC

Exploit Transactions

TX 1Ethereum
0x1f1aba5bef04b7026ae3cb1cb77987071a8aff9592e785dd99860566ccad83d1
Oct 14, 2022 11:58 UTCExplorer

Victim Addresses

0xe39fd820b58f83205db1d9225f28105971c3d309Ethereum
0xbae7ec1baaae7d5801ad41691a2175aa11bcba19Ethereum

Loss Breakdown

480.01WETH
18.72ETH

Similar Incidents

Root Cause Analysis

EFLeverVault Whole-Balance Withdrawal

1. Incident Overview TL;DR

EFLeverVault on Ethereum mainnet exposed an anyone-can-take withdrawal path that let a holder of a tiny EF position redeem the vault's entire transient ETH balance instead of only the holder's pro-rata claim. In transaction 0x1f1aba5bef04b7026ae3cb1cb77987071a8aff9592e785dd99860566ccad83d1 at block 15746200, the adversary minted 94980416046706432 EF from a 0.1 ETH deposit and then redeemed 94980415767635144 EF for 480.108083265872754211 ETH in the same transaction. The transaction also returned 15187097051993531005676 WETH to funding EOA 0x56178a0d5f301baf6cf3e1cd53d9863437345bf9, which was 480.008083265872754211 WETH more than it supplied, and sent 18.717054853768549061 ETH to EOA 0x690b9a9e9aa1c9db991c7721a92d351db4fac990.

The root cause is a share-accounting bug in EFLeverVault::withdraw. After deleveraging through Balancer and Aave, the contract computes uint256 to_send = address(this).balance; and sends that entire balance to msg.sender. Because the vault also accepts plain ETH through its payable receive path, attacker-controlled ETH can be made resident in the vault immediately before payout and then drained by any account holding a positive EF balance.

2. Key Background

EFLeverVault at 0xe39fd820b58f83205db1d9225f28105971c3d309 is a leveraged stETH/WETH vault whose public deposit(uint256) function mints EF token 0xbae7ec1baaae7d5801ad41691a2175aa11bcba19. The deposit path mints EF against vault volume, where volume is collateral minus debt. A correct redemption therefore must pay only the redeemer's proportional share of post-unwind net asset value.

The vault is also payable. Its internal _deposit and _withdraw flows convert between ETH, WETH, stETH, Aave positions, and Balancer flash-loan liquidity, so ETH can temporarily or externally exist at the vault address outside EF minting math. That design makes withdrawal pricing highly sensitive to whether payout is derived from explicit share accounting or from raw address(this).balance.

The collected verified source confirms that the paused withdrawal branch uses proportional payout math, but the non-paused branch does not. That asymmetry is the critical clue: the contract's own paused path encodes the invariant that payout should scale with _amount / totalSupply, while the live non-paused path ignores that invariant and transfers the whole ETH balance.

3. Vulnerability Analysis & Root Cause Summary

This incident is an ATTACK root cause, not a benign MEV unwind. The broken invariant is straightforward: for each non-paused withdrawal, the ETH paid to the redeemer must equal that redeemer's proportional claim on vault value after deleveraging. The verified source violates that invariant by computing a proportional loan_amount for the flash-loan unwind but then discarding proportional payout math and sending the full ETH balance resident in the vault.

The relevant code from the collected verified source is:

function withdraw(uint256 _amount) public nonReentrant{
    require(IERC20(ef_token).balanceOf(msg.sender) >= _amount, "not enough balance");
    if (is_paused){
      uint256 to_send = address(this).balance.safeMul(_amount).safeDiv(IERC20(ef_token).totalSupply());
      (bool status, ) = msg.sender.call.value(to_send)("");
      require(status, "transfer eth failed");
      TokenInterfaceERC20(ef_token).destroyTokens(msg.sender, _amount);
      return;
    }

    _earnReward();
    uint256 loan_amount = getDebt().safeMul(_amount).safeDiv(IERC20(ef_token).totalSupply());
    IBalancer(balancer).flashLoan(address(this), tokens, amounts, userData);

    uint256 to_send = address(this).balance;
    (bool status, ) = msg.sender.call.value(to_send)("");
    require(status, "transfer eth failed");
}

The breakpoint is the assignment to_send = address(this).balance in the non-paused path. Once the flash-loan unwind returns and any attacker-controlled ETH is resident in the vault, that entire balance becomes claimable by the withdrawing EF holder. The payable receive path and flash-loan-assisted unwind make that reachable by an unprivileged attacker.

4. Detailed Root Cause Analysis

At pre-state σ_B, just before transaction 0x1f1aba5bef04b7026ae3cb1cb77987071a8aff9592e785dd99860566ccad83d1, EF already had nonzero supply, the vault was deployed and live, and the adversary could interact with the public executor and helper contracts using only public chain state, approvals, and balances. The auditor's cited seed metadata and trace are sufficient to reconstruct the execution from public data.

The deposit side of the invariant is explicit in code. deposit(uint256 _amount) requires _amount == msg.value, computes volume_before = getVolume(), and mints EF as either _amount - fee_amount for initialization or (_amount - fee_amount) * totalSupply / volume_before once supply exists. That means a 0.1 ETH deposit should grant only a tiny claim on the vault's net assets.

The exploit trace then shows the attacker using that tiny claim as a key to the broken withdrawal path. In the seed trace:

EFLeverVault::deposit{value: 100000000000000000}(100000000000000000)
emit CFFDeposit(... eth_amount: 100000000000000000, ef_amount: 94980416046706432 ...)
EFLeverVault::withdraw(94980415767635144)
emit CFFWithdraw(... eth_amount: 480108083265872754211, ef_amount: 94980415767635144 ...)

Those events prove that the minted EF amount was tiny relative to the redeemed ETH amount. The payout was not the depositor's fair share of vault equity; it was the whole ETH balance available at payout time.

The trace also shows the temporary funding leg and downstream profit realization:

WETH9::transferFrom(
  0x56178a0d5F301bAf6CF3e1Cd53d9863437345Bf9,
  0x0E113904cB1d53cAbB324927dffaE2f8F6705111,
  14707088968727658251465
)
...
WETH9::transfer(
  0x56178a0d5F301bAf6CF3e1Cd53d9863437345Bf9,
  15187097051993531005676
)

The balance diff confirms the same result numerically. Funding EOA 0x56178a0d5f301baf6cf3e1cd53d9863437345bf9 netted 480008083265872754211 WETH, and EOA 0x690b9a9e9aa1c9db991c7721a92d351db4fac990 netted 18717054853768549061 wei. Sender 0x26bce6ecb5b10138e4bf14ac0ffcc8727fef3b2e paid 33881454980296116 wei in gas. That is consistent with an ACT exploit whose profit predicate is attacker-cluster gain after temporary funding recovery.

The exploit conditions are therefore concrete and complete:

  1. EFLeverVault is not paused.
  2. The attacker acquires any positive EF balance through the public deposit path.
  3. Additional ETH is made resident in the vault before withdrawal payout executes.
  4. The attacker redeems through the non-paused withdrawal path that pays address(this).balance.

Because all of those conditions are permissionless and observable on-chain, this is a deterministic ACT opportunity.

5. Adversary Flow Analysis

The adversary cluster consisted of sender EOA 0x26bce6ecb5b10138e4bf14ac0ffcc8727fef3b2e, funding EOA 0x56178a0d5f301baf6cf3e1cd53d9863437345bf9, and payout EOA 0x690b9a9e9aa1c9db991c7721a92d351db4fac990. The funding EOA is defensibly linked because it repeatedly appears as the funding address in nearby execute payloads and the exploit transaction pulls WETH from it and returns more WETH to it. The payout EOA is linked because the transaction's native balance diff shows the direct ETH gain there.

The execution sequence was:

  1. The sender called public executor 0xa69babef1ca67a37ffaf7a485dfff3382056e78c, routing into helper 0x42ebdfe801bc8ec1b8f93a77c910beb0f8833d7e and then into helper 0x0e113904cb1d53cabb324927dffae2f8f6705111.
  2. The helper pulled 14707088968727658251465 WETH from 0x56178a0d5f301baf6cf3e1cd53d9863437345bf9.
  3. The helper unwrapped 0.1 ETH and deposited it into EFLeverVault, minting 94980416046706432 EF.
  4. The helper then withdrew 94980415767635144 EF through the non-paused withdrawal path, receiving 480108083265872754211 wei from the vault.
  5. The resulting ETH was wrapped and distributed so that the funding EOA recovered more WETH than it provided and the payout EOA received additional native ETH.

The same broken path remained observable in related transaction 0x160c5950a01b88953648ba90ec0a29b0c5383e055d35a7835d905c53a3dda01e, which redeemed 264550236749673571 EF for 268.896775381681756360 ETH, reinforcing that this was not an isolated accounting anomaly.

6. Impact & Losses

The direct, measured incident loss is the attacker-cluster gain realized in the exploit transaction:

  • 480008083265872754211 wei of WETH (480.008083265872754211 WETH) to 0x56178a0d5f301baf6cf3e1cd53d9863437345bf9
  • 18717054853768549061 wei of ETH (18.717054853768549061 ETH) to 0x690b9a9e9aa1c9db991c7721a92d351db4fac990

The safety impact is broader than the single transaction. The vulnerability lets any positive EF holder capture the vault's whole ETH balance during non-paused withdrawal if extra ETH is resident at the vault. That breaks the core share-accounting invariant for every depositor and makes transient unwind balances or unsolicited ETH transfers socially unsafe.

7. References

  1. Verified EFLeverVault source: 0xe39fd820b58f83205db1d9225f28105971c3d309
  2. Exploit transaction: 0x1f1aba5bef04b7026ae3cb1cb77987071a8aff9592e785dd99860566ccad83d1
  3. Related transaction: 0x160c5950a01b88953648ba90ec0a29b0c5383e055d35a7835d905c53a3dda01e
  4. EF token: 0xbae7ec1baaae7d5801ad41691a2175aa11bcba19
  5. Public executor used by the adversary: 0xa69babef1ca67a37ffaf7a485dfff3382056e78c
  6. Helper contracts reached by the executor: 0x42ebdfe801bc8ec1b8f93a77c910beb0f8833d7e and 0x0e113904cb1d53cabb324927dffae2f8f6705111
  7. Evidence artifacts used for validation: exploit transaction metadata, opcode trace, balance diff, EOA transaction history, and the collected verified source/decompilation artifacts under /workspace/session/artifacts/collector