OneRing Finance OShare LP-reserve share-price manipulation drain (2022-03-21)

  • Name: OneRing Finance OShare LP-reserve share-price manipulation drain
  • Date: 2022-03-21
  • Protocol: OneRing Finance
  • Chain: Fantom
  • Category: oracle manipulation / vault strategy-path valuation manipulation / protocol-authored market-state oracle failure / same-block reserve-quoted share-price inflation
  • Loss: OneRing said the attacker stole 1,454,672.244369 USDC in the main exploit and that flash-loan plus swap fees pushed total protocol damage to about $2M
  • Summary: The OneRing attacker borrowed 80,000,000 USDC with a Solidly flash loan, ran that capital through OneRing’s depositSafe() / _doHardWorkAll() path, and forced the vault’s own underlying stable-LP reserve state to become the OShare pricing oracle. The vulnerable path in MasterChefBaseStrategy ultimately valued inventory through getUSDBalanceFromUnderlyingBalance(), which read live pair reserves and fed getSharePrice(). That let the attacker mint about 41,965,509.691846094312718922 OShare when the share price was about 1.062758591235248117, then withdraw against a higher same-block reserve-quoted share price of about 1.136563707735425848 and redeem about 81,534,750.101089 USDC. After repaying the flash loan, the attacker kept roughly 1.454M USDC.
  • Trigger / entry point:
    • The exploit happened on 2022-03-21 in transaction 0xca8dd33850e29cf138c8382e17a19e77d7331b57c7a8451648788bbb26a70145.
    • SlowMist identified OneRingVault at 0xc06826f52f29b34c5d8b2c61abf844cebcf78abf as the attacked contract surface.
    • The critical user-facing paths were depositSafe() for entry and withdraw() for exit.
    • The critical valuation path was getSharePrice() -> balanceWithInvested() -> investedBalanceInUSD() -> getUSDBalanceFromUnderlyingBalance() inside the strategy layer.
  • Root cause:
    • OneRing did not rely on a delayed or manipulation-resistant valuation source for OShare. It priced vault inventory directly from instantaneous reserves in the underlying pair.
    • The same strategy-routing / reserve-reading path that invested user funds also published the share-price truth later consumed by mint and withdrawal. In practice, depositSafe() / _doHardWorkAll() were not only execution helpers; they were the publication path for reserve-spending truth.
    • Because depositSafe() could route a huge flash-loaned balance through _doHardWorkAll() and the vault immediately trusted the resulting reserve state, the attacker could manufacture one temporary price for minting and another for withdrawal inside the same atomic bundle.
    • In other words, the dangerous oracle was not an imported bad feed. It was OneRing’s own live reserve-accounting path, with no meaningful quarantine between strategy path just changed local reserves and those reserves now define redeemable OShare value.
  • Exploit path:
    1. The attacker funded their operation through address 0x12efed3512ea7b76f79bcde4a387216c7bce905e and used attack contract 0x6a6d593ed7458b8213fa71f1adc4a9e5fd0b5a58.
    2. They borrowed 80,000,000 USDC from Solidly via flash loan.
    3. They routed the borrowed capital into the OneRing vault and called depositSafe() with about 79,999,997 USDC.
    4. depositSafe() called _deposit(), which in turn called _doHardWorkAll() and pushed the deposited stablecoins through the strategy’s underlying pool routing.
    5. During that routing, OneRing’s strategy logic valued its position through getUSDBalanceFromUnderlyingBalance(), which used live reserve values from the underlying pair rather than a delayed or manipulation-resistant valuation object.
    6. At that stage the vault minted about 41.965M OShare to the attacker at a share price around 1.062758591235248117.
    7. The same deposit / swap path had already left the underlying pair in a distorted reserve state, so a subsequent getSharePrice() call read a higher share price around 1.136563707735425848.
    8. The attacker then called withdraw() with the freshly minted OShare balance.
    9. Because withdrawal reused the same reserve-driven share-pricing path, the vault redeemed those shares for about 81,534,750.101089 USDC.
    10. The attacker repaid the 80,000,000 USDC flash loan and kept the difference, which OneRing quantified as 1,454,672.244369 USDC.
  • What the attacker needed to be true:
    • They needed enough same-block capital to dominate the stable-LP reserve state that OneRing used as share-price truth.
    • depositSafe() and withdraw() both needed to remain callable inside one atomic flash-loan bundle.
    • The vault needed to trust instantaneous reserve composition through getUSDBalanceFromUnderlyingBalance() rather than a delayed or otherwise manipulation-resistant mark.
    • The protocol needed no quarantine between strategy path just changed local reserves and those reserves now define mint / withdraw value.
  • Technical anatomy worth remembering:
    • This was not simply stablecoin pool moved, oracle bad. The vault’s own strategy-routing and reserve-reading path was the oracle.
    • The exploit is a good cousin to Harvest, Yearn V1 yDAI, and Value DeFi, but the mechanical seam is slightly different. Harvest under-minted expensive shares on entry, Yearn let the attacker force strategy timing, and Value overpaid on a hypothetical unwind. OneRing instead reused same-block local reserve state to both invest funds and publish OShare redemption truth.
    • The sharper design failure is that OneRing collapsed three stages into one economic window: local reserve observation, internal mark publication, and reserve spend. Once _doHardWorkAll() left the underlying pair in an attacker-shaped state, getSharePrice() treated that fresh local mark as already mature enough for both mint and redemption.
    • SlowMist’s preserved numbers make the spread concrete: roughly 41.965M OShare minted around 1.0627585912, then redeemed around 1.1365637077, which is why an 80M USDC flash-loan cycle could exit with roughly 81.535M USDC.
    • The incident is also a clean example of protocol-authored oracle failure: no external feed had to break because OneRing’s own reserve-accounting path was enough.
  • Representative artifacts:
    • Main exploit transaction: 0xca8dd33850e29cf138c8382e17a19e77d7331b57c7a8451648788bbb26a70145
    • Attacker address: 0x12efed3512ea7b76f79bcde4a387216c7bce905e
    • Attack contract: 0x6a6d593ed7458b8213fa71f1adc4a9e5fd0b5a58
    • Attacked vault surface: OneRingVault 0xc06826f52f29b34c5d8b2c61abf844cebcf78abf
    • Team’s laundering-out transaction reference: 0xb15d0f107ca09a76a01c588f51c17c4a26cd31ffe54be6e1a3a81272165a5bb0
    • Critical functions called out in public analysis: depositSafe(), _deposit(), _doHardWorkAll(), withdraw(), getSharePrice(), balanceWithInvested(), investedBalanceInUSD(), and getUSDBalanceFromUnderlyingBalance()
  • Response / mitigation:
    • OneRing paused the affected vault after the exploit.
    • The team said it was working with PeckShield and node providers to recover the self-destructed attack-contract bytecode and understand the exploit path in more detail.
    • OneRing published a repayment plan, said it would redeploy and clean up the affected contracts, and offered the attacker a bounty for returning funds.
    • The postmortem direction was straightforward: stop relying on instantly manipulable reserve calculations for OShare pricing.

Pattern tags

  • vault-strategy-path-valuation-manipulation
  • protocol-authored-market-state-oracle-failure
  • state-publication-authority-failure
  • OneRing’s dangerous oracle was not an imported third-party feed; depositSafe() / _doHardWorkAll() and withdraw() reused the same live reserve-accounting path that strategy routing had just perturbed.
  • The exploit path was: flash-loan capital → strategy routing reshapes local LP reserves → getSharePrice() publishes that fresh reserve state → mint and redeem authority both honor it before any delayed snapshot or realizability check runs.
  • The real failure was one same-window collapse between local reserve observation, internal mark publication, and reserve spend, not just generic stable LP oracle manipulation.

Three execution-path valuation promotions this incident collapsed

  1. Some local reserve state existed after strategy routing

    • _doHardWorkAll() left the underlying pair in a new reserve composition, and helper reads like getUSDBalanceFromUnderlyingBalance() could observe it immediately.
    • That only proved the strategy route currently looks like this.
  2. That fresh reserve state counted as canonical OShare inventory truth

    • getSharePrice() and its supporting accounting path promoted the just-shaped reserve mix into vault share-price truth without any delayed snapshot, realizability gate, or separate publication stage.
    • This was the decisive publication failure: attacker-shaped local execution state became money-good OShare value in the same window.
  3. That accepted mark deserved immediate mint and redemption authority

    • The vault then let the attacker mint against the lower reserve-driven mark and redeem against the higher follow-on mark before the system forced any slower validation or narrower withdrawal mode.
    • This was the reserve-spend failure: freshly published local state inherited full-size USDC payout authority too quickly.

Why the exploit worked

  • OneRing’s own postmortem said the hack was a flashloan-assisted price manipulation of the LP tokens, but SlowMist’s function-level trace makes the sharper boundary explicit: depositSafe() called _deposit(), _deposit() called _doHardWorkAll(), and the later getSharePrice() / withdraw() path still trusted the reserve state that strategy routing had just changed.
  • SlowMist preserved the pricing spread that made the trade profitable. The attacker deposited about 79,999,997 USDC, received about 41,965,509.691846094312718922 OShare while getSharePrice() read roughly 1.062758591235248117, then withdrew against a later same-transaction share price around 1.136563707735425848 and exited with about 81,534,750.101089 USDC.
  • The decisive valuation helper was getUSDBalanceFromUnderlyingBalance(), which read live pair reserves (_reserves0, _reserves1) after the strategy path had already changed pool composition. That meant OneRing’s local reserve state was not merely an execution byproduct; it was the object later treated as canonical OShare redemption truth.
  • No separate oracle break, governance action, or offchain operator error was required. Once the attacker could flash-loan enough USDC to dominate the vault’s underlying reserve state for one block, OneRing itself converted that attacker-shaped state into spendable share-price authority.
  • The missing control was therefore not just use a safer oracle in the abstract. It was a concrete separation between strategy path just moved local reserves and those reserves may now define mint / withdraw value at size.

Detection and audit pivots

  • Trace whether any deposit, rebalance, or harvest path both changes local reserve composition and later feeds the same reserve snapshot into share-price, NAV, or withdrawal math. If the same economic window can do both, the protocol is already close to OneRing’s failure mode.
  • Test atomic bundles that do flash loan -> deposit / route funds through strategy -> mint shares -> withdraw / redeem. If a protocol can print a fresh internal mark and immediately spend against it before any delayed checkpoint, cooldown, or realizability check, the publication boundary is too weak.
  • Review internal valuation helpers like getUSDBalanceFromUnderlyingBalance() as oracle code, not just accounting convenience. If they consume live reserves, current active bins, or same-path LP balances, ask what stops those observations from becoming immediate collateral, redemption, or treasury authority.
  • Separate local execution state from reserve-spend truth. OneRing needed some combination of delayed snapshots, same-block quarantine, bounded redemption against freshly published marks, or a slower realizability lane before OShare pricing could move real USDC.
  • Audit publication rights as carefully as oracle sources. When a protocol’s own strategy path is allowed to author the next canonical mark, the real question is no longer only can reserves be manipulated? but what exact gate prevents that manipulated local state from being monetized immediately?

Failure-prevention checklist

  • Treat strategy-side reserve readers like getUSDBalanceFromUnderlyingBalance() as oracle surfaces with their own activation policy, not as harmless bookkeeping helpers. If a strategy path can reshape the reserves those helpers read, the resulting mark should begin life as low-trust local state.
  • Separate strategy path observed or changed local reserves from vault may mint or redeem against the resulting mark. Fresh share-price updates should age through delayed snapshots, same-block quarantine, bounded shadow publication, or another explicit spend-authorization gate before real USDC exits depend on them.
  • Refuse same-transaction deposit / route / publish / withdraw loops at size. If a path can both publish a new share price and cash out against it inside one flash-loan bundle, the publication boundary is too weak.
  • Cap how much one reserve-state update can move mint, redeem, or withdrawal authority before slower validation catches up. Large share-price jumps should narrow authority automatically rather than inherit full healthy-mode exit rights.
  • Keep a slower realizability lane for vault marks derived from LP or strategy inventory. If the fast path reads current reserves, the spend-valid path should still ask whether that state is durable enough to support full-size withdrawals.
  • Make the emergency response surface asset-local and publication-aware: pause or narrow only the affected vault / strategy mark if local reserve state looks attacker-shaped, instead of waiting until deeper treasury loss forces a broad shutdown.
  • Fuzz atomic bundles that cycle flash loan -> depositSafe() -> _doHardWorkAll() -> getSharePrice() -> withdraw() with distorted stable-pair reserves, and assert that fresh strategy-shaped reserve states cannot become immediate mint and redemption truth.

Audit prompts

  • Which exact functions are allowed to author OShare pricing truth, and can any of them also perturb the reserves or LP composition that pricing reads in the same economic window?
  • Does getSharePrice() or any sibling helper consume a mark that is merely freshly observed local reserve state, or a mark that has already cleared a separate spend-validity gate?
  • If getUSDBalanceFromUnderlyingBalance() reads current pair reserves, what proves those reserves reflect durable unwind value rather than attacker-shaped same-block execution state?
  • Can one transaction still do deposit / rebalance / publish new mark / withdraw before any delayed snapshot, cooldown, or activation latch fires?
  • What explicit cap or degraded mode would stop a one-block jump from about 1.0627 to about 1.1365 from becoming full-size redeemable authority?
  • Which downstream consumers trust the fresh mark: minting, withdrawals, collateral logic, treasury accounting, or all of them at once?
  • If monitors detect a violent reserve-state move, can operators quarantine the affected mark faster than a flash-loan attacker can recycle it into deeper reserve outflows?
  • Are strategy-routing functions reviewed as execution code only, or as execution-plus-publication code that may be silently authoring the next canonical solvency object?

Lessons that should already be standard

  • A vault’s own strategy path can be its oracle. If execution helpers also publish the next canonical share price, they are money-moving trust surfaces, not neutral plumbing.
  • Freshly observed local reserve state is not yet safe redeemable share-price truth. Publication and reserve spend need separate gates even when both live inside the same codebase.
  • Same-block reserve reads are usually too cheap to become full-size mint and withdrawal authority without delay, caps, or a stronger realizability check.
  • A protocol does not get credit for avoiding third-party oracle dependence if it simply hardens its own manipulable local state into immediate reserve authority.
  • Deposit and withdrawal paths should not share one attacker-shapeable mark with no time or policy separation. If they do, flash-loan capital can turn one local state print into both overmint and overredemption.
  • Large internal mark moves should fail narrower, not looser. The more a strategy-shaped price diverges from stable expectations, the less reserve authority it should receive until the protocol understands why.
  • Reviewing vault math without reviewing who gets to publish the next spend-valid mark leaves the real solvency boundary unguarded.

Why this incident matters

  • OneRing is a useful vault-accounting case because the exploit did not need a generic lending market to honor fake collateral or an external oracle to fail first. The vault’s own share-pricing path created the fiction and then paid it out.
  • It is a strong exemplar for the idea that execution path and oracle path often collapse together inside strategy-heavy vaults.
  • The incident also helps sharpen the line between broad oracle manipulation and the narrower protocol published its own manipulable local reserve state as canonical truth family.
  • The healthier design question is not only should OneRing have used a safer oracle? but what exact gate should have stopped fresh strategy-shaped reserve state from becoming same-window mint and withdraw authority at size?

Internal linkages

Control surface

  • The leverage sat in OneRing’s own share-pricing stack: depositSafe(), _doHardWorkAll(), withdraw(), getSharePrice(), balanceWithInvested(), investedBalanceInUSD(), and getUSDBalanceFromUnderlyingBalance().
  • depositSafe() / _doHardWorkAll() were the decisive publication surfaces: they reshaped local reserve state, and the very next getSharePrice() consumer treated that fresh state as canonical OShare mint and withdraw truth.
  • OneRing did not mainly fail because a third-party price feed lied. It failed because its strategy layer converted same-block local reserve state directly into canonical OShare mint and withdraw value.
  • The missing healthy control was a separation between strategy path observed a new reserve state and vault may now spend reserves against that mark.

Sources

Last reviewed

  • 2026-08-01 UTC