Calculated from recorded token losses using historical USD prices at the incident time.
0xaee0f8d1235584a3212f233b655f87b89f22f1d4890782447c4ef742b37af58d0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3BEthereum0xbAfe01fF935C7305907C33bf824352Ee5979b526Ethereum0x50ce56A3239671Ab62f185704Caedf626352741eEthereum0x39AA39c021dfbaE8faC545936693aC917d5E7563Ethereum0x35A18000230DA775CAc24873d00Ff85BccdeD550EthereumCompound was exploited in Ethereum block 19290921 because its deployed UniswapAnchoredView oracle kept the cUNI underlying price frozen at 8.34e18 while public UNI market prices had moved higher. The attacker used a single flash-loan-funded transaction, 0xaee0f8d1235584a3212f233b655f87b89f22f1d4890782447c4ef742b37af58d, to mint cUSDC, borrow UNI against that stale price, sell the borrowed UNI on public Uniswap V3 pools at the higher executable market price, repay the flash loan, and keep the spread.
The root cause is the interaction between the oracle guard path and Compound's borrow checks. In UniswapAnchoredView.validate, out-of-band reporter updates emit PriceGuarded and preserve the previously stored price instead of replacing it. In Comptroller.borrowAllowed and Comptroller.getHypotheticalAccountLiquidityInternal, Compound only requires a non-zero oracle price and then uses that cached value for liquidity calculations, so the stale UNI price was treated as current for borrow-capacity enforcement.
Compound v2 prices each market through a price oracle and consumes those prices in the Comptroller when deciding whether a user can borrow. For cUNI, the protocol used the oracle at 0x50ce56A3239671Ab62f185704Caedf626352741e, and for borrow permissioning it relied on the Comptroller implementation at 0xbAfe01fF935C7305907C33bf824352Ee5979b526 behind the Unitroller proxy 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B.
The relevant oracle design is not a pure spot feed. UniswapAnchoredView keeps a stored price per symbol, compares reporter updates against a Uniswap anchor band, and only writes a new price when the reported value stays within that tolerance. When a new report falls outside the band, the contract emits PriceGuarded and leaves the stored price unchanged:
// UniswapAnchoredView.validate
if (priceData.failoverActive) {
prices[config.symbolHash].price = uint248(anchorPrice);
emit PriceUpdated(config.symbolHash, anchorPrice);
} else if (isWithinAnchor(reportedPrice, anchorPrice)) {
prices[config.symbolHash].price = uint248(reportedPrice);
emit PriceUpdated(config.symbolHash, reportedPrice);
valid = true;
} else {
emit PriceGuarded(config.symbolHash, reportedPrice, anchorPrice);
}
That design matters because Compound borrows are validated from whatever price the oracle returns at the time of the liquidity check, not from an independently refreshed execution price. If the stored value is stale but non-zero, the borrow gate still proceeds.
This is an oracle-staleness borrowing failure, not a privileged access issue. The protocol invariant should have been: if Compound allows a borrow, then the account remains collateralized when both the collateral asset and the borrowed asset are valued at a contemporaneous realizable market price. That invariant failed because the borrowed UNI leg was valued at a cached price that had stopped tracking the public market.
The code-level breakpoint is in UniswapAnchoredView.validate, where the PriceGuarded branch preserves the old stored price instead of updating it. The collector artifacts show accepted PriceUpdated events for the UNI symbol earlier in the window and then multiple later PriceGuarded events before the exploit block, consistent with the oracle being frozen while fresh reporter prices were rejected. The root cause is then completed by the Comptroller logic:
// Comptroller.borrowAllowed
if (oracle.getUnderlyingPrice(CToken(cToken)) == 0) {
return uint(Error.PRICE_ERROR);
}
(Error err, , uint shortfall) =
getHypotheticalAccountLiquidityInternal(borrower, CToken(cToken), 0, borrowAmount);
// Comptroller.getHypotheticalAccountLiquidityInternal
vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset);
vars.sumBorrowPlusEffects =
mul_ScalarTruncateAddUInt(vars.oraclePrice, borrowAmount, vars.sumBorrowPlusEffects);
Because oracle.getUnderlyingPrice(cUNI) still returned the frozen 8340000000000000000 value, the borrow-side UNI leg was underpriced during the liquidity calculation. That let the attacker borrow more market value than the cUSDC collateral should have supported and immediately monetize the mispricing on public liquidity venues.
The validated pre-state is block 19290920, one block before the exploit, where the oracle still returned a stale cUNI underlying price of 8.34e18 and a cUSDC price of 1e30. The auditor's oracle definition and the PoC both check this directly with getUnderlyingPrice(cUNI) and getUnderlyingPrice(cUSDC). The collector also recovered the proxy implementation and oracle addresses from Unitroller storage, which ties the incident to the concrete deployed contracts rather than to a guessed code path.
The oracle evidence is consistent with a frozen UNI price. The collector captured PriceUpdated logs and later PriceGuarded logs from the same oracle contract in the window leading into the exploit. The accepted updates show the oracle can write prices for the relevant symbol, while the guarded updates show later reported prices falling outside the anchor bounds and therefore not replacing the stored value. The root cause claim that the stored value remained stale is further corroborated by the incident PoC and the auditor run: both reproduce the exploit from the immediately preceding block with the stale cUNI price still present.
Once the stale price existed, Compound's borrow gate trusted it end to end. The exploit trace shows the executor contract receiving a Balancer flash loan of 193020254960 USDC, minting cUSDC, entering the cUSDC market, and then borrowing 19748550630884311045840 UNI from cUNI. The relevant trace lines are unambiguous:
0xBA1222...::flashLoan(..., [193020254960], ...)
0x39AA39...::mint(193020254960)
Comptroller::enterMarkets([0x39AA39...])
0x35A180...::borrow(19748550630884311045840)
Comptroller::borrowAllowed(..., 19748550630884311045840)
After the borrow succeeded, the attacker immediately unwound the borrowed UNI through public Uniswap V3 pools. The trace shows a swap on the UNI/WETH pool at 0x1d42064Fc4Beb5F8aAF85F4617AE8b3b5B8Bd801, then a WETH/USDC swap on 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640, repayment of the Balancer flash loan, and a final USDC/WETH conversion on 0xb263dd92A23E57e37B7C7132cc898D697D443951 to realize profit in ETH. The balance-diff artifact confirms the sender EOA 0xe000008459b74a91e306a47c808061dfa372000e increased from 0.207902193314900878 ETH to 0.974290207415629576 ETH, a net gain of 0.766388014100728698 ETH after gas.
This sequence proves the exploit was ACT. It used only public state, a permissionless Balancer flash loan, public Compound markets, and public Uniswap pools. No privileged key material, private order flow, attacker-owned historical bytecode, or private helper infrastructure was required to realize the profit condition.
The adversary cluster has two relevant addresses in the validated incident artifacts. The EOA 0xe000008459b74a91e306a47c808061dfa372000e submitted the exploit transaction and received the final ETH profit. The contract 0x2f99fb66ea797e7fa2d07262402ab38bd5e53b12 acted as the executor that received the flash loan, minted cUSDC, borrowed UNI, performed the Uniswap V3 swaps, and returned value to the EOA.
The on-chain flow is:
193020254960 USDC.enterMarkets on Compound.19748.550630884311045840 UNI from cUNI while Compound still values UNI at the frozen oracle price.The critical decision point is step 3. Without the stale borrow-side UNI price, the borrow amount would have been constrained by the higher executable market price and the trade would not have produced the same spread. The rest of the flow is a straightforward realization of that oracle-induced mispricing.
The directly measured token loss in the validated artifacts is 2441308933 raw USDC units, which is 2441.308933 USDC at 6 decimals. The same incident also yielded a net 0.766388014100728698 ETH increase to the sender EOA after fees, as shown in the balance-diff artifact.
The affected protocol components were Compound's oracle-driven borrow controls for cUNI and the related collateralization logic for the attacker's cUSDC position. Economically, the protocol allowed a borrow sized from an outdated oracle value rather than from the public price at which the borrowed UNI could actually be sold.
0xaee0f8d1235584a3212f233b655f87b89f22f1d4890782447c4ef742b37af58d0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B0xbAfe01fF935C7305907C33bf824352Ee5979b5260x50ce56A3239671Ab62f185704Caedf626352741e0x39AA39c021dfbaE8faC545936693aC917d5E75630x35A18000230DA775CAc24873d00Ff85BccdeD550PriceUpdated and PriceGuarded logs collected for blocks 19290000 to 19290921