Read-only reentrancy has emerged as one of the most dangerous and persistent vulnerability classes in decentralized finance, responsible for multiple multi-million dollar exploits throughout 2022 and early 2023. The dForce attack on February 10, which resulted in a $3.65 million loss across Arbitrum and Optimism before funds were recovered on February 13, provides an ideal case study for understanding this sophisticated attack vector at a technical level.
The Objective
This tutorial aims to equip experienced smart contract developers and security researchers with a deep understanding of how read-only reentrancy exploits work, why they are particularly insidious, and how to protect protocols against them. By the end of this walkthrough, you should be able to identify read-only reentrancy vectors in code reviews, implement the correct mitigations, and audit existing integrations for exposure to this vulnerability class.
Prerequisites
This tutorial assumes familiarity with Solidity smart contract development, understanding of the Ethereum Virtual Machine execution model, and basic knowledge of DeFi protocols including lending platforms, automated market makers, and oracle systems. You should be comfortable reading Solidity code and understanding concepts like view functions, reentrancy guards, and flash loan mechanics.
For context, you should understand that standard reentrancy involves a malicious contract calling back into the vulnerable contract during state changes, before those changes are fully committed. The classic example is an Ether withdrawal function that sends funds before updating the sender’s balance, allowing repeated withdrawals. Read-only reentrancy is a subtler variant that targets view functions rather than state-changing functions.
Step-by-Step Walkthrough
Step 1: Understand the Curve Virtual Price Oracle
Curve Finance pools expose a get_virtual_price function that returns the virtual price of LP tokens, accounting for accumulated trading fees. Many DeFi protocols, including dForce, use this virtual price as an oracle to value Curve LP tokens deposited as collateral. The virtual price should normally only increase over time as fees accumulate, making it appear safe for use in collateral calculations.
Step 2: Identify the Reentrancy Window
The vulnerability exists in Curve’s remove_liquidity function. When a user removes liquidity from a Curve pool, the contract transfers the underlying tokens to the user before updating the pool’s internal accounting. During this brief window, a callback to the user’s contract can call get_virtual_price, which now returns an incorrect value because the pool state has been partially updated — tokens have been sent but the virtual price calculation has not been adjusted.
This creates a momentary inconsistency where get_virtual_price returns a manipulated value. Any protocol reading this value during the callback window receives incorrect oracle data.
Step 3: Trace the dForce Attack Path
The dForce attacker used flash-loaned funds to deposit into Curve’s wstETH/ETH pool and receive LP tokens. These LP tokens were then deposited into dForce’s wstETHCRV-gauge vault as collateral. The attacker then called remove_liquidity on the Curve pool. During the reentrancy callback in remove_liquidity, the attacker’s contract interacted with dForce’s vault, which called get_virtual_price on the Curve pool. Because the pool state was inconsistent during the callback, the virtual price was artificially manipulated.
This manipulated oracle value allowed the attacker to overvalue their collateral position in dForce, enabling them to borrow more assets than their actual collateral supported. The difference between the borrowed amount and the true collateral value represented the exploit profit — approximately $1.9 million on Arbitrum and $1.7 million on Optimism.
Step 4: Implement the Mitigation
Curve itself recommends a simple mitigation: call any method with the nonreentrant modifier before relying on get_virtual_price. The cheapest approach is removing zero liquidity, which acquires the reentrancy lock without modifying state. Protocols integrating with Curve should wrap their oracle reads in this pattern.
An alternative mitigation is to cache oracle values rather than reading them on-demand. By reading get_virtual_price at the beginning of a transaction and using the cached value throughout, protocols eliminate the window where reentrancy could manipulate the reading.
Troubleshooting
A common mistake when implementing the mitigation is placing the reentrancy guard call in the wrong location. The guard must be called before any code that relies on the virtual price value, not after. Some developers incorrectly add the guard after their collateral calculation, which provides no protection.
Another issue arises with gas optimization. Some protocols skip the zero-liquidity removal to save gas, reasoning that the attack is unlikely. The dForce exploit demonstrates that the gas savings — typically a few thousand wei — are utterly disproportionate to the potential losses from a successful attack. Always prioritize security over gas optimization when dealing with oracle integrations.
Testing mitigations requires careful construction of attack scenarios. Unit tests should simulate the exact callback pattern used in the attack, with a malicious contract that attempts to manipulate oracle reads during remove_liquidity. Fork tests against mainnet Curve pool states provide the most reliable validation.
Mastering the Skill
Read-only reentrancy represents a broader category of vulnerabilities that security researchers call state inconsistency attacks. The same principle applies whenever one protocol reads state from another protocol that might be in a transitional state. To master this skill, practice auditing integrations between protocols, focusing on every point where external contract state is read and used in financial calculations.
The dForce exploit should not have happened. ChainSecurity disclosed the vulnerability to Curve and affected projects in April 2022, and multiple exploits using the same vector preceded dForce. Security is not a one-time activity but a continuous process of monitoring disclosures, updating integrations, and maintaining awareness of the evolving attack landscape across the interconnected DeFi ecosystem.
Disclaimer: This article is for educational purposes only. The techniques described are intended for defensive security research and protocol development. Always practice responsible disclosure and comply with applicable laws.
3.65 million exploited and recovered in 3 days. dForce got incredibly lucky. most reentrancy exploits end with a mixer and a goodbye letter
dForce lost 3.65M because auditors skip view functions assuming readonly means safe. the word readonly is doing all the heavy lifting in these exploits
ERC4626 vault totalAssets() reading stale virtual price mid-transaction is the exact vector. the pattern is documented and teams still ship it
finally a proper technical walkthrough. most coverage of the dForce exploit just says flash loan attack without explaining the actual reentrancy vector. the virtual price oracle manipulation part is key
the virtual price oracle manipulation is exactly why defi protocols shouldnt rely on spot prices from pools with withdrawable fees. classic invariant break
the dForce exploit recovering funds in 3 days was unusual. most reentrancy losses are gone permanently
dForce recovering 3.65M in 3 days was unusual luck. most reentrancy exploits end with a mixer and a goodbye. the arb and op recovery showed fast negotiation works sometimes
the prerequisites section is generous. you need serious EVM knowledge to actually follow this, not just basic solidity
serious EVM knowledge is underselling it. you need to understand how storage slots interact with external calls during execution. most devs skip this
storage slots during external calls is the key insight. most devs think view functions are safe by definition and thats exactly why this keeps happening
Ming L. storage slots during external calls is where most devs lose the thread. the EVM doesnt protect you from your own assumptions about view functions
Ming L. storage slot corruption during external calls is a fundamentally EVM-specific issue. Move VM and Solana dont have this vector because their execution models handle reentrancy differently at the VM level
audit_memo_ saying Move VM doesnt have this vector is technically true but Solana had its own read-only issues with account model confusion in 2022. different VM same class of bug
static_analysis_fan_ the Solana account model confusion in 2022 was functionally identical. different VM same developer assumption that read means safe
the dForce exploit being read-only reentrancy is wild. most audits still dont check view function calls for reentrancy guards because they assume read-only means safe
dforce got lucky recovering 3.65M in 3 days. most teams just see funds hit a mixer and vanish. the whitehat negotiation worked because the attacker wasnt sophisticated enough to bridge out fast
read-only reentrancy is sneaky because the view function technically does not modify state. static analysis tools miss it half the time
the view function returning stale virtual price during the external call is the exact vector. most auditors skip view function reentrancy because they assume readonly means safe
view_fn_nightmare spot on. the word readonly creates false confidence in audit reviews. its basically a blind spot by definition
view_fn_nightmare the assumption that view functions are inherently safe is exactly why auditors skip them. the word readonly creates false confidence in the review process
read-only reentrancy is insidious because the view function returns stale data while the state is mid-update. the ERC4626 vault pattern is especially vulnerable because totalAssets() reads happen during deposit and withdrawal loops
dForce deployed the fix on Arbitrum but the Optimism deployment was still vulnerable for 6 more hours. same team same codebase different patch schedule. bridges and cross-chain apps inherited the exposure blindly
Wei B. 3.65M recovered in 3 days because the attacker bridged to optimism where the dForce team had contacts. change the chain and that negotiation never happens