0x46567c731c4f4f7e27c4ce591f0aebdeb2d9ae1038237a0134de7b13e63d87290x2CCb7d00a9E10D0c3408B5EEfb67011aBfaCb075Ethereum0xeE894c051c402301bC19bE46c231D2a8E38b0451Ethereum0xBD20ae088deE315ace2C08Add700775F461fEa64EthereumIn Ethereum mainnet transaction 0x46567c731c4f4f7e27c4ce591f0aebdeb2d9ae1038237a0134de7b13e63d8729 at block 20834658, an unprivileged attacker used a Balancer flash loan to mint a large OEther collateral position on Onyx, borrow multiple assets against it, and then force a self-liquidation path that returned most of the ETH collateral while leaving the borrowed assets in attacker-controlled hands. The attacker exited with incident-scale VUSD, XCN, DAI, WBTC, USDT, and ETH value after repaying the 2000 WETH flash loan.
The root cause is a rounding bug in Onyx OEther redemption accounting. redeemUnderlying ultimately computes the number of oETH base units to burn by calling divScalarByExpTruncate, which floors the required burn amount instead of rounding it up. That under-burn lets a caller redeem more ETH than the burned oETH shares economically justify, which in turn perturbs collateral accounting enough to make the attacker's maximally borrowed position liquidatable inside the same transaction.
Onyx inherits the Compound-style oToken model. Users hold interest-bearing share units, and the market-level exchange rate converts those shares into underlying assets. For OEther, the share token uses 8 decimals while the underlying asset is native ETH with 18 decimals, so one smallest-unit oETH share can still map to a meaningful amount of ETH when the exchange rate is high.
The relevant invariant is straightforward: a call to redeemUnderlying(redeemAmountIn) must burn at least ceil(redeemAmountIn * 1e18 / exchangeRateMantissa) oToken base units. If the protocol instead floors that quotient, a redeemer can withdraw underlying while paying too few shares.
The pre-incident state already made the exploit feasible. Public RPC state before the transaction showed live Onyx markets, liquid OEther collateralization, borrowable oToken markets, and public liquidity sources. The attacker needed no privileged key material or private orderflow; the entire path used publicly callable contracts and observable state.
The bug is in the victim protocol's redemption logic, not in Balancer or the external tokens. In OToken.redeemFresh, the redeemAmountIn branch converts a requested underlying amount into oToken units with divScalarByExpTruncate(redeemAmountIn, Exp({mantissa: vars.exchangeRateMantissa})). In Exponential.divScalarByExpTruncate, the return value is truncate(fraction), which is a floor.
That implementation is unsafe for redemptions because the protocol must round against the redeemer. During the exploit transaction, the attacker repeatedly called redeemUnderlying(430454691) at an exchange rate near 215227346567557751910407371. The exact burn requirement was slightly below 2 oETH base units but above 1, so the protocol burned only 1. Repeating that mismatch changed the OEther market's cash-versus-share relationship without charging the attacker the full share cost.
The resulting accounting drift mattered because the attacker had already borrowed up to the practical limit against a freshly minted OEther position. Once the repeated under-burn loop pushed the position into a liquidatable state, the attacker used a separate liquidator path to repay a trivial portion of oVUSD debt, seize almost all OEther collateral, redeem the seized collateral back into ETH, repay the flash loan, and retain the borrowed assets.
The vulnerable code path is the victim-side redemption branch:
// OToken.redeemFresh
(vars.mathErr, vars.redeemTokens) =
divScalarByExpTruncate(redeemAmountIn, Exp({mantissa: vars.exchangeRateMantissa}));
The math helper makes the problem explicit:
// Exponential.divScalarByExpTruncate
(MathError err, Exp memory fraction) = divScalarByExp(scalar, divisor);
return (MathError.NO_ERROR, truncate(fraction));
For a safe redemption, Onyx needed to burn the ceiling of the underlying-to-share conversion. Instead it burned the floor. With the incident exchange rate, redeeming 430454691 wei required about 1.999999989 oETH base units, but the protocol accepted a burn of 1.
The on-chain trace shows the exploit sequence deterministically:
0xeE894c051c402301bC19bE46c231D2a8E38b0451::borrow(4107530423554)
...
OEther::seize(..., 9281190894431)
emit LiquidateBorrow(... repayAmount: 1, seizeTokens: 9281190894431)
emit Redeem(... redeemAmount: 1941634238686470224691, redeemTokens: 9021317549387)
The same trace also shows the initial collateralization and borrow setup: a Balancer flash loan of 2000 WETH, minting roughly 1999.5 ETH into OEther, and sequential borrows from the Onyx markets. The balance diff confirms that the oVUSD market lost 4107530423553 VUSD units and that the attacker EOA ended with 3807530423553 VUSD and 7350326135730346092551099 XCN, matching the reported realized proceeds.
The exploit therefore breaks the intended collateral invariant end to end:
redeemUnderlying to distort market accounting.The adversary cluster contains the sender EOA 0x680910cf5fc9969a25fd57e7896a14ff1e55f36b, the main attack contract 0xa57eda20be51ae07df3c8b92494c974a92cf8956, and the helper contract 0xAE7d68b140Ed075E382e0A01d6c67ac675AFa223 deployed during the transaction. Their roles are visible in metadata, trace flow, and final balance changes.
The transaction starts with a Balancer flash loan callback. The attacker unwraps the borrowed WETH, seeds the helper with 0.5 ETH, enters the OEther market, and mints 9290176326977 oETH base units using 1999.5 ETH. The attacker then borrows from OEther, oXCN, oDAI, oBTC, oUSDT, and oVUSD, including 4107530423554 raw VUSD units and 7350326135730346092551099 raw XCN units.
Next, the helper executes the repeated under-burn loop. The root-cause artifact describes 55 iterations of redeemUnderlying(430454691), and the validator-checked PoC reproduces the same pattern. This loop is the accounting perturbation stage that converts a barely safe position into a liquidatable one.
After the loop, the attacker uses the NFT liquidation path to repay 1 wei of oVUSD debt and seize 9281190894431 OEther units. The seized collateral is redeemed for 1941634238686470224691 wei of ETH, returning enough value to cover the 2000 WETH flash-loan repayment. The attacker then swaps part of the VUSD for WETH, repays the vault, and keeps the residual borrowed assets.
The exploit converted what appeared to be collateralized borrowing into protocol loss. The attacker extracted assets from multiple Onyx markets while recovering enough ETH collateral to settle the flash-loan leg.
Measured losses reported in smallest units are:
4107530423554 with 6 decimals7350326135730346092551099 with 18 decimals5148046590995580075613 with 18 decimals22990636 with 8 decimals50780121544 with 6 decimalsThe balance diff also records a positive native ETH delta of 2962743866393322337 wei on the attacker EOA after the transaction, showing that the attacker left the transaction with additional ETH value on top of the borrowed token positions.
0x46567c731c4f4f7e27c4ce591f0aebdeb2d9ae1038237a0134de7b13e63d87290x2CCb7d00a9E10D0c3408B5EEfb67011aBfaCb0750xeE894c051c402301bC19bE46c231D2a8E38b0451OToken.redeemFresh and Exponential.divScalarByExpTruncate in the collected Onyx source