Calculated from recorded token losses using historical USD prices at the incident time.
0x4822d9172e5b76b9db37b75f5552f9988f98a888Ethereum0x85a948fd70b2b415bda93324581fb5fff1293df7Ethereum0x8125afd067094cd573255f82795339b9fe2a40abEthereumAlkemi Earn Public on Ethereum was exploited in block 24627185 through a permissionless same-account self-liquidation sequence. The attacker first deployed helper contract 0x54ad675428e704b0e97c147da46f8f8743decb81 in tx 0xfa64b1d97d9c136e7da3375c3d06d10854a5bae57690fb1fedc4d75a60eed6c2, then used it in tx 0xd4ec3e344fe270f34078e8cabc7f7bee36204049bc9c61febce3c1668b8df42c to flash-loan 100 WETH from Morpho, supply WETH collateral into Alkemi, borrow the same WETH market, liquidate that borrow against the same WETH collateral, withdraw inflated collateral, and repay the flash loan.
The root cause is a storage-aliasing bug in AlkemiEarnPublic.liquidateBorrow. When targetAccount == msg.sender and assetBorrow == assetCollateral == wethAddress, the function treats borrower collateral and liquidator collateral as if they belonged to different storage slots even though they resolve to the same mapping entry. The final write sequence therefore overwrites the borrower's reduced balance with the liquidator's increased balance, minting collateral from liquidation math and leaving the protocol with an unbacked 43.45395 WETH liability.
Alkemi Earn Public tracks user supply balances and borrow balances per market and accrues interest through stored indices. The ETH market is represented through Alkemi's custom AlkemiWETH token at 0x8125afd067094cd573255f82795339b9fe2a40ab, while the canonical WETH token remains 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2.
The proxy entrypoint is 0x4822d9172e5b76b9db37b75f5552f9988f98a888, which delegates to implementation 0x85a948fd70b2b415bda93324581fb5fff1293df7. Supplying ETH into the WETH market mints AlkemiWETH claims, borrowing the WETH market pays out ETH, and withdrawals burn AlkemiWETH claims back into ETH. Morpho's flash-loan contract at 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb gives any unprivileged caller enough same-tx liquidity to prime Alkemi's cash and execute the full loop. The enabling risk parameters (collateralRatio, originationFee, liquidationDiscount, and closeFactorMantissa) are all publicly queryable on-chain, which is why the exploit remains ACT-realizable without any attacker-private inputs.
Two attacker-controlled contracts matter in the realized exploit. The helper contract performs the flash loan, same-market borrow, self-liquidation, and withdrawal. A temporary contract 0x7b6d57758d02a2833cde7672efa5d03aece08436 is deployed mid-transaction to seed Alkemi's WETH cash with 43.45395 ETH and retain the final stranded claim after the helper exits flat.
This is an ATTACK-class bug in protocol accounting, not a MEV opportunity and not an access-control issue. The vulnerable condition is that Alkemi permits liquidateBorrow(targetAccount, assetBorrow, assetCollateral, requestedAmountClose) where the borrower and liquidator are the same account and the borrowed asset equals the collateral asset. Under that condition, the function binds both supplyBalances[targetAccount][assetCollateral] and supplyBalances[msg.sender][assetCollateral] as independent storage references even though they alias the same slot. It then accrues both balances independently, updates totalSupply as though two distinct accounts existed, computes a borrower post-seizure balance and a liquidator post-seizure balance, and writes both back in sequence. The second write wins, so the self-liquidating account keeps currentSupply + seized instead of netting to its original collateral amount. Because the helper can then withdraw against that inflated balance, the protocol ends with zero AlkemiWETH cash while a positive AlkemiWETH claim remains on the attacker temp contract. The broken invariant is simple: a self-liquidation against the same collateral market must conserve user collateral apart from explicit fees, but Alkemi creates extra collateral instead.
The verified source shows the breakpoint directly:
Balance storage supplyBalance_TargetCollateralAsset =
supplyBalances[targetAccount][assetCollateral];
Balance storage supplyBalance_LiquidatorCollateralAsset =
supplyBalances[localResults.liquidator][assetCollateral];
(err, localResults.updatedSupplyBalance_TargetCollateralAsset) = sub(
localResults.currentSupplyBalance_TargetCollateralAsset,
localResults.seizeSupplyAmount_TargetCollateralAsset
);
(err, localResults.updatedSupplyBalance_LiquidatorCollateralAsset) = add(
localResults.currentSupplyBalance_LiquidatorCollateralAsset,
localResults.seizeSupplyAmount_TargetCollateralAsset
);
supplyBalance_TargetCollateralAsset.principal =
localResults.updatedSupplyBalance_TargetCollateralAsset;
supplyBalance_LiquidatorCollateralAsset.principal =
localResults.updatedSupplyBalance_LiquidatorCollateralAsset;
In a normal liquidation these storage references point to different users, so the bookkeeping is coherent. In the realized exploit they point to the same supplyBalances[helper][AlkemiWETH] slot, so the subtraction is discarded by the final addition.
The ACT pre-state is Ethereum mainnet immediately before block 24627180, when any unprivileged EOA could still deploy a helper contract and execute the same sequence. The necessary conditions were public and on-chain: the Alkemi WETH market was live, Morpho flash loans were available, the same WETH market could be used as both borrow asset and collateral asset, and liquidateBorrow did not reject targetAccount == msg.sender.
The exploit sequence works because the attacker can both create temporary cash and manufacture the aliased liquidation path in one transaction. The helper flash-loans 100 WETH, unwraps it to ETH, sends 43.45395 ETH into a freshly deployed temp contract, and has that temp contract supply the ETH to Alkemi's WETH market. That priming step gives Alkemi enough WETH-market cash to honor the helper's later same-market borrow while also ensuring the temp contract will hold a protocol claim that can survive after the helper exits.
The helper then supplies an additional 50 ETH into the same market and borrows 39.5 ETH from that market. Because borrow interest is accrued on read, getBorrowBalance returns 39.5395 ETH just before liquidation. The helper passes that exact debt amount into liquidateBorrow(address(this), ALKEMI_WETH, ALKEMI_WETH, debt) with matching ETH value. Inside liquidateBorrow, Alkemi computes the borrower's updated collateral as currentSupply - seized and the liquidator's updated collateral as currentSupply + seized. Since borrower and liquidator are the same address and the collateral market is the same asset, both storage references alias the helper's single WETH-collateral slot. The final write leaves the helper with the inflated currentSupply + seized balance instead of the expected net-neutral result.
With the inflated collateral now recorded, the helper withdraws the maximum possible AlkemiWETH balance, receiving 93.49345 ETH from the protocol. That ETH is rewrapped into canonical WETH and used to close the Morpho flash loan loop. After the helper exits, on-chain state at block 24627185 still shows 0 AlkemiWETH cash on the Alkemi proxy, 0 helper supply balance, 0 helper borrow balance, and a live 43.45395 AlkemiWETH supply claim on the attacker temp contract. The artifact cluster_value_summary.json also shows that the attacker cluster's ETH-equivalent value rises from 0.048747019553259608 ETH before the sequence to 43.501942760487326483 ETH after it, for a net gain of 43.453195740934066875 ETH-equivalent after gas.
Independent post-state reads match the core invariant break:
AlkemiWETH.balanceOf(protocol) at block 24627185 = 0
getSupplyBalance(attacker_temp, AlkemiWETH) = 43453950000000000000
getSupplyBalance(attacker_helper, AlkemiWETH) = 0
getBorrowBalance(attacker_helper, AlkemiWETH) = 0
Those values prove the exploit is not merely an intermediate accounting glitch. The helper has already repaid the flash loan and exited flat, yet the protocol still owes 43.45395 WETH-equivalent collateral that is not backed by AlkemiWETH cash.
The adversary flow consists of two public Ethereum transactions:
0xfa64b1d97d9c136e7da3375c3d06d10854a5bae57690fb1fedc4d75a60eed6c2
This deploys helper contract 0x54ad675428e704b0e97c147da46f8f8743decb81.0xd4ec3e344fe270f34078e8cabc7f7bee36204049bc9c61febce3c1668b8df42c
This executes the flash-loan-assisted self-liquidation from EOA 0x8366d87bfb60e4f9574bf0f7e3f42744e79c7b76.The reconstructed high-level call flow is:
EOA 0x8366... -> helper 0x54ad...::attack(protocol)
helper -> Morpho::flashLoan(WETH, 100e18, abi.encode(protocol))
helper -> temp 0x7b6d...::perform{value: 43.45395 ETH}(protocol)
temp -> Alkemi::supply(AlkemiWETH, 43.45395 ETH)
helper -> Alkemi::supply(AlkemiWETH, 50 ETH)
helper -> Alkemi::borrow(AlkemiWETH, 39.5 ETH)
helper -> Alkemi::getBorrowBalance(helper, AlkemiWETH) = 39.5395 ETH
helper -> Alkemi::liquidateBorrow(helper, AlkemiWETH, AlkemiWETH, 39.5395 ETH)
helper -> Alkemi::withdraw(AlkemiWETH, type(uint256).max)
helper -> WETH::deposit(all ETH)
helper -> Morpho repayment via approved WETH
The trace summary recorded from the exploit transaction captures the critical external calls in the same order:
[
{"from":"0x54ad...","to":"0xbbbbbb...","sig":"flashLoan(WETH,100e18,...)"},
{"from":"0x7b6d...","to":"0x4822...","sig":"supply(AlkemiWETH,43.45395e18)"},
{"from":"0x54ad...","to":"0x4822...","sig":"supply(AlkemiWETH,50e18)"},
{"from":"0x54ad...","to":"0x4822...","sig":"borrow(AlkemiWETH,39.5e18)"},
{"from":"0x54ad...","to":"0x4822...","sig":"liquidateBorrow(self,AlkemiWETH,AlkemiWETH,39.5395e18)"},
{"from":"0x54ad...","to":"0x4822...","sig":"withdraw(AlkemiWETH,type(uint256).max)"}
]
The attacker's decision point is deterministic: once the helper confirms its live debt amount, it closes exactly that debt through the aliased liquidation path and immediately withdraws the inflated collateral. No privileged calldata, private orderflow, or attacker-only bytecode is required. Any unprivileged actor with flash-loan access and the same public chain state could have executed the same path.
The measurable protocol loss is an unbacked 43.45395 WETH liability, encoded on-chain as 43453950000000000000 wei-denominated WETH units. After the exploit transaction, Alkemi's WETH market cash is zero while the attacker temp contract still holds a positive AlkemiWETH claim of the same size. That means later honest withdrawals become dependent on fresh deposits rather than existing backing, which is a direct solvency failure for the market.
The attacker cluster's ETH-equivalent balance change is also deterministic. The cluster valuation summary shows a pre-state total of 0.048747019553259608 ETH-equivalent, a post-state total of 43.501942760487326483 ETH-equivalent, and a net delta of 43.453195740934066875 ETH-equivalent after gas. The report still classifies the formal success predicate as non-monetary because the strongest invariant proof is the stranded claim against zero protocol cash.
0xbc1ebbea7a35db0e49f7639323e34e89b379318b17653086ccdb0ea466fc75350xfa64b1d97d9c136e7da3375c3d06d10854a5bae57690fb1fedc4d75a60eed6c20xd4ec3e344fe270f34078e8cabc7f7bee36204049bc9c61febce3c1668b8df42c0x4822d9172e5b76b9db37b75f5552f9988f98a8880x85a948fd70b2b415bda93324581fb5fff1293df70x8125afd067094cd573255f82795339b9fe2a40ab0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCbAlkemiEarnPublic: https://etherscan.io/address/0x85a948fd70b2b415bda93324581fb5fff1293df7#code/workspace/session/artifacts/auditor/iter_0/discovery_notes.md/workspace/session/artifacts/auditor/iter_0/attack_trace_summary.json/workspace/session/artifacts/auditor/iter_0/post_state_checks.json/workspace/session/artifacts/auditor/iter_1/cluster_value_summary.json