We manage your security to weather the AI storm →

← Resource Hub

Read-Only Reentrancy: Protecting Curve and Balancer Reads

By Daniel GelfandPublished 11 min read

Read-Only Reentrancy: Protecting Curve and Balancer Reads

Quick answer. Read-only reentrancy is when your protocol reads a Curve or Balancer price mid-callback, while pool supply and balances disagree. Before every read, check the pool's lock state, for example with Balancer's ensureNotInVaultContext, and revert if it is active.

Watch: Read-Only Reentrancy: Protecting Curve and Balancer Reads 1:52
Video transcript

Your protocol can be drained without ever being re-entered. Read-only reentrancy feeds you a Curve or Balancer price mid-operation.

A view function cannot write storage, but it can return a snapshot taken halfway through another contract's update.

In Curve, LP price is the invariant D over supply. During remove liquidity, supply falls before balances update. An ETH callback then calls your oracle, which reads a stale price.

In early 2023, Midas Capital, dForce and Sentiment were exploited this way, with Jarvis's assets drained on Midas. dForce got its funds back, and Sentiment's attacker returned ninety percent.

Your non-reentrant modifier guards your storage, not the dependency's half-finished state. The fix: ask the dependency if it is mid-operation, and revert.

On Balancer V2, call ensure not in Vault context in every BPT price function. On V3, review the deployed Vault, which exposes is unlocked. Newer Curve stableswap pools lock get virtual price, but not get p.

In Guardian's GMX GLV finding M zero eight, a native token transfer ran before supply was burned, so integrators could liquidate on a wrong value.

Then inventory every LP token, BPT and vault share you price, and map each dependency's callbacks. Check the lock state on every read, then fuzz your oracle inside joins and exits.

Guardian's smart contract audit puts two independent teams on one scope, with invariant fuzzing of edge states. Read the full checklist, or talk to Guardian at guardian.security.

Why integrators get caught

Read-only reentrancy hurts the protocol that reads a price, not the pool that holds the funds. Your lending market or vault can be drained without ever being re-entered. ChainSecurity, Balancer

In early 2023, Midas Capital (draining Jarvis's jFIAT assets), dForce and Sentiment were exploited through Curve and Balancer LP prices; dForce later got its funds back. Sentiment, Aave Governance, CertiK, dForce, rekt.news

The pattern still appears in audits. Guardian findings from 2024 and 2025 flag the same stale-state window in vault and liquidity code. Guardian

This guide is for teams that price LP tokens, BPT or vault shares. It ends with a checklist to run against your own oracle.

Why view functions are not safe

A view function cannot write storage. It can still return a snapshot taken halfway through another contract's state change. Solidity Foundation

Solidity's security guidance treats every external call as a possible handoff of control. It recommends checks-effects-interactions as the defense.

Curve's get_virtual_price divides the pool invariant D by LP total supply. ChainSecurity

During remove_liquidity, supply can fall before every balance is updated. A native ETH transfer hands control to the receiver inside that gap.

The receiver then calls your protocol. Your oracle reads get_virtual_price and gets a value that will not exist once the transaction ends.

Balancer's February 2023 advisory described the same class. The Vault and its pools were each non-reentrant, but their guards were not synchronized. Balancer

During joins and exits, pool supply and Vault balances could diverge. Balancer listed stale rates, excess fee payments and bad trades as outcomes.

Nested Composable Stable pools were the highest-risk class in that advisory.

Your own nonReentrant modifier does not help. It guards your storage, not the dependency's half-finished state.

For classic, cross-function and cross-contract variants, see reentrancy attacks explained. This article covers only the integrator's side.

Sentiment, Jarvis and dForce

Midas Capital on Polygon was exploited on 15 January 2023. Aave governance first estimated about 368,000 jEUR uncollateralized, revised to 257k unbacked on 18 January. rekt.news, Aave Governance

AIP-143 froze jEUR on Aave Polygon v3, executed 28 January 2023.

Midas accepted a WMATIC-stMATIC Curve LP token as collateral. rekt.news reports its value depended on D and total supply, and D was updated after an unexpected callback.

Numen traced the reentrant path through Curve remove_liquidity on Polygon and puts the loss at 663,101 MATIC, a token count, not a USD figure. Numen Cyber

Jarvis said the attacker took 368k jEUR, 237k jCHF and 45k jGBP, leaving those assets unbacked. Its January 2023 recovery plan was a stated plan, not confirmed completion. Jarvis Network

dForce's Curve-based oracle was read while a withdrawal left pool accounting inconsistent. The inflated collateral value let the attacker borrow, CertiK reports. CertiK

dForce said on 13 February 2023 that the attacker returned all funds to its Arbitrum and Optimism multisigs. It offered a bounty and dropped its investigation and law enforcement actions. dForce, Crypto.news

Sentiment's own recovery plan says the attacker manipulated the oracle price of a Balancer LP token. Sentiment

Its postmortem names view-only reentrancy in Balancer pools during an ETH liquidity removal. BPT was burned before Vault balances updated, so Sentiment's oracle read an inflated price.

The exploiter returned 90% of misappropriated funds, 465.75 ETH, and insurers committed $65,701.03, part of it pending at the time. Bad debt and recovered assets are separate measures.

Each lending protocol, Midas, dForce and Sentiment, priced an LP token from live pool state without asking whether the pool was mid-operation.

IncidentDatePrice sourceReported impact
Jarvis via Midas, Polygon15 Jan 2023WMATIC-stMATIC Curve LP token368k jEUR, 237k jCHF, 45k jGBP taken (Jarvis)
dForce9 Feb 2023Curve-based oracleAbout $3.7M taken (CertiK); all returned 13 Feb 2023 (dForce)
Sentiment4 Apr 2023Balancer LP token priceAbout $1.09M bad debt; attacker returned 90% of taken funds

Checking an external lock state

The durable fix is to ask the dependency whether it is mid-operation, and revert if it is.

Balancer V2 publishes this check as VaultReentrancyLib.ensureNotInVaultContext. It staticcalls the Vault's manageUserBalance with an empty operation list and a 10,000 gas limit. Balancer Labs GitHub

Outside a Vault operation, the guard tries to write storage, so the staticcall fails with empty revert data. That result means the read can proceed.

Inside an operation, the guard reverts with BAL#400. The library then reverts, and so does your price read.

Because the probe is a staticcall, it works inside view functions. Put it at the top of every function that prices BPT or reads pool rates.

Balancer V3 moves BPT accounting into the Vault, so balances and BPT supply update together. During a swap, its Vault still holds balances in memory, and marks _swap nonReentrant to guard against read-only reentrancy. Balancer Docs

The V2 library does not apply to V3, whose Vault exposes isUnlocked(), true while it is executing an operation. Balancer's V3 BPT oracle contracts offer shouldRevertIfVaultUnlocked, which reverts TVL calculations while the Vault is unlocked.

Outside those oracles, rejecting BPT price reads while isUnlocked() is true is a conservative check, not a documented Balancer requirement. It also blocks reads in legitimate same-session calls, so confirm the behavior against the deployed Vault.

Stableswap-ng pools lock get_virtual_price and price_oracle, but not get_p. For older pools, ChainSecurity recommended calling the pool's withdraw_admin_fees function to trigger its reentrancy lock. Curve Finance GitHub, ChainSecurity

Do not copy that workaround blindly, since withdraw_admin_fees still moves accrued admin fees. Confirm the deployed pool source, its Vyper version and lock behavior, and that your probe cannot move fees or funds.

Guardian's Gamma Limit Order Manager finding L-14: Gamma read Algebra's globalState directly, bypassing the pool's lock, which exposed Gamma to read-only reentrancy. Guardian

It was rated Low and resolved. The finding recommended reading through safelyGetStateOfAMM, which respects the lock, or first checking the pool's isUnlocked() before any direct read.

In Guardian's reports

Guardian's GMX GLV finding M-08 shows the window in LP code. A native token transfer ran before GLV supply was burned. Guardian

A receiver callback could see assets reduced but supply unchanged. Integrators using GLV as collateral could then liquidate on a wrong value.

Guardian's Gamma Position Managers finding L-17 linked token transfers made before state writes to the same risk.

Integrator checklist

The review rule: whenever a callback can run, every price input you consume must be final, or rejected as in progress.

Encode that rule as an invariant, with handlers that call your oracle from inside joins, exits and withdrawals. Our Foundry invariant testing guide shows how to build handlers.

Run each check below against every LP token, BPT and vault share you accept.

CheckEvidence to ask forRed flag
Inventory every LP, BPT and vault share pricedOracle map naming the source contract per assetPrice taken from live pool state, undocumented
Find callbacks in each dependencyNative ETH, token hook and pool hook paths listedView-only status offered as the safety argument
Balancer V2 reads check Vault contextensureNotInVaultContext called inside the price functionBPT price or rate read with no Vault check
Balancer V3 reads match the deployed VaultDeployed Vault source reviewed; any isUnlocked() check documentedV2 assumptions applied to V3 or forks
Curve reads match the pool generationPool source shows which views carry the lockUnguarded view such as get_p, or old pool unchecked
Lock check cannot change stateStaticcall or audited non-mutating probeWorkaround that can move fees or funds
Your own code follows checks-effects-interactionsTransfers placed after all state writesTokens or ETH sent before supply updates
Fork tests read prices mid-withdrawalTest calling your oracle from a callbackOnly end-of-transaction prices tested
Invariant suite covers callback statesHandlers that re-enter during joins and exitsFuzzing only at transaction boundaries

Frequently Asked Questions

Is read-only reentrancy the same as oracle manipulation?

No. Classic manipulation moves a price with a swap, often flash-loan funded. Read-only reentrancy needs no price move: the read lands inside a half-finished withdrawal or join. Our guide to oracle manipulation attacks covers the swap-driven kind. ChainSecurity

Should the pool or the integrator fix read-only reentrancy?

Both. The root cause is a dependency calling out before its state is final, which checks-effects-interactions prevents. Deployed pools rarely change, so Balancer's advisory also set out mitigations for integrators. Solidity Foundation, Balancer

Why does Balancer's lock probe use only 10,000 gas?

The probe is expected to revert in both cases. Balancer's library caps the call so a revert caused by the guard's storage write does not waste gas. Review the library source before changing the cap. Balancer Labs GitHub

Sources

Test your LP pricing against callback states

Guardian's smart contract audit puts two independent teams on one scope, with an invariant suite fuzzing accounting, solvency and edge states, then reviews your fixes. Bring the oracle and vault code that reads Curve or Balancer.