Velocore V2 ConstantProductPool unauthenticated velocore__execute() / feeMultiplier overflow liquidity-overmint drain (2024-06-02)
- Name: Velocore V2 ConstantProductPool unauthenticated
velocore__execute()/feeMultiplieroverflow liquidity-overmint drain - Date: 2024-06-02
- Protocol: Velocore V2
- Chain: Linea, zkSync Era
- Category: missing pool-entrypoint authentication / fee-multiplier overflow / liquidity overmint / pool drain
- Loss: public reporting clustered around about
$6.8M-$7.6M; Velocore and rekt.news emphasized about$6.8Min drained ETH-equivalent, while flow-of-funds reporting tracked about1,807 ETHlaundered back to Tornado Cash on Ethereum mainnet - Summary: Velocore’s Balancer-style ConstantProductPool expected the Vault to be the only caller of pool-local
velocore__execute()swap logic, but the pool contract never authenticated that boundary. The attacker calledvelocore__execute()directly on volatile-pool contracts with craftedrarrays that simulated huge LP withdrawals in the same block, ratchetingfeeMultiplierhigh enough thateffectiveFee1e9exceeded100%. Once the fee path crossed that boundary, later pool math underflowed and treated a small single-token withdrawal / LP-mint path as if it were a huge deposit, minting far too many LP claims. The attacker then unwound those counterfeit claims through ordinary pool logic, drained volatile pools on Linea and zkSync, bridged proceeds to Ethereum, and sent roughly1,807 ETHinto Tornado Cash. - Trigger / entry point:
- Velocore’s public router path normally entered
Vault.execute(), which in turn called pool-localISwap(opDst).velocore__execute(...)on ConstantProductPool instances. - The ConstantProductPool implementation exposed
velocore__execute(address, Token[] calldata, int128[] memory, bytes calldata)asexternaland did not check thatmsg.senderwas the canonical Vault. - Inside that function, LP-withdraw-like flows updated
feeMultiplierviafeeMultiplier = ((feeMultiplier * invariantMax) / (invariantMax - uint256(int256(r.u(iLp))))).toUint128();wheneverr[iLp] > 0. - The same function later derived
effectiveFee1e9 = (fee1e9 * feeMultiplier) / 1e9underuncheckedmath whenlastWithdrawTimestamp == block.timestamp.
- Velocore’s public router path normally entered
- Root cause:
- Velocore treated
velocore__execute()as if it were an internal pool callback reachable only through the Vault, but encoded that assumption socially instead of in-contract. - Because the pool itself owned
feeMultiplierstate, a direct caller could mutate the same pricing state that later legitimate Vault-routed operations trusted. - The design then compounded the missing-authentication bug with unsafe arithmetic assumptions:
effectiveFee1e9was allowed to grow past1e9, meaning past100%fees. - Once that happened, the fee-adjustment branch could underflow the growth math and invert the economics of a withdrawal / LP-mint path, so the pool priced a tiny payment as if it funded a much larger liquidity claim.
- Velocore treated
- Exploit path:
- The attacker funded fresh addresses from Tornado Cash and targeted Velocore V2 volatile ConstantProductPool deployments on Linea and zkSync.
- They called pool-local
velocore__execute()directly instead of going through the Vault, supplying crafted token arrays and extremervalues that simulated very large LP withdrawals. - Repeating that maneuver inside one block kept
lastWithdrawTimestamp == block.timestampand ratchetedfeeMultiplierupward through the pool’s own withdrawal-side state update. - On subsequent execution,
effectiveFee1e9became greater than1e9, so the fee term insideunaccountedFeeAsGrowth1e18 = rpow(1e18 - ((1e18 - k) * effectiveFee1e9) / 1e9, ...)crossed the range the surrounding math expected. - That broken fee state made the pool mis-handle a later operation as if a small withdrawal were actually a huge deposit, minting an outsized amount of LP value / pool claim for trivial input.
- The attacker then used ordinary pool exits and swaps to redeem those counterfeit claims for real pool assets, draining the affected volatile pools.
- They consolidated proceeds, bridged assets to Ethereum mainnet via Across, converted funds toward ETH, and routed roughly
1,807 ETHinto Tornado Cash.
- What the attacker needed to be true:
- The targeted pool had to expose
velocore__execute()publicly without Vault-only authentication. - The pool’s same-block withdrawal logic had to keep compounding
feeMultiplierinstead of bounding or saturating it below the100%fee threshold. - Later quote / settlement logic had to keep trusting the corrupted
effectiveFee1e9value strongly enough that a malformed withdrawal path flipped into a highly favorable mint / deposit outcome. - The volatile pools had to remain liquid enough that the counterfeit LP claim could be redeemed into real assets before operators paused the system.
- The targeted pool had to expose
- Technical anatomy worth remembering:
- This was not just an arithmetic bug and not just an access-control bug. The decisive failure was their composition: an unauthenticated internal-looking pool verb let attackers mutate a fee state variable that downstream math assumed stayed within protocol-authored bounds.
- The exploit is a clean
official entrypoint versus real state authoritylesson. Velocore’s public UX and router centered onVault.execute(), but the economically decisive state mutation lived in a pool-local function that anyone could call. - The cash-out rail was classic issuance/redemption asymmetry: once the pool undercharged the claim-creation side, normal redemption and swap logic still paid out against real reserves.
- The affected surface was the volatile ConstantProductPool implementation; Velocore stated stable pools and Slipstream were unaffected.
- Linea’s operator halted block production for roughly an hour to slow bridge-out and limit further damage, which also made the incident a memorable example of L2 operator intervention during live protocol failure.
- Representative transactions and addresses:
- Attacker EOA highlighted by rekt.news:
0x8cdc37ed79c5ef116b9dc2a53cb86acaca3716bf - Representative Linea attack tx:
0xed11d5b013bf3296b1507da38b7bcb97845dd037d33d3d1b0c5e763889cdbed1 - Representative Linea attack tx:
0x37434e674efc4e7cfeed7746095301ace5636028906fe548b786ead286e35eb0 - Representative zkSync attack tx:
0x4156d73cadc18419220f5bcf10deb4d97a3d3f7533d63ba90daeabc5fd11ba17 - Final Ethereum destination before Tornado Cash:
0xe4062fcade7ac0ed47ad794028967a2314ee02b3 - Exploited Linea pool contracts cited publicly:
0xed4e130f6f9e68918996f7e1e46a3306b3e12cec,0xb7f6354b2cfd3018b3261fbc63248a56a24ae91a,0xc030fba4b741b770f03e715c3a27d02c41fc9dae - Exploited zkSync pool contract cited publicly:
0xf7f76b30a301524fe76508546B1e3762eF2B9267
- Attacker EOA highlighted by rekt.news:
- Response / mitigation:
- Velocore acknowledged the exploit on 2024-06-02, paused the affected volatile pools, and published a postmortem describing the fee-overflow and access-control failure.
- The team stated stable pools and Slipstream were unaffected.
- Linea halted the sequencer for about an hour as an emergency measure to impede further bridging-out while trying to contact the Velocore team.
- Velocore later sent an onchain message offering the attacker a
10%whitehat bounty in exchange for return of funds. - The incident triggered renewed scrutiny of whether audited protocols had actually authenticated their internal-looking pool callbacks and whether same-block fee or withdrawal multipliers had hard upper bounds.
Pattern tags
- issuance-redemption-solvency-asymmetry
- entrypoint-only-policy-off-path-state-mutation
- The economically decisive break was
mutate pool-local fee state off-path, then mint or withdraw a reserve-backed LP claim on-path: the attacker did not need to globally falsify balances, only to poison a fee variable that later pricing logic treated as protocol-authored truth. - Velocore is also a good
internal-looking external verb became the real trust boundaryexemplar.Vault.execute()was the intended product-facing path, butConstantProductPool.velocore__execute()itself controlled the fee state and quote semantics that determined how many real reserves a tiny input could later command. - The exploit belongs to the
payment side under fiction, unwind side under realitysubfamily: onceeffectiveFee1e9moved outside its intended domain, the pool could mint or release too much LP-side value for trivial payment while ordinary exit logic still honored those claims against live assets.
Why the exploit worked
- The exploit worked because Velocore placed the real solvency boundary inside a pool-local function but only socially enforced who was supposed to cross it. The Vault and router were presented as the official path, yet
velocore__execute()itself held authority to mutatefeeMultiplierand therefore to shape the pricing state every later user-facing operation relied on. - The arithmetic bug mattered only because it sat behind that missing caller check. A bounded same-block fee multiplier might have been a local pricing oddity; an unbounded multiplier in a publicly callable pool verb became attacker-authored reserve truth. Once repeated direct calls pushed
effectiveFee1e9above1e9, the surrounding growth math stopped meaningsmall withdrawal pays a feeand started meaningsmall input can create a much larger LP claim than reserves justify. - The decisive asymmetry was
issue or release cheaply, redeem honestly later. Velocore did not need to lose track of every balance in the system. It only needed one path that undercharged for LP-style claim creation while later pool exits and swaps still paid out against real volatile-pool inventory. - This is why the incident is more useful than a generic
overflow buglabel suggests. The important lesson is not justfees should not exceed 100%; it ispool-local quote state, caller authority, and reserve-paying unwind logic were one joined balance-sheet object. Velocore broke that object at the function boundary first and only then at the arithmetic boundary. - Linea’s sequencer halt reinforces the same point operationally: once the counterfeit claim could already be redeemed into real bridgeable assets, even a chain-level response was reduced to damage containment rather than preventing the underlying accounting lie.
Detection and audit pivots
- Inventory every
externalpool or strategy verb that product docs describe asonly called by the vault,router,manager, orcore. If the contract itself does not authenticate that boundary, the real authority surface is wider than the architecture diagram. - Treat fee multipliers, same-block withdrawal penalties, growth factors, and quote caches as solvency state, not UX state. If poisoning one of those variables can later change LP mint, burn, or swap economics, it belongs inside the reserve boundary.
- Differential-test
small payment -> large LP claim -> ordinary unwindround trips under same-block repeated calls, extreme signed deltas, and attacker-chosen token arrays. The useful question is not onlycan the math overflow?butcan an off-path caller move quote state into a domain where later on-path operations still honor counterfeit value? - Explicitly assert domain limits such as
effectiveFee1e9 <= 1e9or whatever stronger invariant the design actually needs. Do not rely on economic intuition that a multiplier can never get there if one public state-mutating path still exists. - Review whether pool-local execution surfaces both mutate state and interpret user intent. The more one function simultaneously parses token arrays, updates fee state, and computes reserve effects, the more dangerous a missing caller check becomes.
Failure-prevention checklist
- Authenticate every pool-local execution verb at the pool boundary. If only the Vault may call
velocore__execute(), the pool must enforce that invariant itself rather than inheriting it from docs, routers, or frontends. - Bound fee-state variables with fail-closed arithmetic and semantic caps. Multipliers, penalties, and growth factors should revert or saturate before they cross domains the surrounding math was never designed to price.
- Separate
state that tracks user activityfromstate that certifies solvency-critical quote truth. If one same-block withdrawal statistic can later change how much LP value a trivial input receives, that state needs the same protection as reserve accounting. - Add invariant tests for
add/mint/remove/burnround trips under poisoned fee state, repeated same-block operations, attacker-sized signed deltas, and direct pool calls that bypass normal routing. - Constrain blast radius even if quote state fails: per-pool withdrawal caps, rate limits, delayed large exits, and fast pauses sized for
pricing state may already be counterfeitrather than only for normal volatility. - Inventory every internal-looking public function during audits and upgrades. A function is part of the threat model if it can still mutate reserve-significant state, even if the intended UI never calls it directly.
Audit prompts
- Which exact external functions can mutate fee, growth, inventory, or quote state without first proving they were reached through the canonical Vault or router?
- Can an attacker supply token arrays, signed deltas, or synthetic withdrawal-like parameters that update fee state without paying the economic cost the state variable is supposed to summarize?
- What hard invariant keeps
effectiveFee1e9,feeMultiplier, or similar derived fee variables inside the domain assumed by downstream mint/burn/swap math? - If quote state becomes poisoned for one block, which later operations still honor it as reserve truth: LP minting, LP withdrawal, swaps, gauge accounting, or bridgeable asset extraction?
- Does any same-block fast path combine three roles at once — parsing user intent, mutating pricing state, and releasing value — such that one missing caller check becomes a full solvency bug rather than a local accounting nuisance?
- What concrete cap, pause, or delayed-withdrawal control limits loss if an attacker can already create counterfeit LP claims before operators notice?
Lessons that should already be standard
Only the vault calls thisis not a security property unless the pool enforces it. Architecture intent and frontend routing do not narrow onchain authority by themselves.- Fee state is part of the custody model once later quote, mint, or unwind logic trusts it. Multipliers and same-block penalty variables are not harmless tuning knobs when they can reprice reserve-backed claims.
- Access-control bugs and arithmetic bugs compound. A state variable that is safe under trusted callers may become a treasury-drain primitive under public callers, even if each issue looks less dramatic in isolation.
- Same-block convenience logic deserves hostile review. Paths added for anti-MEV, anti-withdrawal-gaming, or fee fairness often become the easiest place to smuggle state into an unintended domain.
- Reserve-outflow brakes still matter after correctness work. Linea’s halt did not fix Velocore’s bug, but it did preserve the lesson that bounded-loss design is the difference between one broken quote and an instantly unrecoverable treasury event.
Why this incident matters
- Velocore is a strong exemplar of how
only the router / vault is supposed to call thisis not a security boundary unless the pool itself enforces it. - It also preserves a particularly useful CLMM/AMM cousin of issuance-redemption asymmetry: the attacker did not need to forge balances globally; they only needed one path that minted liquidity claims too cheaply while the unwind side stayed economically honest.
- The event is also worth preserving because chain-operator intervention became part of the incident story: the exploit anatomy and the emergency halt are both central to understanding real-world L2 risk surfaces.
Internal linkages
- Positive-design companion: reserve-backed claim truth as a single security object — the healthier framing is that caller authority, fee-state integrity, claim quantity, and reserve-paying unwind rights should all be fused into one proof object rather than split across social routing assumptions and pool-local mutable state.
- Adjacent pattern: issuance-redemption-solvency-asymmetry — the sharp reusable lens is
undercharged claim creation, still-honest reserve redemption, expressed here through LP-style pool claims rather than a simple mint/burn token. - Adjacent pattern: entrypoint-only-policy-off-path-state-mutation — Velocore’s public policy boundary centered on Vault-routed execution, but the pool-local state that actually priced solvency could still be mutated off-path by direct callers.
- Adjacent incident: cetus-protocol-clmm-checked-shlw-truncation-liquidity-overmint-pool-drain-2025-05-22 — another concentrated-liquidity / AMM case where the payment side undercharged for a liquidity claim that the unwind side later honored against real reserves.
Control surface
-
The decisive control surface sat inside
ConstantProductPool.velocore__execute(), not inside the user-facing Vault. -
That one function simultaneously acted as quote engine, fee-state mutator, and LP-mint/burn state transition surface.
-
Once Velocore failed to bind
who may call this functiontowho may mutate feeMultiplier-backed pricing state, the pool’s own pricing memory became attacker-writable and the rest of the reserve-paying logic simply obeyed it. -
Sources:
- https://rekt.news/velocore-rekt
- https://medium.com/zokyo-io/postmortem-velocore-90cc92996aad
- https://medium.com/neptune-mutual/taking-a-closer-look-at-velocore-exploit-f029df18cbe5
- https://www.merklescience.com/blog/investigating-the-velocore-hack-and-flow-of-funds
- https://www.theblock.co/post/298032/decentralized-exchange-velocore-addresses-7-million-hack-in-postmortem-offers-bounty-to-hacker
-
Last reviewed: 2026-07-18 UTC