Calculated from recorded token losses using historical USD prices at the incident time.
0x6e6e556a5685980317cb2afdb628ed4a845b3cbd1c98bdaffd0561cb2c4790fa0x5655c442227371267c165101048e4838a762675dEthereumAn 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.
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.
The bug is a checks-effects-interactions violation in EFVault redemption logic. first validates that the caller's current share balance can support the requested asset amount and derives . Instead of burning those shares immediately, it executes the external call . After control returns, the vault checks whether and, if so, silently reduces to the caller's reduced post-callback balance before calling . 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.
withdrawshares = (totalSupply() * assets) / totalAssets()(uint256 withdrawn, uint256 fee) = IController(controller).withdraw(assets, receiver)balanceOf(msg.sender) < sharesshares_burnThe 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.
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.
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.
0x6e6e556a5685980317cb2afdb628ed4a845b3cbd1c98bdaffd0561cb2c4790fa0x5655c442227371267c165101048e4838a762675d0x88e6a0c2ddd26feeb64f039a2c41296fcb3f56400xfe141c32e36ba7601d128f0c39dedbe0f6abb9830xcfd26fe5fe6028539802275c1cc6e9325aa2e3b7Victim 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: