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 USDCin 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 USDCwith a Solidly flash loan, ran that capital through OneRing’sdepositSafe()/_doHardWorkAll()path, and forced the vault’s own underlying stable-LP reserve state to become the OShare pricing oracle. The vulnerable path inMasterChefBaseStrategyultimately valued inventory throughgetUSDBalanceFromUnderlyingBalance(), which read live pair reserves and fedgetSharePrice(). That let the attacker mint about41,965,509.691846094312718922 OSharewhen the share price was about1.062758591235248117, then withdraw against a higher same-block reserve-quoted share price of about1.136563707735425848and redeem about81,534,750.101089 USDC. After repaying the flash loan, the attacker kept roughly1.454M USDC. - Trigger / entry point:
- The exploit happened on
2022-03-21in transaction0xca8dd33850e29cf138c8382e17a19e77d7331b57c7a8451648788bbb26a70145. - SlowMist identified
OneRingVaultat0xc06826f52f29b34c5d8b2c61abf844cebcf78abfas the attacked contract surface. - The critical user-facing paths were
depositSafe()for entry andwithdraw()for exit. - The critical valuation path was
getSharePrice() -> balanceWithInvested() -> investedBalanceInUSD() -> getUSDBalanceFromUnderlyingBalance()inside the strategy layer.
- The exploit happened on
- 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 reservesandthose reserves now define redeemable OShare value.
- Exploit path:
- The attacker funded their operation through address
0x12efed3512ea7b76f79bcde4a387216c7bce905eand used attack contract0x6a6d593ed7458b8213fa71f1adc4a9e5fd0b5a58. - They borrowed
80,000,000 USDCfrom Solidly via flash loan. - They routed the borrowed capital into the OneRing vault and called
depositSafe()with about79,999,997 USDC. depositSafe()called_deposit(), which in turn called_doHardWorkAll()and pushed the deposited stablecoins through the strategy’s underlying pool routing.- 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. - At that stage the vault minted about
41.965M OShareto the attacker at a share price around1.062758591235248117. - 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 around1.136563707735425848. - The attacker then called
withdraw()with the freshly minted OShare balance. - Because withdrawal reused the same reserve-driven share-pricing path, the vault redeemed those shares for about
81,534,750.101089 USDC. - The attacker repaid the
80,000,000 USDCflash loan and kept the difference, which OneRing quantified as1,454,672.244369 USDC.
- The attacker funded their operation through address
- 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()andwithdraw()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 reservesandthose 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 OShareminted around1.0627585912, then redeemed around1.1365637077, which is why an80M USDCflash-loan cycle could exit with roughly81.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.
- This was not simply
- Representative artifacts:
- Main exploit transaction:
0xca8dd33850e29cf138c8382e17a19e77d7331b57c7a8451648788bbb26a70145 - Attacker address:
0x12efed3512ea7b76f79bcde4a387216c7bce905e - Attack contract:
0x6a6d593ed7458b8213fa71f1adc4a9e5fd0b5a58 - Attacked vault surface:
OneRingVault0xc06826f52f29b34c5d8b2c61abf844cebcf78abf - Team’s laundering-out transaction reference:
0xb15d0f107ca09a76a01c588f51c17c4a26cd31ffe54be6e1a3a81272165a5bb0 - Critical functions called out in public analysis:
depositSafe(),_deposit(),_doHardWorkAll(),withdraw(),getSharePrice(),balanceWithInvested(),investedBalanceInUSD(), andgetUSDBalanceFromUnderlyingBalance()
- Main exploit transaction:
- 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()andwithdraw()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
-
Some local reserve state existed after strategy routing
_doHardWorkAll()left the underlying pair in a new reserve composition, and helper reads likegetUSDBalanceFromUnderlyingBalance()could observe it immediately.- That only proved
the strategy route currently looks like this.
-
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 valuein the same window.
-
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 latergetSharePrice()/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 about41,965,509.691846094312718922 OSharewhilegetSharePrice()read roughly1.062758591235248117, then withdrew against a later same-transaction share price around1.136563707735425848and exited with about81,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 oraclein the abstract. It was a concrete separation betweenstrategy path just moved local reservesandthose 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?butwhat 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 reservesfromvault 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 / withdrawloops 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 merelyfreshly 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 / withdrawbefore any delayed snapshot, cooldown, or activation latch fires? - What explicit cap or degraded mode would stop a one-block jump from about
1.0627to about1.1365from 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 stateis not yetsafe 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 pathandoracle pathoften 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 truthfamily. - The healthier design question is not only
should OneRing have used a safer oracle?butwhat exact gate should have stopped fresh strategy-shaped reserve state from becoming same-window mint and withdraw authority at size?
Internal linkages
- Core recurring pattern: vault-strategy-path-valuation-manipulation.
- Adjacent oracle family: oracle-manipulation.
- Adjacent protocol-self-pricing family: protocol-authored-market-state-oracle-failure.
- Positive-design companion: internal-mark-publication-quarantine-and-reserve-spend-separation — the sharp design lesson is that OneRing let one freshly strategy-shaped reserve print become immediate OShare mint and redemption authority.
- Timing companion: delayed-snapshots-and-same-block-quarantine — useful when the design question is how to break
manipulate local reserves -> publish new share price -> cash outinto separable stages.
Control surface
- The leverage sat in OneRing’s own share-pricing stack:
depositSafe(),_doHardWorkAll(),withdraw(),getSharePrice(),balanceWithInvested(),investedBalanceInUSD(), andgetUSDBalanceFromUnderlyingBalance(). depositSafe()/_doHardWorkAll()were the decisive publication surfaces: they reshaped local reserve state, and the very nextgetSharePrice()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 stateandvault may now spend reserves against that mark.
Sources
- https://medium.com/oneringfinance/onering-finance-exploit-post-mortem-after-oshare-hack-602a529db99b
- https://slowmist.medium.com/post-mortem-analysis-of-onering-incident-d6166354e46e
- https://ftmscan.com/tx/0xca8dd33850e29cf138c8382e17a19e77d7331b57c7a8451648788bbb26a70145
- https://ftmscan.com/address/0xc06826f52f29b34c5d8b2c61abf844cebcf78abf
Last reviewed
- 2026-08-01 UTC