All incidents

0VIX ovGHST Oracle Inflation

Share
Apr 28, 2023 10:45 UTCAttackLoss: 1,453,546.07 USDC, 584,444.54 USDT +1 morePending manual check1 exploit txWindow: Atomic
Estimated Impact
1,453,546.07 USDC, 584,444.54 USDT +1 more
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Apr 28, 2023 10:45 UTC → Apr 28, 2023 10:45 UTC

Exploit Transactions

TX 1Polygon
0x10f2c28f5d6cd8d7b56210b4d5e0cece27e45a30808cd3d3443c05d4275bb008
Apr 28, 2023 10:45 UTCExplorer

Victim Addresses

0x8849f1a0cb6b5d6076ab150546eddee193754f1cPolygon
0xe053a4014b50666ed388ab8cbb18d5834de0ab12Polygon

Loss Breakdown

1,453,546.07USDC
584,444.54USDT
9,565.87GHST

Similar Incidents

Root Cause Analysis

0VIX ovGHST Oracle Inflation

1. Incident Overview TL;DR

On Polygon block 42054769, transaction 0x10f2c28f5d6cd8d7b56210b4d5e0cece27e45a30808cd3d3443c05d4275bb008 let an unprivileged attacker inflate the value of ovGHST, the 0VIX market whose underlying asset is vGHST, and then extract value from 0VIX in the same transaction. The attacker did not need privileged keys or private orderflow: they used a fresh exploit contract, public flash-loan liquidity, and public 0VIX/Aavegotchi entrypoints.

The root cause was a bad composition between Aavegotchi's vGHST share accounting and 0VIX's custom ovGHST oracle. vGHST.convertVGHST(1 ether) was donation-sensitive because the vault counted all GHST sitting in the vault address, including unsolicited transfers, as backing for every share. 0VIX's custom oracle then treated that spot conversion as an authoritative collateral price, so a same-transaction GHST donation immediately raised ovGHST collateral value and unlocked excess borrowing power.

2. Key Background

vGHST at 0x51195e21BDaE8722B29919db56d95Ef51FaecA6C is a vault-style share token. Each share represents a claim on GHST and related staked positions, and the share-to-GHST conversion is exposed through convertVGHST(uint256).

ovGHST at 0xE053A4014b50666ED388ab8CbB18D5834de0aB12 is the 0VIX lending market for vGHST. 0VIX therefore depends on a correct vGHST price for collateral checks, borrowing limits, and liquidations.

At the exploit pre-state, 0VIX's oracle adapter OvixChainlinkOracleV2 at 0x1c312b14c129EabC4796b0165A2c470b659E5f01 resolved the ovGHST feed to 0x738FE8A918d5e43B705FC5127450e2300f7b08Ab. Exploit-block RPC reads confirm:

  • getFeed(ovGHST) returned 0x738FE8A918d5e43B705FC5127450e2300f7b08Ab at blocks 42054768 and 42054769.
  • getUnderlyingPrice(ovGHST) moved from 1169366570000000000 to 2011981510000000000 across the exploit block.

The attacker cluster identified from the transaction is:

  • EOA 0x702ef63881b5241ffb412199547bcd0c6910a970, the sender and final profit recipient.
  • Contract 0x407feaec31c16b19f24a8a8846ab4939ed7d7d57, the exploit contract that received public flash-loan liquidity and executed the on-chain sequence.

3. Vulnerability Analysis & Root Cause Summary

This was an oracle-manipulation attack, not a governance or access-control failure. The vulnerable pricing path was fully on-chain and fully synchronous: 0VIX priced ovGHST by reading a custom feed, and that feed directly multiplied GHST/USD by vGHST.convertVGHST(1 ether). Because vGHST.convertVGHST was derived from the total GHST visible at the vault address, any direct GHST donation immediately raised the reported GHST-per-share ratio without minting corresponding liabilities. That meant flash-loaned GHST could be made to look like durable collateral backing during the same transaction. Once 0VIX accepted the pumped price, the attacker's account health improved instantly, allowing the transaction to borrow and liquidate against mispriced collateral before the flash-loan unwind. The exploit therefore violated the core lending invariant that collateral prices must not be same-transaction manipulable through unsolicited asset transfers.

From the verified vGHST contract:

function totalGHST(address _user) public view returns(uint256 _totalGHST){
    uint256 totalGHSTHeld = IERC20Upgradeable(ghstAddress).balanceOf(_user);
    ...
    _totalGHST = totalGHSTHeld + totalGHSTStaked + wapGHSTtoGHST;
}

function convertVGHST(uint256 _share) public view returns(uint256 _ghst){
    uint256 totalShares = totalSupply();
    uint256 totalTokenLocked = totalGHST(address(this));
    _ghst = _share.mul(totalTokenLocked).div(totalShares);
}

From the verified VGHSTOracle contract configured for ovGHST at the exploit block:

function latestRoundData()
    external
    view
    override
    returns (uint80, int256, uint256, uint256, uint80)
{
    (bool success, bytes memory data) = vGHST.staticcall(
        abi.encodeWithSignature("convertVGHST(uint256)", 1 ether)
    );
    ...
    (, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
        = ghstFeed.latestRoundData();

    int256 price = int((toUint256(data) * uint(answer)) / 10 ** vGHSTDecimals);
    return (roundId, price, startedAt, updatedAt, answeredInRound);
}

4. Detailed Root Cause Analysis

The exploit pre-state was Polygon block 42054768, immediately before the attacker transaction entered block 42054769. Independent RPC validation at that pre-state gives:

  • vGHST.convertVGHST(1e18) = 1038332409239877123
  • VGHSTOracle.latestRoundData().answer = 116936657
  • OvixChainlinkOracleV2.getUnderlyingPrice(ovGHST) = 1169366570000000000

The direct accounting flaw is visible in the code above: totalGHST(address(this)) includes IERC20(ghst).balanceOf(address(this)). That means GHST that arrives by plain transfer is treated exactly like GHST that arrived through the intended enter() path. No checkpoint, time delay, or liability adjustment prevents that donation from affecting convertVGHST() immediately.

The exploit transaction first minted vGHST and built a helper borrow position, then donated a large amount of GHST into the vault address. The collector balance diff shows the effect on the vault directly:

{
  "token": "GHST",
  "holder": "0x51195e21bdae8722b29919db56d95ef51faeca6c",
  "before": "27512443836174838889",
  "after": "824457741083718931111488",
  "delta": "824430228639882756272599"
}

That donation materially changed the share conversion and the oracle price inside the same transaction. Independent exploit-block RPC reads confirm:

  • vGHST.convertVGHST(1e18) rose to 1786527556471688230
  • VGHSTOracle.latestRoundData().answer rose to 201198151
  • OvixChainlinkOracleV2.getUnderlyingPrice(ovGHST) rose to 2011981510000000000

The trace shows 0VIX consuming that manipulated state. The custom oracle adapter invoked getUnderlyingPrice(ovGHST), which called VGHSTOracle.latestRoundData(), which in turn called vGHST.convertVGHST(1e18) at the inflated vault state:

OvixChainlinkOracleV2::getUnderlyingPrice(0xE053A4014b50666ED388ab8CbB18D5834de0aB12)
  VGHSTOracle::latestRoundData()
    vGHST::convertVGHST(1000000000000000000)
    ...
    ← answer 201033025

The exact returned value in the trace differs slightly from the block-end read because the attack continued mutating state after this call, but both values confirm the same semantic fact: the ovGHST price was materially higher after the donation than before it.

Once that price moved, 0VIX's borrow and liquidation logic accepted the inflated collateral value. The trace then shows the attacker liquidating a helper borrow position and seizing oUSDC collateral:

0xE053A4014b50666ED388ab8CbB18D5834de0aB12::liquidateBorrow(
  0x49c6dd832d76fB9dd0DFD3a889775FAa51AF095c,
  467016954212907747535135,
  0xEBb865Bf286e6eA8aBf5ac97e1b56A76530F3fBe
)
...
emit LiquidateBorrow(
  liquidator: 0x407feAec31c16B19f24a8A8846AB4939ed7D7D57,
  borrower: 0x49c6dd832d76fB9dd0DFD3a889775FAa51AF095c,
  repayAmount: 467016954212907747535135,
  oTokenCollateral: 0xEBb865Bf286e6eA8aBf5ac97e1b56A76530F3fBe,
  seizeTokens: 5071115696329091
)

The same trace later shows the seized oUSDC being redeemed into liquid USDC:

OErc20::redeem(116681375699696032)
emit Redeem(
  redeemer: 0x407feAec31c16B19f24a8A8846AB4939ed7D7D57,
  redeemAmount: 23764949461402,
  redeemTokens: 116681375699696032
)
...
OErc20::redeemUnderlying(15050538598)

This is the code-level breakpoint and exploitation path in one chain: donation-sensitive vGHST accounting raised convertVGHST, the custom oracle exposed that spot value to 0VIX, and 0VIX then allowed extraction of real market cash against artificially inflated ovGHST collateral.

5. Adversary Flow Analysis

The adversary flow was a single-transaction, multi-stage ACT sequence:

  1. The exploit contract sourced public flash-loan liquidity from Aave V3, Aave V2, and Balancer. The collected trace contains calls to the Aave V3 pool 0x794a61358d6845594f94dc1db02a252b5b4814ad, the Aave V2 pool 0x8dff5e27ea6b7ac08ebfdf9eb090f32ee9a30fcf, and the Balancer vault 0xba12222222228d8ba445958a75a0704d566bf2c8.
  2. It called vGHST::enter(294000 ether) to mint vGHST shares and used a helper borrower contract to build a position that could later be liquidated profitably.
  3. It transferred GHST directly into the vGHST vault address, increasing the vault's counted GHST backing without any corresponding share dilution.
  4. With the ovGHST price now inflated, the attacker borrowed across multiple 0VIX markets. The trace shows borrow events from oMATIC, oWBTC, oDAI, oWETH, oUSDC, oUSDT, and ovGHST.
  5. It repeatedly liquidated the helper borrower's ovGHST debt into oUSDC collateral, redeemed the seized oUSDC into underlying USDC, and finished the transaction with liquid profits before repaying the flash loans.

The borrowing phase is visible directly in the exploit trace:

OErc20::borrow(471478201676)      // oUSDC
OErc20::borrow(452850498013984938642514) // another 0VIX market
0xE053A4014b50666ED388ab8CbB18D5834de0aB12::borrow(467016954212907747535135)

The attack remained permissionless throughout. The transaction sender was an EOA, the exploit contract was freshly deployed by that EOA, and every liquidity source and victim interaction used public protocol entrypoints.

6. Impact & Losses

The collector balance diff records the attacker's final asset gains as:

  • USDC: 1453546066478 base units (1,453,546.066478 USDC)
  • USDT: 584444536038 base units (584,444.536038 USDT)
  • GHST: 9565867204501278658739 base units (9,565.867204501278658739 GHST)

Using the same-block GHST/USD answer of 1.12619657, the reported post-transaction profit was 2048763.64935078482858608606232523 USD-equivalent before immaterial Polygon gas costs.

The loss was not confined to a single market. The transaction drained value from multiple 0VIX markets after the manipulated ovGHST collateral price increased the borrowable amount. One concrete depletion visible in the balance diff is the oUSDC market at 0xEBb865Bf286e6eA8aBf5ac97e1b56A76530F3fBe, whose USDC balance dropped from 471478201676 to 15050538598.

7. References

  1. Exploit transaction: 0x10f2c28f5d6cd8d7b56210b4d5e0cece27e45a30808cd3d3443c05d4275bb008
  2. 0VIX Unitroller: 0x8849f1a0cB6b5D6076aB150546EddEe193754F1C
  3. ovGHST market: 0xE053A4014b50666ED388ab8CbB18D5834de0aB12
  4. vGHST vault: 0x51195e21BDaE8722B29919db56d95Ef51FaecA6C
  5. Exploit-time custom ovGHST feed: 0x738FE8A918d5e43B705FC5127450e2300f7b08Ab
  6. 0VIX oracle adapter: 0x1c312b14c129EabC4796b0165A2c470b659E5f01
  7. Collected exploit metadata, trace, and balance diff under /workspace/session/artifacts/collector/seed/137/0x10f2c28f5d6cd8d7b56210b4d5e0cece27e45a30808cd3d3443c05d4275bb008/
  8. Verified source for VGHSTOracle and OvixChainlinkOracleV2 from Etherscan V2