dForce wstETH/ETH Curve Vault Read-Only Reentrancy Liquidation Manipulation (2023-02-09)

  • Name: dForce wstETH/ETH Curve vault read-only reentrancy liquidation manipulation
  • Date: 2023-02-09
  • Protocol: dForce
  • Chain: Arbitrum and Optimism
  • Category: read-only reentrancy / Curve virtual-price oracle manipulation / liquidation-path exploitation / cross-chain vault integration failure
  • Loss: about $3.64M across Arbitrum and Optimism, later fully returned
  • Summary: dForce’s wstETH/ETH Curve vaults on Arbitrum and Optimism used Curve’s get_virtual_price() as a live pricing input for wstETHCRV-gauge collateral. The attacker used flash liquidity to build a large wstETHCRV position, then entered Curve’s remove_liquidity() path so ETH left the pool before LP supply had been fully updated. During that callback window, get_virtual_price() temporarily fell, not rose. dForce treated that depressed virtual price as final collateral truth, so healthy wstETHCRV-gauge borrowers looked undercollateralized. The attacker used borrowed USX to liquidate those positions at the false price, seized discounted gauge collateral, unwound after Curve state normalized, and repeated the same read-only reentrancy pattern on both Arbitrum and Optimism.
  • Trigger / entry point:
    • dForce exposed wstETH/ETH vaults on both Arbitrum and Optimism and valued wstETHCRV-gauge collateral through Curve’s get_virtual_price() path.
    • Curve’s read-only reentrancy issue in get_virtual_price() during liquidity removal had already been disclosed by ChainSecurity in 2022.
    • The attacker used a callback-capable contract to enter Curve’s remove_liquidity() flow and call dForce liquidation paths before Curve had finished settling the LP burn.
    • dForce’s liquidation logic accepted the callback-window price immediately, so a transiently lower virtual price could be turned into real seized collateral.
  • Root cause:
    • This was a read-only reentrancy exploit: the attacker did not need to corrupt dForce storage first; they only needed dForce to read Curve while Curve was mid-transition.
    • In the vulnerable remove_liquidity() sequencing, ETH was transferred out before LP supply was fully reduced, so pool balances temporarily fell while supply still looked too large.
    • get_virtual_price() therefore returned an artificially low value for wstETHCRV during the callback window.
    • dForce used that transiently depressed value as a live oracle for wstETHCRV-gauge collateral and let the attacker liquidate other positions too cheaply.
    • The attacker then redeemed the seized gauge / LP exposure after Curve state normalized, converting a temporary undervaluation into permanent profit.
  • Exploit path:
    1. On Arbitrum, attacker EOA 0xe0d551017c0111ac11108641771897aa33b2817c used attack contract 0xee29b6aee6e4783db176946e4e8f1e5fdcd446a7 in main exploit transaction 0x5db5c2400ab56db697b3cc9aa02a05deab658e1438ce2f8692ca009cc45171dd.
    2. Public reconstructions describe roughly 68,000+ ETH of flash-loaned liquidity being assembled across venues, then added into Curve’s wstETH/ETH pool to mint about 65,343 wstETHCRV.
    3. The attacker transferred roughly 1,904 wstETHCRV into a second attack leg and deposited it through dForce’s wstETHCRV-gauge vault path, receiving gauge collateral and borrowing around 2.08M USX.
    4. The attacker then called Curve remove_liquidity() on the much larger LP position. During the ETH transfer back to the attacker contract, Curve had already paid out ETH but had not yet fully updated LP supply, leaving get_virtual_price() temporarily too low.
    5. Inside that callback window, the attacker called dForce liquidation logic; Numen specifically identifies vMUSX.liquidateBorrow being used to liquidate two positions while the manipulated price was live.
    6. Because dForce valued wstETHCRV-gauge collateral from the depressed Curve virtual price, those positions appeared undercollateralized and could be liquidated at an artificial discount.
    7. The attacker seized additional wstETHCRV-gauge collateral through those liquidations, then redeemed / unwound after Curve state normalized, when the LP was worth more than the liquidation math had recognized.
    8. QuillAudits’ reconstruction puts the Arbitrum-side realized profit at about 1,236.65 WETH (roughly $1.91M at the time).
    9. The attacker repeated the same pattern on Optimism in transaction 0x6c19762186c9f32c81eb2a79420fc7ad4485aa916cab37ec278b216757bfba0d, with public reporting putting that leg near $1.73M, for about $3.64M total before funds were later returned.
  • What the attacker needed to be true:
    • dForce had to trust Curve get_virtual_price() for collateral and liquidation decisions on wstETHCRV-gauge vault positions.
    • Curve had to expose the known callback window where balances and LP supply were temporarily inconsistent during remove_liquidity().
    • dForce had to let liquidation paths consume that stale read immediately instead of rejecting callback-window upstream state.
    • There had to be existing vault borrowers whose positions became falsely liquidatable once the temporary underpricing was applied.
    • The attacker needed enough flash liquidity that the temporary mispricing and resulting liquidation discounts outweighed flash-loan costs.
  • Technical anatomy worth remembering:
    • dForce is an especially useful read-only-reentrancy exemplar because the profit came from undervaluing other users’ collateral, not from overvaluing the attacker’s own position for borrowing power.
    • The decisive output was a liquidation discount on seized gauge collateral, not a direct same-transaction overborrow against the manipulated LP mark.
    • This makes dForce a strong cousin to the more common temporary LP overvaluation -> fake collateral -> bad borrow family represented elsewhere in the corpus.
    • The vulnerability had already been publicly documented at the Curve layer, so the preserved lesson is downstream integration failure: a known unsafe view remained wired directly into solvency-critical liquidation logic.
    • The incident also shows how vault wrappers and gauge tokens do not insulate a protocol from upstream AMM callback risk if the wrapper’s value still ultimately resolves to the unsafe LP pricing primitive.
  • Representative transactions and addresses:
    • Arbitrum main exploit tx: 0x5db5c2400ab56db697b3cc9aa02a05deab658e1438ce2f8692ca009cc45171dd
    • Optimism exploit tx: 0x6c19762186c9f32c81eb2a79420fc7ad4485aa916cab37ec278b216757bfba0d
    • Arbitrum attacker EOA: 0xe0d551017c0111ac11108641771897aa33b2817c
    • Arbitrum attacker contract: 0xee29b6aee6e4783db176946e4e8f1e5fdcd446a7
    • Returned-funds txs: Optimism 0x3024a206b93fe748e5bac7f2e80974a453cda755c0d03404275d276e31ad0d77, Arbitrum 0x97f7510ade6118552e9d206b5441b13ba1c8910fdd4f82fcf7482de2cf340ecb
  • Response / mitigation:
    • dForce said it immediately paused all vaults after the exploit and stated that lending-side user funds outside the affected vaults were safe.
    • The team publicly negotiated with the attacker, claimed to have identifying information, and offered a bounty for the return of funds.
    • On 2023-02-13 dForce announced that all exploited funds had been returned to its multisigs on both Arbitrum and Optimism.
    • The recurring mitigation lesson was the same one ChainSecurity had already published for Curve integrators: do not treat get_virtual_price() as safe during callback windows; use explicit lock/context checks or other safe-read defenses before consuming it in collateral or liquidation logic.

Pattern tags

  • temporary LP markdown, not markup, became liquidation-authoritative collateral truth
  • gauge and vault wrapping did not create a new safe valuation boundary once the mark still bottomed out in Curve get_virtual_price()
  • known upstream callback-window oracle risk stayed live on two deployments because liquidation paths still trusted the raw read
  • other users' healthy collateral became buyable at a discount because liquidation consumed a mid-transition view as final state

Why the exploit worked

  • dForce’s decisive mistake was not merely using Curve LP collateral; it was letting liquidation math consume Curve’s callback-window get_virtual_price() answer as if it were a settled-state oracle. The false mark only needed to exist for one liquidation-relevant instant.
  • The wrapped object in dForce was wstETHCRV-gauge, but that wrapper did not create a new truth surface. Its value still ultimately resolved to Curve LP state, so the economically real oracle boundary remained Curve’s remove_liquidity() sequencing.
  • This is the liquidation-path cousin of the more familiar temporary markup -> overborrow read-only-reentrancy family. Here the attacker made someone else’s collateral look too cheap instead of making their own collateral look too rich, then used dForce’s own liquidation engine as the monetization rail.
  • The same unsafe integration existed on both Arbitrum and Optimism, so one upstream read-surface mistake immediately duplicated blast radius across deployments. The cross-chain detail matters because it shows how shared oracle design can be the real common-mode failure, not just shared brand.

Detection and audit pivots

  • Treat liquidation code as a first-class oracle consumer. If a temporary markdown of collateral can open a liquidation window, borrow-path hardening alone is not enough.
  • Trace wrapper and gauge collateral all the way down to the upstream value-authoring primitive. If the final answer still bottoms out in Curve get_virtual_price(), the wrapper stack did not add a safe-read surface by itself.
  • Test both sides of the read-only-reentrancy family: temporary overvaluation of the attacker’s own collateral and temporary undervaluation of someone else’s. dForce shows the latter can be just as monetizable.
  • Review multi-deployment integrations as one risk object when they share the same callback-sensitive oracle path. A known unsafe read on one chain is often already unsafe everywhere the same oracle adapter was copied.
  • Ask whether the right design answer was a canonical safe-read probe or a delayed settled-state valuation rule. If liquidation remains willing to spend a same-transaction view from an upstream callback-capable pool, the protocol is probably still trusting transition-state truth.

Failure-prevention checklist

  • Route liquidation eligibility, close-factor math, and collateral-seizure pricing through the same canonical safe-read surface as borrow and collateral-admission paths. A callback-window LP mark must never become liquidation authority merely because the protocol only hardened borrow entrypoints.
  • Treat wrapper and gauge collateral as provenance-preserving packaging, not as new oracle boundaries. If wstETHCRV-gauge still bottoms out in Curve get_virtual_price(), the integration needs an explicit lock/context check or delayed settled snapshot before that value can decide solvency.
  • Quarantine callback-sensitive LP collateral across every deployment that reuses the same oracle adapter. Multi-chain copies should inherit the fix and the same temporary caps or pauses as one shared risk object, not wait to fail chain by chain.
  • Make liquidation paths prove settled upstream state before they can spend against other users’ collateral. Useful current estimate is not enough when the next line can seize real assets at a discount.
  • Prefer previous-block or delayed snapshots when the upstream venue cannot expose a trustworthy same-transaction safe-read primitive. A slightly stale but settled price is healthier than a live price that only exists inside an AMM callback frame.
  • Bound loss even after one bad read gets through: conservative collateral factors, liquidation throttles, per-market borrow caps, and fast pause rights should assume one transient markdown can otherwise turn into immediate collateral extraction.
  • Monitor for same-block sequences where a large LP unwind is followed by liquidation of third-party accounts that share the same LP-derived collateral source. That pattern is a production signature for markdown-side read-only reentrancy, not ordinary liquidation activity.
  • Document upstream callback assumptions as executable integration requirements. If the defense depends on probing Curve’s lock or rejecting same-block pool state, that rule should live in code and tests instead of auditor memory.

Audit prompts

  • Which exact functions can turn a Curve-LP-derived price into liquidation authority, collateral ratio changes, or seizure math, and do all of them consume the same guarded safe-read surface?
  • Does the wrapper stack (wstETHCRV, gauge receipt, vault adapter) create any genuinely new settled valuation boundary, or does it merely relay Curve’s callback-sensitive get_virtual_price() one abstraction layer higher?
  • Can a transient markdown of shared collateral make healthy third-party accounts look liquidatable even when the attacker’s own borrow power is not the main monetization rail?
  • If the upstream pool offers no canonical safe to read now primitive, why is the protocol still willing to liquidate or seize against same-transaction LP marks at all?
  • Are multi-chain deployments treated as one oracle-risk object, or can the same adapter, collateral type, and liquidation logic remain live on another chain after one leg is already known unsafe?
  • What concrete control limits loss after a false markdown is consumed: delayed liquidations, borrow caps, close-factor limits, pause authority, or nothing until collateral has already been seized?
  • Would a simple invariant like LP-derived collateral may not lose liquidation-critical value during the same callback-bearing unwind that is updating pool balances fail under current tests?
  • Does the protocol distinguish informational wrapper pricing from liquidation-authoritative pricing, or can any callback-window view instantly decide who is safe to liquidate?

Lessons that should already be standard

  • Liquidation engines are oracle consumers. A protocol that hardens borrow paths but leaves liquidation willing to trust callback-window reads is still letting unsettled state move money.
  • Wrapper and gauge layers do not sanitize upstream callback risk. If the final value still bottoms out in Curve state, the wrapper has moved the trust boundary, not repaired it.
  • Markdown-side read-only reentrancy is as dangerous as markup-side borrowing exploits. Attackers do not need fake collateral for themselves if the protocol will sell someone else’s real collateral too cheaply.
  • Shared oracle integrations create shared blast radius. Reusing the same unsafe LP-pricing adapter across Arbitrum and Optimism was not operational redundancy; it was duplicated exposure.
  • Same-transaction LP pricing is a luxury, not a requirement. If the upstream venue cannot prove quiescent state during callbacks, delayed settled snapshots are the healthier design for liquidation-critical collateral.
  • A known upstream disclosure is already part of the downstream protocol’s security boundary. Once ChainSecurity had documented Curve’s unsafe read window, every integrator still trusting get_virtual_price() in solvency logic was carrying live balance-sheet risk.
  • Bounded-loss controls matter even for returned funds incidents. dForce got the assets back, but the design lesson is still that one transient read should never have had line-of-sight to immediate third-party collateral seizure on two chains at once.

Why this incident matters

  • It broadens the read-only-reentrancy branch beyond the familiar temporary overvaluation -> borrow against fake collateral pattern.
  • It preserves the liquidation-path cousin where a transient undervaluation lets the attacker buy real collateral too cheaply from the protocol’s own liquidation engine.
  • It is also a clean cross-chain reminder that reusing the same unsafe upstream oracle integration on more than one deployment multiplies blast radius immediately.
  • Just as importantly, it preserves the design lesson that liquidation paths need the same canonical safe-read surface as borrow paths; a temporary markdown can be as monetizable as a temporary markup.

Internal linkages

Control surface