EraLend SyncSwap ETH/USDC LP Oracle Read-Only Reentrancy Exploit (2023-07-25)

  • Name: EraLend SyncSwap ETH/USDC LP oracle read-only reentrancy exploit
  • Date: 2023-07-25
  • Protocol: EraLend
  • Chain: zkSync Era
  • Category: read-only reentrancy / LP-token oracle mispricing / SyncSwap integration failure / collateral inflation / liquidation-loop extraction
  • Loss: roughly 3.4M, depending on whether the source counts protocol damage or attacker profit
  • Summary: EraLend had just launched SyncSwap USDC-ETH LP collateral and priced that LP position from SyncSwap pool state plus external token prices. The dangerous assumption was that SyncSwap reserve reads were safe during LP burns. They were not. In SyncSwapClassicPool.burn(), the pool burned LP supply, transferred out underlying tokens, and even advertised in a code comment that reserves were intentionally not yet updated before the callback ran. The attacker used that callback window to make EraLend value a tiny remaining ETH/USDC LP position against stale pre-burn reserves and already-reduced LP supply, borrowed USDC against the inflated mark, then liquidated themself after reserves normalized so they could recover the collateral cheaply and repeat the cycle until EraLend’s USDC liquidity was drained.
  • Trigger / entry point:
    • EraLend had enabled SyncSwap USDC/ETH LP positions as borrowable collateral on 2023-06-27.
    • EraLend’s LP oracle used SyncSwap pool state to determine LP value and trusted reserve amounts during same-transaction collateral checks.
    • SyncSwap burn() exposed a callback before _updateReserves(...), explicitly leaving old reserve values readable while LP supply had already changed.
    • The attacker used a self-controlled callback contract so the LP burn path itself became the moment to borrow from EraLend.
  • Root cause:
    • This was a read-only reentrancy exploit: the attacker did not need to corrupt EraLend storage first; they only needed EraLend to read upstream AMM state while it was half-settled.
    • EraLend priced SyncSwap LP collateral from reserve amounts that SyncSwap intentionally left stale during the burn callback window.
    • Because LP supply had already gone down while reserve reads still reflected the larger pre-burn pool, EraLend overvalued the attacker’s remaining LP collateral and extended real USDC against a transient fiction.
    • Once SyncSwap finished _updateReserves(...), the collateral mark fell back toward reality, but the attacker had already borrowed and could recycle the position through liquidation.
  • Exploit path:
    1. In the two main exploit transactions 0x99efebacb3edaa3ac34f7ef462fd8eed85b46be281bd1329abfb215a494ab0ef and 0x7ac4da1ea1b0903dfabda56f713ea5e4a960a3fc34467a844d037f86ee8bfe98, attacker EOA 0xf1D076c9Be4533086f967e14EE6aFf204D5ECE7a assembled large temporary ETH and USDC balances through flash loans from zkSync venues including SyncSwap, SpaceFi, and Velocore.
    2. The attacker supplied liquidity into SyncSwap’s ETH/USDC pool and received LP tokens.
    3. Only a very small slice of that LP position was deposited into EraLend as collateral for the eSYNC_ETHUSDC market; one public reconstruction describes about 0.004 LP being turned into roughly 0.222 collateral receipt tokens.
    4. The attacker then burned the rest of the LP position through a contract-controlled callback path.
    5. Inside SyncSwapClassicPool.burn(), LP supply had already been burned and token transfers had already happened, but _updateReserves(...) had not yet executed. SyncSwap’s own source comment at lines 199-200 says: Note reserves are not updated at this point to allow read the old values.
    6. During that callback window, the attacker borrowed USDC from EraLend against the tiny remaining LP collateral while EraLend’s oracle still saw stale reserve balances. Public reconstructions put one cycle’s borrow at about 344,076 USDC.
    7. After the callback returned, SyncSwap finally updated reserves and the LP collateral price dropped sharply; the same reconstruction places the corrected collateral value around 233,165 USDC.
    8. The attacker then liquidated the now-underwater position, paying only about 214,511 USDC to recover the LP collateral, which recycled the setup for the next loop.
    9. Repeating that sequence drained EraLend’s USDC side. Rekt reported about $3.4M lost, while some incident analyses separated total protocol damage from roughly $2.66M-$2.76M of attacker profit after costs.
  • What the attacker needed to be true:
    • EraLend had to accept SyncSwap ETH/USDC LP as collateral and trust live reserve reads for valuation.
    • SyncSwap had to expose a callback window where LP supply and reserve state were temporarily inconsistent.
    • EraLend had to let the inflated LP valuation feed directly into borrow capacity before any delayed oracle or context check could reject it.
    • EraLend’s USDC market had to hold enough liquid assets that the inflated mark could be monetized repeatedly.
    • Liquidation terms had to let the attacker reacquire the same LP collateral cheaply once the temporary overvaluation disappeared.
  • Technical anatomy worth remembering:
    • This is a useful read-only-reentrancy cousin because it is neither the Balancer Vault.getPoolTokens() path nor the Curve LP / virtual-price path. The stale upstream read came from SyncSwap classic-pool reserve accounting on zkSync Era.
    • The exploit was not just LP token price oracle bad. The decisive bug was when EraLend sampled pool state: during a callback window that SyncSwap itself documented as still exposing old reserves.
    • SyncSwap later argued EraLend was using the wrong LP-pricing primitive entirely: reserve amounts were a bad basis for same-transaction LP valuation here, and an invariant-aware method was safer than raw reserves.
    • Economically, the attacker did not need the inflated collateral mark to persist. They only needed it long enough to borrow, let the price collapse, and then use liquidation mechanics to cheaply reset the same loop.
    • This makes EraLend a strong example of how lending protocols can lose money even when the upstream DEX was behaving according to its own intended callback semantics.
  • Representative transactions and addresses:
    • Main exploit transactions: 0x99efebacb3edaa3ac34f7ef462fd8eed85b46be281bd1329abfb215a494ab0ef, 0x7ac4da1ea1b0903dfabda56f713ea5e4a960a3fc34467a844d037f86ee8bfe98
    • Attacker EOA: 0xf1D076c9Be4533086f967e14EE6aFf204D5ECE7a
    • Relevant upstream code path: SyncSwapClassicPool.burn() in syncswap/core-contracts commit 5285a3a7..., especially lines 199-210
  • Response / mitigation:
    • EraLend suspended lending, USDC deposits, and LP-token deposits after the exploit and publicly asked the attacker to return funds.
    • SyncSwap stated its own contracts were not drained and attributed the loss to EraLend’s LP-oracle implementation rather than a SyncSwap pool exploit.
    • SyncSwap also warned other protocols reusing the same reserve-reading pattern to review their integrations.
    • The incident reinforced the broader integration lesson later seen across other protocols: LP collateral oracles should either prove the upstream AMM is not mid-transition or avoid raw same-transaction reserve reads entirely.

Pattern tags

  • LP supply burned first while old reserves stayed readable inside SyncSwap burn callbacks
  • tiny seeded LP collateral gained real borrow authority from a reserve/supply mismatch that only existed mid-transition
  • post-normalization self-liquidation recycled the same LP collateral into a repeatable drain loop
  • reserve-based LP pricing treated a documented upstream transition state as if it were settled collateral truth

Why the exploit worked

  • EraLend’s integration assumed that raw SyncSwap reserve reads were an acceptable basis for same-transaction LP valuation, even though SyncSwap’s own burn() path deliberately left old reserve values readable until _updateReserves(...) ran.
  • That meant the protocol was effectively pricing remaining claim on pool reserves with a numerator and denominator taken from different moments in the state machine: reduced LP supply after burn, but stale pre-burn reserves before settlement.
  • The attacker only needed a tiny LP seed inside EraLend. The large temporary position lived outside the lending market and existed mainly to distort the upstream reserve/supply relation during the callback window. EraLend then converted that temporary mismatch into real USDC borrowing power.
  • Once SyncSwap normalized reserves, EraLend’s liquidation path did the rest of the work: the attacker could liquidate the now-underwater position, recover the same LP collateral cheaply, and rerun the sequence. The protocol therefore failed not only on pricing formula choice, but on lacking any canonical safe to read this pool now or wait for settled state boundary.

Detection and audit pivots

  • Review every LP-collateral oracle that derives value from raw AMM reserves plus live totalSupply() and compare it against the upstream venue’s burn, withdraw, or callback sequencing. If supply and reserves can be sampled from different stages of the transition, the oracle is probably not same-transaction safe.
  • Treat explicit upstream comments like SyncSwap’s Note reserves are not updated at this point to allow read the old values as security-relevant interface disclosures, not implementation trivia. If an integration still prices collateral from that surface, it is inheriting the callback window by design.
  • Simulate the full profit loop, not only the first borrow: inflate collateral during the callback, let the pool normalize, then test whether liquidation lets the attacker reacquire the same LP cheaply enough to repeat. EraLend’s repeatability lived in the borrow-plus-liquidation cycle together.
  • Audit newly listed LP collateral markets with extra skepticism when the listing is recent and the valuation path is custom to one DEX. The dangerous question is not just is the LP oracle mathematically plausible? but what exact upstream state is it trusting while the AMM is still settling?
  • Ask whether the safer answer was a canonical safe-read surface or an outright prohibition on same-transaction reserve-based valuation. If neither exists, borrow and liquidation logic are probably still consuming raw transition-state truth.

Failure-prevention checklist

  • Do not price LP collateral from raw reserve reads during burn or withdrawal callbacks unless the upstream venue publishes a machine-checkable safe-read surface that proves the pool is fully settled. If the venue explicitly leaves old reserves readable mid-transition, same-transaction reserve-derived collateral truth should fail closed by default.
  • Bind reserve numerators and LP-supply denominators to one settled observation boundary. A lending market should never let reserves from before burn and supply from after burn coexist inside one borrow-authoritative valuation.
  • Treat venue comments, docs, and adapter notes about stale reserves, deferred _updateReserves(...), or callback-time old-state exposure as part of the integration contract. If the upstream venue says the values are intentionally old during callbacks, the downstream protocol should assume those reads are not solvency-safe.
  • Apply the same safe-read or delayed-snapshot rule to liquidation logic as to borrow logic. EraLend lost twice: first when the inflated mark created borrow power, then when ordinary liquidation terms let the attacker cheaply reacquire the same collateral after normalization.
  • Prefer delayed settled snapshots or previous-block valuation for LP collateral when the DEX cannot expose a trustworthy same-tx pricing primitive. A brittle live-read formula is worse than an explicitly slower object.
  • Add market-level loss guards around newly listed LP collateral: low initial caps, borrow caps, liquidation throttles, and rapid pause paths should assume custom DEX integrations may still hide callback-window truth failures.
  • Test the full seed tiny collateral -> distort upstream reserve/supply ratio -> borrow -> normalize -> self-liquidate -> recycle loop in simulations. If that loop is profitable, the protocol has not actually contained the oracle boundary.
  • Monitor for same-block patterns where tiny LP collateral suddenly supports outsized borrowing and then flips immediately into liquidation eligibility. That sequence is a production signature that transient upstream state is leaking into lending truth.

Audit prompts

  • What exact object grants LP collateral its borrow power here: raw reserves, invariant-aware reserves, a venue adapter, a delayed snapshot, or something else? Can that object still be read while the AMM is mid-burn or mid-withdrawal?
  • Does the upstream venue explicitly document any callback window where reserves, balances, or fees remain stale after LP supply has already changed? If yes, why is downstream collateral logic still allowed to trust those values in the same transaction?
  • Can an attacker keep only a dust-sized LP seed inside the lending market while using a much larger off-market position to distort the reserve/supply ratio during the callback window?
  • After the temporary overvaluation disappears, does the liquidation path let the attacker buy back the same collateral cheaply enough to rerun the exploit? If so, liquidation is part of the exploit design, not just post-loss cleanup.
  • Are newly listed LP collateral markets forced onto conservative caps, delayed activation, or quarantine until the pricing surface is proven safe under callback conditions, or do they inherit full borrow authority immediately?
  • If the venue offers no canonical safe to read now primitive, what concrete mechanism forces the lending market onto delayed valuation rather than optimistic same-block reserve reads?
  • Which assumptions in the oracle came from mathematical plausibility alone rather than execution-order guarantees? EraLend’s LP formula only looked reasonable because it ignored that SyncSwap exposed old reserves on purpose.
  • Would an invariant like tiny residual LP collateral cannot gain more borrow power after a large same-tx burn or collateral price cannot spike within the same callback-bearing burn that reduced supply have caught the issue before funds left the pool?

Lessons that should already be standard

  • AMM reserve reads are not a neutral raw material. If the venue publishes them before settlement, downstream lending code is inheriting the venue’s execution order as part of its collateral model.
  • LP pricing bugs are often time-bound accounting bugs, not just bad math. The decisive question is whether numerator and denominator came from the same settled moment.
  • A liquidation path can be the second half of a read-only-reentrancy exploit. If the protocol lets the attacker recycle collateral after the transient mark collapses, liquidation mechanics are participating in the drain rather than containing it.
  • Tiny seeded collateral plus huge off-market distortion capital is a recurring integration smell. The small on-market position is enough once the protocol mistakes an upstream callback-window ratio for durable solvency truth.
  • Upstream comments that old values stay readable here should be treated like hazard signs, not implementation color. If the integration still prices from that surface, it is knowingly accepting transitional state as money-good.
  • When no canonical safe live read exists, delayed snapshots and same-block quarantine are healthier than inventing one more custom LP formula and hoping it happens to line up with venue sequencing.

Why this incident matters

  • It adds a clean read-only-reentrancy exemplar outside the current Balancer / Curve-heavy branch of the corpus.
  • It shows a distinctive profit loop: inflated collateral read, real borrow, post-normalization self-liquidation, then collateral recycle.
  • It also sharpens the difference between AMM callback semantics and downstream oracle safety: an upstream pool can be acting exactly as designed and still be unsafe to price as if its state were already final.
  • Just as importantly, it preserves the healthy-side lesson that raw reserve reads were not a real settled-state read surface, even though SyncSwap was behaving exactly as its own callback semantics intended.
  • EraLend is also a clean boundary case for the design-layer split between safe live read surfaces and delayed snapshots and same-block quarantine: if the upstream venue deliberately exposes stale reserves during ordinary burns, the healthier answer may be to forbid same-block reserve-derived collateral truth entirely rather than bolt on one more DEX-specific live-read guard.

Internal linkages

Control surface