Onyx Protocol oETH / NFTLiquidation Fake-oToken Substitution Self-Liquidation Drain (2024-09-26)
- Name: Onyx Protocol oETH / NFTLiquidation fake-oToken substitution self-liquidation drain
- Date: 2024-09-26
- Protocol: Onyx Protocol
- Chain: Ethereum
- Category: liquidation-helper trust-boundary failure / fake integration endpoint substitution / self-liquidation reward inflation
- Loss: about $3.8 million
- Summary: Onyx was exploited a second time when an attacker combined two mistakes: first, they pushed their own
oETHposition into liquidatable shortfall by abusing the same Compound-family precision edge that let tiny mint / redeem loops distort exchange-rate accounting in low-liquidity conditions; second, they fed Onyx’sNFTLiquidationhelper a counterfeitoTokenRepay, counterfeit underlying token, and counterfeitoTokenCollateral. BecauseliquidateWithSingleRepay()trusted caller-supplied market contracts by interface shape instead of proving they were canonical Onyx markets, the helper let the attacker choose an arbitraryrepayAmount, treated almost all of it asextraRepayAmount, and then executed attacker-controlled callbacks during the fake repay-token transfer path. Inside that callback, the attacker performed a realoVUSD.liquidateBorrow(attacker, 1, oETH), seized the manipulatedoETHcollateral, redeemed it to ETH, repaid the flash loan, and kept roughly4.1M VUSD,7.35M XCN,50K USDT,5K DAI, and0.23 WBTCworth about$3.8M. - Trigger / entry point:
- The attacker launched the exploit in transaction
0x46567c731c4f4f7e27c4ce591f0aebdeb2d9ae1038237a0134de7b13e63d8729after flash-borrowing2,000 WETHfrom the Balancer Vault. - They used Onyx’s
oETHmarket at0x2CCb7d00a9E10D0c3408B5EEfb67011aBfaCb075as the self-liquidation target andNFTLiquidationat0xf10bc5be84640236c71173d1809038af4ee19002as the trusted helper they would later poison with fake market addresses. - The decisive helper surface was
NFTLiquidation.liquidateWithSingleRepay(borrower, oTokenCollateral, oTokenRepay, repayAmount), which only checked thatborrowerhad shortfall and then trusted the suppliedoTokenRepay/oTokenCollateralcontracts for borrow-balance, underlying-asset, mint, and transfer behavior.
- The attacker launched the exploit in transaction
- Root cause:
NFTLiquidationaccepted attacker-suppliedoTokenRepayandoTokenCollateraladdresses without proving they were canonical Onyx markets from the Comptroller.- The helper’s decisive state machine was built entirely on that untrusted root object: it read
borrowedAmount = IOErc20(oTokenRepay).borrowBalanceCurrent(borrower), derivedextraRepayAmount = repayAmount - borrowedAmount, fetchedunderlying = IOErc20(oTokenRepay).underlying(), then approved and called back into the same caller-chosenoTokenRepayforliquidateBorrow()andmint(). - Because the counterfeit repay market returned
borrowedAmount = 0, almost the full caller-chosenrepayAmountwas reclassified asextraRepayAmount; because it pointed to a fake underlying token,transferFrom()andapprove()could report success without moving real value; and because fakeliquidateBorrow()/mint()returned success, the helper advanced as if a canonical repay-and-remint cycle had happened. - After that, the helper executed
IERC20(oTokenRepay).transfer(borrower, IERC20(oTokenRepay).balanceOf(address(this)))before its finalborrowBalanceCurrent(borrower) == 0check and before resettingextraRepayAmount = 0, giving the counterfeit repay-token contract an attacker-controlled callback point inside a still-trusted liquidation flow. - Separately, the attacker could distort
oETHexchange-rate accounting with tiny mint / redeem loops and direct ETH sends, creating a self-liquidatable account state in which repaying a tiny amount of realoVUSDdebt seized economically large amounts ofoETHcollateral. - In short: Onyx let a liquidation helper reason over attacker-authored market fiction, and the attacker used that fictional wrapper to cash out a real self-liquidation opened by precision-manipulated collateral accounting.
- Exploit path:
- The attacker EOA
0x680910cf5fc9969a25fd57e7896a14ff1e55f36bdeployed a main exploit contract at0xa57eda20be51ae07df3c8b92494c974a92cf8956plus helper contracts including theoETHrate manipulator0xae7d68b140ed075e382e0a01d6c67ac675afa223, a fake repay oToken0x4f8b8c1b828147c1d6efc37c0326f4ac3e47d068, a fake underlying token0x3f100c9e9b9c575fe73461673f0770435575dc0e, and a fake collateral oToken0xad45812c62fcbc8d54d0cc82773e85a11f19a248. - Using the Balancer flash loan, the attacker unwrapped WETH, minted
oETHwith roughly1,999.5 ETH, entered Onyx markets, and borrowed deep-liquidity assets includingXCN,DAI,WBTC,USDT, andVUSDagainst that collateral. - The attacker then sent
0.5 ETHto the rate-manipulator helper and ran roughly56tinyoETHmint /redeemUnderlying()loops around values like215,227,349 weiand330,454,701 wei, using the known precision edge in low-liquidity cToken-style accounting to distortoETHexchange-rate state enough that their own account crossed into shortfall. - Once the account was liquidatable, the attacker called
NFTLiquidationProxy.liquidateWithSingleRepay(attacker, fake_oTokenCollateral, fake_oTokenRepay, 4_764_735_291_322). - Inside
liquidateWithSingleRepayFresh(), Onyx asked the fake repay market forborrowBalanceCurrent(attacker), got0, setextraRepayAmount = repayAmount, accepted a fake underlying token whosetransferFrom()always returnedtrue, and accepted fakeliquidateBorrow()/mint()success without any canonical-market check. - The critical cash-out happened when Onyx reached
IERC20(oTokenRepay).transfer(borrower, balanceOf(this)): the counterfeit repay token used that callback to approve realVUSDto realoVUSD, calloVUSD.liquidateBorrow(attacker, 1, oETH), receive seizedoETH, redeem the seizedoETHto ETH, and send the ETH back to the main attacker contract. - The attacker then swapped part of the borrowed
VUSDinto WETH on Uniswap v3, rewrapped ETH, repaid Balancer’s2,000 WETH, and transferred the remaining stolen assets to the EOA.
- The attacker EOA
- What the attacker needed to be true:
- Onyx had to let a borrower liquidate through
NFTLiquidationusing caller-chosen market addresses rather than Comptroller-authenticated markets. - The helper had to trust interface-compatible return values from
borrowBalanceCurrent(),underlying(),liquidateBorrow(),mint(), andtransfer()without proving those contracts were real Onyx integrations. - The attacker needed a real borrow position that could be pushed into shortfall by
oETHexchange-rate distortion. - A tiny real
oVUSDliquidation had to seize meaningfuloETHonce the manipulated exchange-rate state made the borrower’s collateral appear under-water. - Sibling Onyx markets needed enough liquidity that the earlier borrow step could turn the synthetic collateral state into real extractable assets.
- Onyx had to let a borrower liquidate through
- Technical anatomy worth remembering:
- The exploit was not just
the old Compound precision bug again. The precision bug created the self-liquidation setup, but the large extraction came from a second trust-boundary failure in the liquidation helper. liquidateWithSingleRepay()was designed to repay one market, mint the leftover amount back into the same repay market, and return those oTokens to the borrower. That convenience path became the exploit primitive because the whole helper-owned authority object —which market defines debt,which underlying token should move,where extra repay is reminted, andwhat token is safe to send back to the borrower— was caller-supplied.- The code path matters:
borrowBalanceCurrent()determinedextraRepayAmount;underlying()chose the token whosetransferFrom()/approve()semantics would be trusted; fakeliquidateBorrow()and fakemint()let the helper keep advancing; and the finalIERC20(oTokenRepay).transfer(...)handed control to the counterfeit repay token before the helper proved the borrow was actually cleared. - This is a strong EVM exemplar of
interface-shapedtrust failure: the fake repay token only had to look enough like an Onyx market to satisfy local calls. No canonical market identity was enforced. - The final malicious action did not happen in the obvious liquidation call. It was smuggled into the counterfeit repay token’s
transfer()callback after Onyx had already accepted the fake market and fake accounting story.nonReentrantdid not save the protocol because the attacker did not need to re-enterNFTLiquidation; they only needed the helper to bless a counterfeit endpoint and then let that endpoint call real Onyx markets. - The sharper positive-design lesson is not merely
check the market address. A liquidation helper must keep canonical market identity, underlying-asset semantics, extra-repay meaning, and borrower / payout context fused as one protocol-owned object before any convenience repay/mint/return path runs. That is the helper-side lesson carried by helper-path invariant reuse and fail-closed discipline as much as by the counterfeit-root pattern itself. - Onyx’s second major exploit therefore preserved a different lesson than its 2023
oPEPEincident: repeating a known arithmetic family is bad, but layering an unpinned helper-integrity bug on top of it is worse.
- The exploit was not just
- Representative transactions and addresses:
- Attack tx:
0x46567c731c4f4f7e27c4ce591f0aebdeb2d9ae1038237a0134de7b13e63d8729 - Attacker EOA:
0x680910cf5fc9969a25fd57e7896a14ff1e55f36b - Main exploit contract:
0xa57eda20be51ae07df3c8b92494c974a92cf8956 oETHrate manipulator:0xae7d68b140ed075e382e0a01d6c67ac675afa223- Fake repay oToken:
0x4f8b8c1b828147c1d6efc37c0326f4ac3e47d068 - Fake underlying token:
0x3f100c9e9b9c575fe73461673f0770435575dc0e - Fake collateral oToken:
0xad45812c62fcbc8d54d0cc82773e85a11f19a248 - Vulnerable
NFTLiquidationimplementation:0xf10bc5be84640236c71173d1809038af4ee19002 NFTLiquidationProxy:0x323398DE3C35F96053D930d25FE8d92132F83d44oETH:0x2CCb7d00a9E10D0c3408B5EEfb67011aBfaCb075oVUSD:0xeE894c051c402301bC19bE46c231D2a8E38b0451VUSD:0x0BFFDD787C83235f6F0afa0Faed42061a4619B7a
- Attack tx:
- Response / mitigation:
- Onyx publicly acknowledged unusual activity the same day, but the exploit was already complete before the statement.
- The concrete fix direction was to bind liquidation helpers to Comptroller-listed markets and canonical underlying assets rather than letting callers supply interface-compatible substitutes.
- More generally, any helper that composes repay, mint, seize, and token transfer flows must treat contract identity as part of the security boundary;
looks like an oTokenis not enough. - The incident also reinforced that Compound-family precision issues were still exploitable in production whenever markets could be pushed into pathological low-liquidity states.
Pattern tags
- Counterfeit helper-root substitution:
NFTLiquidationlet the caller chooseoTokenRepayandoTokenCollateral, then built the whole liquidation path on those attacker-supplied contracts instead of loading Comptroller-authenticated markets first. - Interface-shaped debt and payout fiction: the helper trusted
borrowBalanceCurrent(),underlying(),liquidateBorrow(),mint(), andbalanceOf()return values from a fake market that only needed to satisfy the expected interface shape, not the protocol’s real market registry. - Helper-owned authority-chain collapse: the decisive trusted object was not just
which address is an oToken; it was the fullborrow balance -> underlying token -> repay sink -> remint target -> final borrower transferchain the counterfeit market was allowed to author inside one privileged liquidation wrapper. - Self-liquidation reward monetization: the fake helper path only became worth money because the attacker first pushed their own
oETHposition into shortfall, then used a tiny realoVUSDliquidation to seize economically large manipulated collateral.
Why the exploit worked
- Onyx split one liquidation authority object into attacker-chosen pieces. The caller got to decide which contract defined outstanding debt, which underlying token would be pulled and approved, which market would receive repay and mint calls, and which token contract would own the final transfer callback to the borrower.
- Canonical-market validation never arrived.
NFTLiquidationchecked the borrower’s shortfall through the Comptroller, but it did not ask the Comptroller whether the supplied repay and collateral markets were legitimate Onyx markets before trusting their methods and return values. - The fake repay market did not need to do much. Returning
borrowBalanceCurrent(borrower) = 0converted almost the whole user-chosenrepayAmountintoextraRepayAmount, while fakeunderlying(),liquidateBorrow(), andmint()responses let the helper keep advancing through a fictional repay-and-remint story. - The final control handoff mattered as much as the earlier fake reads.
IERC20(oTokenRepay).transfer(borrower, IERC20(oTokenRepay).balanceOf(address(this)))let the counterfeit repay token execute arbitrary logic before the helper proved the real borrow was cleared and beforeextraRepayAmountwas reset. nonReentrantdid not save the flow because the attacker did not need to re-enterNFTLiquidationitself. They only needed the helper to bless a fake market and then let that fake market call real Onyx contracts from the helper’s final token-return surface.- The precision-manipulated
oETHstate was the profit amplifier, not the counterfeit-root bug itself. The liquidation helper turned a self-created shortfall into extractable value because it never kept canonical market identity, real underlying semantics, and payout meaning fused.
Detection and audit pivots
- In every liquidation, deleverage, repay-and-remint, or foreclosure helper, ask whether the protocol loads canonical markets from the Comptroller / registry first or instead trusts caller-supplied market addresses to define debt, underlying token, seize target, and payout token meaning.
- Trace the whole helper-owned authority chain, not only the first market check: which contract defines
borrowBalanceCurrent(), which token address is approved, which market receivesliquidateBorrow()andmint(), and which token contract controls the finaltransfer()/ callback surface? - Treat interface compatibility as a red flag in privileged helper flows. A contract that exposes
underlying(),liquidateBorrow(), ormint()is not yet a legitimate market; the sharper question is whether the protocol itself named that exact contract as canonical for this liquidation path. - Review
extra repay,refund, andremint leftoverbranches as first-class authority surfaces. If the branch is computed from attacker-influenced debt reads, fake debt can become real payout inventory inside the helper. - Rehearse the exploit with a mixed graph: real Comptroller shortfall, real seized collateral market, fake repay market, fake underlying token, and a malicious token-return callback that never re-enters the helper. If that still cashes out, the trust boundary is helper-root authenticity, not classical reentrancy alone.
- For Compound-family forks, separate
can a low-liquidity market be forced into pathological accounting?fromwhich helper then monetizes that state?Onyx failed at both layers at once.
Failure-prevention checklist
- Load repay and collateral markets from a protocol-owned registry or Comptroller-authenticated allowlist before any borrow-balance, underlying-token, repay, mint, or transfer logic runs.
- Treat the whole liquidation convenience path as one authority object: canonical repay market, canonical collateral market, canonical underlying token, allowed leftover-remint target, and final payout destination should be fixed together rather than rediscovered from attacker-controlled contracts.
- Never let a caller-supplied market both define debt / payout meaning and own the helper’s final callback-capable token transfer surface.
- Prove real debt reduction before any leftover value is reminted, returned, or exposed to borrower-controlled callbacks.
- Make leftover-repay branches fail closed. If debt reads, mint results, or repayment semantics come from an untrusted or mismatched market, the helper should stop before any asset or tokenized claim is returned.
- Reuse canonical liquidation state machines instead of helper-local reimplementations that independently read debt, approve tokens, remint leftovers, and return wrapper tokens.
- Fuzz mixed fake/real-market paths directly: fake repay market + fake underlying + real collateral seize + final token-transfer callback into a real market.
- For low-liquidity Compound-family markets, pair market-seeding and thin-state controls with helper hardening; otherwise one market-accounting bug can still be cash-settled by an unrelated convenience wrapper.
Audit prompts
- Does the helper ever ask the Comptroller or another protocol-owned registry whether
oTokenRepayandoTokenCollateralare the exact markets this path is allowed to trust? - Which contract defines each step of the helper-owned authority chain: outstanding debt, underlying token identity, repay success, leftover remint success, and final borrower payout?
- Could a counterfeit market return
borrowBalanceCurrent() = 0and thereby turn most ofrepayAmountinto helper-controlledextraRepayAmountwithout moving any real value? - Before the helper returns any token or leftover value, what exact proof shows that the borrower’s real debt in the canonical market actually reached zero?
- Does any final
transfer()or token-return step hand control to attacker-chosen code while helper-local accounting variables likeextraRepayAmountstill imply unfinished privileged work? - If the helper never re-enters itself, can a fake token or fake market still call real protocol markets and monetize the helper’s intermediate state? If yes, the boundary failure is broader than
add nonReentrant. - Are liquidation convenience wrappers reusing the protocol’s audited core repay/seize path, or are they rebuilding it with fresh caller-supplied market and token assumptions?
- If a manipulated low-liquidity market creates shortfall, which helper or liquidation route is next in line to turn that paper shortfall into real reserve outflow?
Lessons that should already be standard
Looks like an oTokenis not a trust proof. In privileged helpers, interface shape without canonical market identity is just counterfeit infrastructure.- Liquidation helpers are not neutral plumbing. If they read debt, pull tokens, mint leftovers, or return claims, they are part of the live solvency and payout boundary.
- Borrow-balance reads, leftover-remint meaning, and final token-return callbacks belong to one protocol-owned authority object. If callers can choose each piece independently, they can compose fiction into payout.
- A final borrower-facing token transfer is still a privileged execution edge. If the token contract is attacker-chosen, the last callback can be the real exploit surface even when earlier calls already looked suspicious.
nonReentrantonly protects the frame it guards. It does not make counterfeit endpoints safe to trust or stop those endpoints from calling real protocol markets elsewhere.- Repeated market-accounting bugs become much worse when helpers can cash-settle them through separate trust-boundary failures. Arithmetic weakness and counterfeit-root acceptance compose.
- Healthy design pins canonical market identity before convenience logic begins, and only then reasons about debt, leftovers, and payout semantics.
Why this incident matters
- It preserves an unusually clear
two-stageexploit: first manufacture a profitable liquidation target out of your own position, then use a counterfeit integration endpoint to turn the protocol’s liquidation helper into the payout path. - It is a useful counterexample to the idea that only explicit upgrade or arbitrary-call bugs create fake-contract risk on EVM. Here, an ordinary convenience helper accepted counterfeit market contracts and reasoned over their return values as if they were canonical protocol state.
- It also shows how repeat incidents in one protocol can rhyme without being identical: Onyx’s 2023 exploit was a fresh-market empty-supply replay, while the 2024 exploit combined precision manipulation with liquidation-helper root-of-trust failure.
Internal linkages
- Closest recurring pattern: account-confusion-root-of-trust-substitution.
- Adjacent recurring pattern: interface-conformance-semantic-compatibility-failure.
- Enabling accounting family: empty-market-exchange-rate-inflation.
- Monetization lens: self-liquidation-monetization — the attacker first manufactured a liquidatable state in their own
oETHposition, then used a counterfeit helper path to capture the liquidation payout themself. - Positive-design companions: liquidator-independence-assumptions — the exploit broke both borrower–liquidator separation and helper-side canonical-market assumptions at once; and helper-path-invariant-reuse-and-fail-closed-discipline — useful when the sharper question is why a liquidation convenience wrapper should have loaded protocol-authenticated markets and reused one canonical repay / seize state machine instead of letting callers supply the market graph it would trust.
- Prior related incident in the same protocol family: onyx-protocol-opepe-empty-market-donation-exploit-2023-11-01.
Control surface
-
The decisive leverage sat in Onyx’s liquidation helper boundary: who was allowed to define the repay market, underlying token, extra-repay branch, mint success, and final return-transfer semantics inside a privileged liquidation flow.
-
The precision-manipulated
oETHshortfall only made the liquidation profitable. The large loss required Onyx to accept attacker-authored market contracts as if they were canonical protocol objects and then let that counterfeit market own the helper’s final callback surface. -
Sources:
- https://rekt.news/onyx-protocol-rekt2
- https://raw.githubusercontent.com/SunWeb3Sec/DeFiHackLabs/main/src/test/2024-09/OnyxDAO_exp.sol
- https://github.com/Onyx-Protocol/onyx-protocol/blob/a83176da9014f23a846c60c9dd8b7f3ff7f78027/contracts/Liquidation/NFTLiquidation.sol#L65-L150
- https://x.com/OnyxDAO/status/1839335665768845452
- https://x.com/BlockSecTeam/status/1839314277359849484
-
Last reviewed: 2026-07-26 UTC