All incidents

EFVault Withdraw Under-Burn

Share
Aug 09, 2023 07:34 UTCAttackLoss: 160.3 WETHPending manual check1 exploit txWindow: Atomic
Estimated Impact
160.3 WETH
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Aug 09, 2023 07:34 UTC → Aug 09, 2023 07:34 UTC

Exploit Transactions

TX 1Ethereum
0x6e6e556a5685980317cb2afdb628ed4a845b3cbd1c98bdaffd0561cb2c4790fa
Aug 09, 2023 07:34 UTCExplorer

Victim Addresses

0x5655c442227371267c165101048e4838a762675dEthereum

Loss Breakdown

160.3WETH

Similar Incidents

Root Cause Analysis

EFVault Withdraw Under-Burn

1. Incident Overview TL;DR

An attacker exploited EFVault in Ethereum mainnet transaction 0x6e6e556a5685980317cb2afdb628ed4a845b3cbd1c98bdaffd0561cb2c4790fa at block 17875886. The exploit used a permissionless flash-loan-funded deposit into EFVault, then abused the vault's withdrawal flow to receive assets before the corresponding shares were irrevocably burned. During the withdrawal receiver callback, the attacker moved nearly all freshly minted EFVault shares to a helper contract. When control returned, EFVault burned only the tiny remaining balance and the helper reused the transferred shares for a second withdrawal. The result was a deterministic extra extraction of 160299820938666181465 wei of WETH-denominated value from the vault.

2. Key Background

EFVault at 0x5655c442227371267c165101048e4838a762675d is an ERC20 share vault whose redemption math depends on totalAssets() reported by an external controller. Deposits mint shares against controller-reported assets, and withdrawals redeem shares into ETH/WETH-routed value through the controller path.

The critical detail is that EFVault does not settle share destruction before it performs the external withdrawal. In the victim source, withdraw(uint256 assets, address receiver) computes the number of shares to burn, then calls the controller, and only later burns shares from msg.sender. Because the receiver can be an arbitrary contract and the vault token itself remains transferable while the call stack is still inside withdraw, the withdrawing account can change its share balance after authorization but before final settlement.

3. Vulnerability Analysis & Root Cause Summary

The bug is a checks-effects-interactions violation in EFVault redemption logic. withdraw first validates that the caller's current share balance can support the requested asset amount and derives shares = (totalSupply() * assets) / totalAssets(). Instead of burning those shares immediately, it executes the external call (uint256 withdrawn, uint256 fee) = IController(controller).withdraw(assets, receiver). After control returns, the vault checks whether balanceOf(msg.sender) < shares and, if so, silently reduces shares to the caller's reduced post-callback balance before calling _burn. That branch converts what should be a hard failure into an under-burn. Because the attacker can transfer the original shares away during the receiver callback and later redeem them from a helper contract, asset redemption becomes decoupled from share destruction. The root cause is therefore the combination of external interaction before burn finalization and the fallback share-cap logic that tolerates the caller's balance dropping mid-withdrawal.

4. Detailed Root Cause Analysis

The victim code in EFVault.withdraw contains the vulnerable sequence:

uint256 totalDeposit = convertToAssets(balanceOf(msg.sender));
require(assets <= totalDeposit, "EXCEED_TOTAL_DEPOSIT");

shares = (totalSupply() * assets) / totalAssets();
(uint256 withdrawn, uint256 fee) = IController(controller).withdraw(assets, receiver);
require(withdrawn > 0, "INVALID_WITHDRAWN_SHARES");

if (balanceOf(msg.sender) < shares) shares = balanceOf(msg.sender);
_burn(msg.sender, shares);

Once assets <= totalDeposit passes and shares is computed, the vault has already determined the exact redemption cost. The next action should be to consume or lock those shares. Instead, the controller withdrawal sends value to the attacker-controlled receiver while the original sender still owns transferable EFVault shares.

The seed trace shows the attacker-funded deposit and the two withdrawals inside one transaction. The first stage deposits 320599641877332363469 wei into EFVault and mints 295761226831070116848 shares. The attacker then triggers a withdrawal for the full deposited amount. During the receiver callback, the attacker moves almost all shares to helper contract 0xcfd26fe5fe6028539802275c1cc6e9325aa2e3b7, leaving only dust in the original withdrawing address. The balance-diff artifact records that helper ending the transaction with EFVault balance delta +1, which matches the share-cap logic leaving dust after the first withdrawal settles.

The transaction trace further records a second vault Withdraw event for the helper:

emit Withdraw(
  asset: WETH9,
  caller: 0xCFD26FE5Fe6028539802275C1CC6e9325aA2e3b7,
  owner: 0xCFD26FE5Fe6028539802275C1CC6e9325aA2e3b7,
  assets: 160299820938666181465,
  shares: 295761226831070115847,
  fee: 0
)

That event proves the transferred shares remained valid after the first withdrawal had already returned the original deposit-sized amount. The attacker contract later repaid 10005000000000000000000 wei to the Uniswap V3 flash pool and still retained positive WETH, while the sender EOA paid only gas. This is consistent with the balance-diff artifact and the stated profit delta of 159.324268834097852251 WETH. The exploit is therefore fully deterministic from public state: a flash loan provides temporary capital, EFVault authorizes a withdrawal before burning shares, callback-time share transfer defeats the intended burn, and the helper redeems the recycled shares for extra value.

5. Adversary Flow Analysis

The adversary cluster consisted of EOA 0xee4b3dd20902fa3539706f25005fa51d3b7bdf1b, primary attacker contract 0xfe141c32e36ba7601d128f0c39dedbe0f6abb983, and helper contract 0xcfd26fe5fe6028539802275c1cc6e9325aa2e3b7.

The traced execution flow was:

1. Borrow WETH from Uniswap V3 pool 0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640.
2. Deposit 320599641877332363469 wei into EFVault and mint 295761226831070116848 shares.
3. Call EFVault.withdraw for the deposited asset amount with an attacker-controlled receiver.
4. In the receiver callback, transfer nearly all shares to the helper contract.
5. Return to EFVault, which caps the burn to the tiny remaining sender balance and burns only dust.
6. Use the helper-held shares to perform a second withdrawal for 160299820938666181465 wei.
7. Wrap proceeds to WETH, repay the flash loan plus fee, and keep the remainder as profit.

This satisfies the ACT model. No privileged role was used, the flash source was a public pool, and the only required capability was deploying contracts able to receive the first withdrawal and transfer shares during the callback window.

6. Impact & Losses

The concrete measurable loss in the evidence is the second withdrawal enabled by recycled shares. EFVault released an additional 160299820938666181465 wei of WETH-denominated value that was not backed by new share destruction. The victim vault at 0x5655c442227371267c165101048e4838a762675d therefore suffered a direct depletion of underlying assets, and the attacker cluster finished the transaction with positive net profit after repaying the public flash loan.

7. References

  • Seed transaction: 0x6e6e556a5685980317cb2afdb628ed4a845b3cbd1c98bdaffd0561cb2c4790fa
  • Victim vault: 0x5655c442227371267c165101048e4838a762675d
  • Flash-loan pool: 0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640
  • Primary attacker contract: 0xfe141c32e36ba7601d128f0c39dedbe0f6abb983
  • Helper contract: 0xcfd26fe5fe6028539802275c1cc6e9325aa2e3b7

Victim code excerpt source: EFVault Vault.sol

shares = (totalSupply() * assets) / totalAssets();
(uint256 withdrawn, uint256 fee) = IController(controller).withdraw(assets, receiver);
if (balanceOf(msg.sender) < shares) shares = balanceOf(msg.sender);
_burn(msg.sender, shares);

Trace and balance evidence used for validation:

  • Seed transaction metadata and trace under the collector seed artifacts
  • Seed transaction balance-diff artifact showing post-transaction account and token deltas