Onyx oPEPE Donation Overvaluation
Exploit Transactions
0xf7c21600452939a81b599017ee24ee0dfd92aaaccd0a55d02819a7658a6ef635Victim Addresses
0x7d61ed92a6778f5abf5c94085739f1edabec2800Ethereum0x5fdbcd61bc9bd4b6d3fd1f49a5d253165ea11750Ethereum0x714bd93ab6ab2f0bcfd2aeaf46a46719991d0d79Ethereum0x8f35113cfaba700ed7a907d92b114b44421e412aEthereum0xbced4e924f28f43a24ceedec69ee21ed4d04d2ddEthereum0x0c19d213e9f2a5cbaa4ec6e8eac55a22276b0641Ethereum0x830dacd5d0a62afa92c9bc6878461e9cd317b085Ethereum0x1933f1183c421d44d531ed40a5d2445f6a91646dEthereum0xfee4428b7f403499c50a6da947916b71d33142dcEthereumLoss Breakdown
Similar Incidents
Metalend Empty-Market Donation Exploit
41%Bao Donation Borrow Exploit
40%bZx iYFI Donation Inflation
37%ParaSpace cAPE Donation Repricing
36%Euler DAI Reserve Donation
34%Aave AMM LP Oracle Manipulation Through Delegated Recursive LP Looping
31%Root Cause Analysis
Onyx oPEPE Donation Overvaluation
1. Incident Overview TL;DR
On Ethereum mainnet transaction 0xf7c21600452939a81b599017ee24ee0dfd92aaaccd0a55d02819a7658a6ef635 in block 18476769, EOA 0x085bdff2c522e8637d4154039db8746bb8642bff called attacker contract 0x526e8e98356194b64eae4c2d443cc8aad367336f and executed a single-transaction drain against Onyx Protocol. The attack flash-borrowed 4,000 WETH from Aave V3, swapped that WETH into 2520870348093423681390050791472 PEPE, then used PEPE to turn the collateral-enabled oPEPE market (0x5fdbcd61bc9bd4b6d3fd1f49a5d253165ea11750) into an empty-market donation oracle.
The root cause is that Onyx trusted the oPEPE exchange rate as a collateral input even though any user could increase totalCash by transferring PEPE directly into the market contract without minting offsetting oPEPE. After the attacker redeemed oPEPE supply down to dust, the donated PEPE made each remaining oPEPE unit appear astronomically valuable. Comptroller (0x7d61ed92a6778f5abf5c94085739f1edabec2800) then treated that synthetic value as honest collateral and authorized cross-market borrows from oETH, oUSDC, oUSDT, oPAXG, oDAI, oBTC, and oLINK. The sender EOA finished the transaction with a net native-balance gain of 1156561128538858638915 wei, or 1156.561128538858638915 ETH, already net of gas.
2. Key Background
Onyx uses Compound-style OToken accounting. Each OToken's stored exchange rate is derived from market cash, total borrows, total reserves, and total supply. That exchange rate is then exposed to Comptroller liquidity checks through getAccountSnapshot(). In the affected deployment, oPEPE was not an isolated market: at block 18476769 it was listed with a collateral factor of 0.5e18, so any overstatement in oPEPE's exchange rate translated directly into extra borrow capacity.
The pricing leg was not the bug. The seed trace shows Comptroller querying ChainlinkOracle for both oPEPE and the borrowed markets, and the PEPE price comes from the configured oracle path rather than a manipulable spot read from the same Uniswap V2 pool used by the attacker. The exploit therefore does not depend on oracle tampering; it depends on internal collateral accounting.
The relevant pre-state is Ethereum mainnet immediately before the transaction was executed, at block 18476768. At that point:
- oPEPE was listed and collateral-enabled.
- oETH and six other Onyx markets held borrowable liquidity.
- Direct ERC-20 transfers into the oPEPE contract were possible.
- Aave V3 flash loans and Uniswap V2 swaps were permissionless.
Those conditions made the exploit permissionless and deterministic from public state.
3. Vulnerability Analysis & Root Cause Summary
This incident is an ATTACK, specifically an empty-market donation overvaluation attack against a collateral-enabled OToken listing. The core invariant is simple: collateral value should rise only when underlying is added in exchange for proportional OToken shares, not when a user donates underlying directly into the market contract. Onyx violated that invariant because exchangeRateStoredInternal() treated donated PEPE the same as minted PEPE and did not defend against a near-zero totalSupply.
Once the attacker became the dominant oPEPE supplier, redeeming supply down to dust let the attacker control the denominator of the exchange-rate formula. A direct PEPE donation then controlled the numerator. Comptroller's borrow checks consumed the resulting inflated exchange rate through getAccountSnapshot() and getHypotheticalAccountLiquidityInternal(), so the attacker could present a dust oPEPE position as collateral worth nearly the entire donated PEPE inventory. Because other Onyx markets still had real cash, the attacker could immediately borrow those assets out. No privileged role, stolen key, or attacker-only access-controlled contract was required.
4. Detailed Root Cause Analysis
The oPEPE market implementation makes the exchange-rate bug explicit:
function getAccountSnapshot(address account) external view returns (uint, uint, uint, uint) {
uint oTokenBalance = accountTokens[account];
uint borrowBalance;
uint exchangeRateMantissa;
(mErr, borrowBalance) = borrowBalanceStoredInternal(account);
(mErr, exchangeRateMantissa) = exchangeRateStoredInternal();
return (uint(Error.NO_ERROR), oTokenBalance, borrowBalance, exchangeRateMantissa);
}
function exchangeRateStoredInternal() internal view returns (MathError, uint) {
uint _totalSupply = totalSupply;
if (_totalSupply == 0) {
return (MathError.NO_ERROR, initialExchangeRateMantissa);
} else {
uint totalCash = getCashPrior();
(mathErr, cashPlusBorrowsMinusReserves) = addThenSubUInt(totalCash, totalBorrows, totalReserves);
(mathErr, exchangeRate) = getExp(cashPlusBorrowsMinusReserves, _totalSupply);
return (MathError.NO_ERROR, exchangeRate.mantissa);
}
}
That value is then consumed by Comptroller's liquidity engine:
function borrowAllowed(address oToken, address borrower, uint borrowAmount) external returns (uint) {
if (oracle.getUnderlyingPrice(OToken(oToken)) == 0) {
return uint(Error.PRICE_ERROR);
}
(Error err, , uint shortfall) =
getHypotheticalAccountLiquidityInternal(borrower, OToken(oToken), 0, borrowAmount);
if (err != Error.NO_ERROR) return uint(err);
if (shortfall > 0) return uint(Error.INSUFFICIENT_LIQUIDITY);
return uint(Error.NO_ERROR);
}
function getHypotheticalAccountLiquidityInternal(
address account,
OToken oTokenModify,
uint redeemTokens,
uint borrowAmount
) internal view returns (Error, uint, uint) {
(oErr, vars.oTokenBalance, vars.borrowBalance, vars.exchangeRateMantissa) =
asset.getAccountSnapshot(account);
vars.collateralFactor = Exp({mantissa: markets[address(asset)].collateralFactorMantissa});
vars.exchangeRate = Exp({mantissa: vars.exchangeRateMantissa});
vars.oraclePrice = Exp({mantissa: oracle.getUnderlyingPrice(asset)});
vars.tokensToDenom = mul_(mul_(vars.collateralFactor, vars.exchangeRate), vars.oraclePrice);
vars.sumCollateral = mul_ScalarTruncateAddUInt(vars.tokensToDenom, vars.oTokenBalance, vars.sumCollateral);
}
The exploit path visible in the seed trace matches that code exactly:
OErc20Delegator::redeem(4999999999999999999999999998)
...
storage change: totalSupply ... -> 2
PepeToken::transfer(OErc20Delegator[oPEPE], 2520870348093423681390050791471)
...
OErc20Delegate::getAccountSnapshot(attackerHelper) -> (0, 2, 0, 1260435174046711840695025395736000000000000000000)
Comptroller::borrowAllowed(OEther, attackerHelper, 334476442580295733160)
That trace proves the concrete breakpoint:
- The attacker minted oPEPE from a tiny seed amount.
- The attacker redeemed almost the full position until
totalSupplywas only2units. - The attacker transferred PEPE directly into oPEPE, increasing
totalCashwithout increasing supply. getAccountSnapshot()returned the attacker's dust oPEPE balance together with an enormous exchange rate.borrowAllowed()accepted that collateral and allowed real assets to be borrowed from other markets.
The ACT conditions were also present and sufficient:
- The attacker could become the dominant supplier in the empty market.
- oPEPE had a non-zero collateral factor.
- Direct token donations into the market contract were permissionless.
- Other Onyx markets held enough cash to make the inflated collateral immediately monetizable.
5. Adversary Flow Analysis
The attacker used one EOA, one main orchestrator, and seven borrow helpers:
- Sender EOA:
0x085bdff2c522e8637d4154039db8746bb8642bff - Main attack contract:
0x526e8e98356194b64eae4c2d443cc8aad367336f - Helper for oETH:
0xf8e15371832aed6cd2741c572b961ffeaf751eaa - Helper for oUSDC:
0xdb9be000d428bf3b3ae35f604a0d7ab938bea6eb - Helper for oUSDT:
0xe495cb62b36cbe40b9ca90de3dc5cdf0a4259e1c - Helper for oPAXG:
0x414764af57c43e36d7e0c3e55ebe88f410a6edb6 - Helper for oDAI:
0xcede81bb4046587dad6fc3606428a0eb4084d760 - Helper for oBTC:
0xe82f9ffe18fe511d31320d73c2e6be4338575d22 - Helper for oLINK:
0xf79cae9a064f4d6395e293fd7162856ffeeb7613
The transaction flowed in three stages.
First, the main contract borrowed 4,000 WETH from Aave V3 and swapped it into 2520870348093423681390050791472 PEPE on Uniswap V2. This PEPE inventory was the fuel for the empty-market donation attack.
Second, the main contract distributed PEPE into helpers. Each helper entered oPEPE as collateral, paired it with one borrow market, and used the inflated collateral value to extract real liquidity. The seed trace shows the following successful borrows:
- oETH
0x714bd93ab6ab2f0bcfd2aeaf46a46719991d0d79:334476442580295733160wei - oUSDC
0x8f35113cfaba700ed7a907d92b114b44421e412a:513987927004USDC units - oUSDT
0xbced4e924f28f43a24ceedec69ee21ed4d04d2dd:249534202651USDT units - oPAXG
0x0c19d213e9f2a5cbaa4ec6e8eac55a22276b0641:81375414746413246657PAXG wei - oDAI
0x830dacd5d0a62afa92c9bc6878461e9cd317b085:103657601740278955029570wei - oBTC
0x1933f1183c421d44d531ed40a5d2445f6a91646d:1312889485satoshis - oLINK
0xfee4428b7f403499c50a6da947916b71d33142dc:10082867210431152021425wei
Finally, the main contract consolidated the borrowed assets, swapped them back toward WETH and ETH, repaid 4,002 WETH to Aave V3, and returned the net profit to the sender EOA. The seed balance diff records the sender's native balance increasing from 4731023480721325444 wei to 1161292152019579964359 wei, a net delta of 1156561128538858638915 wei after gas.
6. Impact & Losses
The exploit drained seven Onyx borrow markets in one transaction and demonstrated that any similarly configured empty collateral market could be turned into a cross-market drain.
| Asset | Raw Loss | Decimals |
|---|---|---|
| ETH | 334476442580295733160 | 18 |
| USDC | 513987927004 | 6 |
| USDT | 249534202651 | 6 |
| PAXG | 81375414746413246657 | 18 |
| DAI | 103657601740278955029570 | 18 |
| WBTC | 1312889485 | 8 |
| LINK | 10082867210431152021425 | 18 |
The sender EOA paid approximately 0.373617125 ETH in transaction fees and still realized 1156.561128538858638915 ETH in net profit. Because the exploit path is fully permissionless and completes in one transaction, this was a realizable ACT opportunity rather than an unreproducible edge case.
7. References
- Seed transaction:
0xf7c21600452939a81b599017ee24ee0dfd92aaaccd0a55d02819a7658a6ef635 - Pre-state anchor: Ethereum mainnet block
18476768 - Main attack path trace:
artifacts/collector/seed/1/0xf7c21600452939a81b599017ee24ee0dfd92aaaccd0a55d02819a7658a6ef635/trace.cast.log - Profit and loss deltas:
artifacts/collector/seed/1/0xf7c21600452939a81b599017ee24ee0dfd92aaaccd0a55d02819a7658a6ef635/balance_diff.json - oPEPE market:
0x5fdbcd61bc9bd4b6d3fd1f49a5d253165ea11750 - OErc20 implementation artifact:
artifacts/collector/seed/1/0x9dcb6bc351ab416f35aeab1351776e2ad295abc4/src/Contract.sol - Comptroller proxy:
0x7d61ed92a6778f5abf5c94085739f1edabec2800 - Comptroller implementation:
0x4345d308f02d1beb92475bda25e7c62be288478e - Oracle contract:
0x2a68c871aaaf80e6d41114e92bc4c313f0b94bb8