We do not have a reliable USD price for the recorded assets yet.
0xcd6ca2f0d0c182c5049d9a1f65cde51a706ae142Ethereum0x1196B60c9ceFBF02C9a3960883213f47257BecdBEthereumAn attacker deployed a fresh helper contract and used Balancer's permissionless flashLoan entrypoint to name Affine's LidoLevEthStrategy as the flash-loan recipient. The strategy's receiveFlashLoan callback trusted Balancer as the sole caller check and accepted attacker-controlled userData, so the attacker could force privileged divest and upgrade branches without vault, strategist, or governance approval.
The result was a forced unwind of the strategy's Aave position and transfer of the remaining aEthwstETH collateral to the attacker cluster. The validated root cause is an exposed privileged callback path, not privileged key compromise or a non-public dependency.
Affine vault 0x1196B60c9ceFBF02C9a3960883213f47257BecdB used strategy 0xcd6ca2f0d0c182c5049d9a1f65cde51a706ae142 to run a leveraged wstETH/WETH position on Aave. Before the exploit block, the strategy held positive aEthwstETH collateral and positive variableDebtWETH, which made it sensitive to any external path that could repay debt or redirect collateral.
Balancer vault 0xBA12222222228d8Ba445958a75a0704d566BF2C8 offers permissionless flash loans. Any caller can choose the recipient contract and arbitrary callback payload. That means a recipient callback must authenticate not just Balancer itself, but also the expected operation context.
The attacker helper 0x12d85e5869258a80d4bebe70d176d0f58b2d68e4 was deployed by EOA 0x09f6be2a7d0d2789f01ddfaf04d4eaa94efc0857 eight blocks before the exploit. Collector artifacts show the helper was bespoke to this strategy flow: its bytecode hard-coded Balancer, WETH, aEthwstETH, and the victim strategy address, and its entire transaction history contains only deployment and the exploit transaction.
The vulnerability is an external-callback authorization failure. In the verified LidoLevV3 implementation, receiveFlashLoan only checks that msg.sender is Balancer, then decodes attacker-controlled userData into (LoanType, address) and dispatches internal strategy actions from that payload. Because Balancer flash loans are permissionless, any unprivileged actor can cause Balancer to call the strategy with arbitrary LoanType and arbitrary destination address data.
That design exposed two privileged flows. First, LoanType.divest reaches _endPosition, which repays strategy debt and unwinds collateral. Second, LoanType.upgrade reaches _payDebtAndTransferCollateral, which repays the remaining debt and transfers the strategy's aEthwstETH collateral to the supplied newStrategy address. Critically, the destination check used by the external upgradeTo entrypoint is not enforced on this callback path, so the attacker can supply an arbitrary helper contract instead of an approved Affine strategy.
The violated invariant is straightforward: only Affine-authorized logic should be able to mutate the strategy's Aave debt and collateral position or migrate collateral to another address. The code-level breakpoint is the callback dispatcher in receiveFlashLoan, where privileged control flow is selected from attacker-controlled payload after only authenticating Balancer as the caller.
if (msg.sender != address(BALANCER)) revert onlyBalancerVault();
(LoanType loanType, address newStrategy) = abi.decode(userData, (LoanType, address));
if (loanType == LoanType.divest) {
_endPosition(ethBorrowed);
} else if (loanType == LoanType.upgrade) {
_payDebtAndTransferCollateral(LidoLevV3(payable(newStrategy)));
}
The exploit became possible because the strategy assumed any Balancer callback had been self-initiated by trusted Affine strategy code. That assumption is false under Balancer's flash-loan model. An attacker does not need any privilege on the strategy; they only need Balancer to invoke the callback on their behalf with chosen userData.
The first attacker-controlled callback used the divest branch. Receipt evidence for transaction 0x03543ef96c26d6c79ff6c24219c686ae6d0eb5453b322e54d3b6a5ce456385e5 shows the strategy repaid its WETH debt on Aave and unwound collateral:
VariableDebtWETH Burn/Transfer:
- from: 0xcd6ca2f0d0c182c5049d9a1f65cde51a706ae142
- value: 318944845338696816295
Aave Repay event:
- reserve: WETH
- user: 0xcd6ca2f0d0c182c5049d9a1f65cde51a706ae142
- repayer: 0xcd6ca2f0d0c182c5049d9a1f65cde51a706ae142
After that unwind left sufficient WETH in the strategy, the attacker triggered a second Balancer flash loan using the upgrade branch. That branch reached _payDebtAndTransferCollateral and moved the remaining aEthwstETH collateral to the attacker helper. The decoded receipt shows the critical collateral transfer chain:
aEthwstETH Transfer:
- from: strategy 0xcd6ca2f0d0c182c5049d9a1f65cde51a706ae142
- to: helper 0x12d85e5869258a80d4bebe70d176d0f58b2d68e4
- value: 33698808979599562499
aEthwstETH Transfer:
- from: helper 0x12d85e5869258a80d4bebe70d176d0f58b2d68e4
- to: EOA 0x09f6be2a7d0d2789f01ddfaf04d4eaa94efc0857
- value: 33698808979599562499
The attacker helper did not need to be a valid Affine strategy. The root cause report correctly notes that the external upgradeTo wrapper may validate destination strategy type, but that protection was bypassed because the attacker entered directly through receiveFlashLoan. The helper exposed createAaveDebt(uint256) as a no-op and still succeeded because the earlier divest step had already left enough WETH inside the victim strategy to settle the second flash loan.
The validator's independent fork execution confirms the same mechanism. Running the PoC on block 19132934 produced a successful exploit path in which the strategy debt reached zero, the strategy's aEthwstETH balance reached zero, and the attacker address received 33698806193381635860 raw aEthwstETH units. The small difference from the incident amount is consistent with Aave index drift on the chosen fork block and does not alter the root cause.
The attacker flow consists of two transactions and one helper contract.
0x51338c8df2b8ef21abb6d6cb9539bb269ab558b742ed4548f1c883ccfa279716 created helper contract 0x12d85e5869258a80d4bebe70d176d0f58b2d68e4.0x03543ef96c26d6c79ff6c24219c686ae6d0eb5453b322e54d3b6a5ce456385e5 invoked the helper, which in turn called Balancer twice against the victim strategy.The helper logic reconstructed from bytecode and validated by the local PoC is:
Balancer.flashLoan(strategy, [WETH], [firstLoanAmount], abi.encode(uint256(1), helper));
Balancer.flashLoan(strategy, [WETH], [WETH.balanceOf(strategy)], abi.encode(uint256(2), helper));
aEthwstETH.transfer(attacker, aEthwstETH.balanceOf(helper));
The first flash loan forces the strategy down the unauthorized divest branch. The second flash loan forces the unauthorized upgrade branch, repays the remaining debt, and transfers the collateral to the helper. The helper then forwards the stolen collateral to the EOA. The exploit is permissionless because all required inputs are public: contract addresses, callback ABI shape, current strategy balances, and Balancer flash-loan access.
The incident drained the victim strategy's remaining aEthwstETH collateral and force-unwound its Aave leverage. The measurable token loss identified in the root-cause artifact is:
[
{
"token_symbol": "aEthwstETH",
"amount": "33698808979599562499",
"decimal": 18
}
]
That is the amount transferred out of the strategy and into the attacker cluster in the incident receipt. The attacker paid only gas and finished with positive aEthwstETH profit.
0x51338c8df2b8ef21abb6d6cb9539bb269ab558b742ed4548f1c883ccfa2797160x03543ef96c26d6c79ff6c24219c686ae6d0eb5453b322e54d3b6a5ce456385e50xcd6ca2f0d0c182c5049d9a1f65cde51a706ae1420x1196B60c9ceFBF02C9a3960883213f47257BecdB0xBA12222222228d8Ba445958a75a0704d566BF2C8