0xfe8bc757d87e97a5471378c90d390df47e1b29bb9fca918b94acd8ecfaadc5980xe3334e66634acf17b2b97ab560ec92d6861b25faEthereum0x4dd6d5d861edcd361455b330fa28c4c9817da687Ethereum0x606246e9ef6c70dcb6cee42136cd06d127e2b7c7Ethereum0x47d748c9babd5cca642f9f98e07442c0b5b04d2fEthereumZenterest was drained because its lending markets continued to trust stale manually reported prices for MPH and Whiteheart. At Ethereum block 20541640, an attacker-funded helper contract supplied 23,200 MPH plus a flash-borrowed 85 WHT, then borrowed 89.909548652799303712 WHT from cWhiteheart, repaid the flash loan, and extracted the market's pre-existing 4.909548652799303712 WHT cash.
The root cause was not a bug in flash loans or token transfers. The failure was Zenterest's collateral accounting: Comptroller.borrowAllowed() trusted ZenterestPriceFeed.getUnderlyingPrice() even though the stored MPH and Whiteheart prices had not been refreshed since January 2023.
Zenterest uses Compound-style cTokens. Borrowing power is computed by the Comptroller from each entered market's collateral factor, exchange rate, and oracle price.
The affected public contracts were:
cWhiteheart: 0xe3334e66634acf17b2b97ab560ec92d6861b25facMPH: 0x4dd6d5d861edcd361455b330fa28c4c9817da687Comptroller: 0x606246e9ef6c70dcb6cee42136cd06d127e2b7c7ZenterestPriceFeed: 0x47d748c9babd5cca642f9f98e07442c0b5b04d2fThe price feed is a storage-backed manual oracle. Its read path returns stored values directly:
function assetPrices(address token) public view returns (uint256 price) {
return _prices[token].value.mantissa;
}
function getUnderlyingPrice(ICorroborativeToken corroborative) public view returns (uint256) {
if (keccak256(abi.encodePacked(corroborative.symbol())) == CORROBORATIVE_ETH_SYMBOL_COMPORATOR) {
return AttoDecimalLib.ONE_MANTISSA;
}
return assetPrices(corroborative.underlying());
}
At the exploit fork block, the stored values were still 66570137662599764 for Whiteheart and 944836858607953 for MPH, with timestamps from January 2023.
The vulnerability class was stale-manual-price collateral accounting. Zenterest allowed borrowing against collateral values that were no longer anchored to contemporaneous market reality. Comptroller.borrowAllowed() first checked that an oracle price existed, then delegated solvency evaluation to getHypotheticalAccountLiquidityInternal(). That function loaded each entered market's collateralFactorMantissa, exchange rate, and oracle.getUnderlyingPrice(asset) and converted them into borrow capacity. Because ZenterestPriceFeed.getUnderlyingPrice() returned stored reporter-written values without a recency bound or market sanity check, the borrow path accepted MPH collateral at an inflated stale valuation. The attacker combined that stale valuation with a flash-borrowed WHT deposit to borrow all available cWhiteheart cash. The protocol was left with an undercollateralized MPH-backed debt position while the liquid WHT was extracted.
The borrow gate in Comptroller is the decisive control point:
if (oracle.getUnderlyingPrice(CToken(cToken)) == 0) {
return uint(Error.PRICE_ERROR);
}
(Error err, , uint shortfall) = getHypotheticalAccountLiquidityInternal(
borrower,
CToken(cToken),
0,
borrowAmount
);
Inside getHypotheticalAccountLiquidityInternal(), the protocol computes collateral value directly from oracle outputs:
vars.collateralFactor = Exp({mantissa: markets[address(asset)].collateralFactorMantissa});
vars.exchangeRate = Exp({mantissa: vars.exchangeRateMantissa});
vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset);
vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa});
vars.tokensToDenom = mul_(mul_(vars.collateralFactor, vars.exchangeRate), vars.oraclePrice);
That design would only be safe if oracle.getUnderlyingPrice(asset) were fresh and market-representative. It was not. The price-feed implementation returned _prices[token].value.mantissa as-is, and the pre-checks at the exploit fork confirm the Whiteheart and MPH entries were stale January 2023 values.
The attacker exploited exactly that stale valuation path. The observed helper contract at 0x90744c976f69c7d112e8fe85c750ace2a2c16f15 was deployed by EOA 0x5d5acc816a41fb59b043bc1028425c9645c0c336, funded with 23,200 MPH, then invoked in transaction 0xfe8bc757d87e97a5471378c90d390df47e1b29bb9fca918b94acd8ecfaadc598. In that transaction, the helper:
85 WHT from the Uniswap V3 flash pool,cMPH market,2,000 MPH directly to cMPH,cMPH with 21,200 MPH,85 WHT into cWhiteheart,89.909548652799303712 WHT from cWhiteheart,4.901048652799303712 WHT to the attacker EOA.The exploit receipt and balance diff show the victim depletion directly. cWhiteheart's WHT balance moved from 4909548652799303712 to 0, and the attacker EOA ended the transaction with 4901048652799303712 WHT. This matches the reported success predicate: the market's pre-existing liquid WHT cash was exhausted by a borrow that only passed because stale manual oracle prices overstated the real value of the MPH-backed collateral position.
The attacker lifecycle is fully observable on-chain.
First, the attacker EOA deployed the helper in transaction 0xce003fcd0985f8ecdd9d28ea7f86a0798d2a582f37ff692587bb6cbcd8168aa8. Second, the same EOA funded the helper with MPH using transactions 0x7506c742c9f19a95d55af91049c736cda32723fd24027306dae146defac9705e and 0xe8175646d49a592d37d6823af0fcefac1a2936e1fb8ba759c913f4d63f5192a2, totaling 23,200 MPH. Third, the attacker called start() on the helper in transaction 0xfe8bc757d87e97a5471378c90d390df47e1b29bb9fca918b94acd8ecfaadc598.
The exploit transaction logs encode the critical transfers:
Uniswap V3 pool -> helper: 85.000000000000000000 WHT
helper -> cWhiteheart: 85.000000000000000000 WHT
cWhiteheart -> helper: 89.909548652799303712 WHT
helper -> flash pool: 85.008500000000000000 WHT
helper -> attacker EOA: 4.901048652799303712 WHT
This was a permissionless ACT path. No privileged keys, governance action, or special access were required. The only prerequisites were public liquidity to source MPH and the continued availability of stale oracle values inside Zenterest.
The directly depleted asset was Whiteheart (WHT). The victim-side liquid loss was the pre-existing cWhiteheart cash balance:
4909548652799303712 raw units of WHT4.909548652799303712 WHT at 18 decimalsThe post-exploit state left cWhiteheart with zero cash and a borrow claim against the helper. The protocol therefore exchanged liquid reserves for a position whose recoverability depended on stale-oracle-valued MPH collateral, which is the concrete protocol loss mechanism in this incident.
0xfe8bc757d87e97a5471378c90d390df47e1b29bb9fca918b94acd8ecfaadc5980xce003fcd0985f8ecdd9d28ea7f86a0798d2a582f37ff692587bb6cbcd8168aa80x7506c742c9f19a95d55af91049c736cda32723fd24027306dae146defac9705e, 0xe8175646d49a592d37d6823af0fcefac1a2936e1fb8ba759c913f4d63f5192a2Comptroller.sol, ZenterestPriceFeed.sol