This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0x599297512c6bc869e2381f2eed7189280fc258d3a26d5b4508b48acbfb0889a40xed11d5b013bf3296b1507da38b7bcb97845dd037d33d3d1b0c5e763889cdbed10x1d0188c4b276a09366d05d6be06af61a73bc7535LineaOn Linea block 5079177, attacker EOA 0x8cdc37ed79c5ef116b9dc2a53cb86acaca3716bf called helper contract 0xb7f6354b2cfd3018b3261fbc63248a56a24ae91a, which routed a multi-stage exploit through Velocore V2 vault proxy 0x1d0188c4b276a09366d05d6be06af61a73bc7535. The helper first performed repeated same-block withdrawal-style interactions against constant-product plugin 0xbD4B27Dd15bc467d5285c0D7435663935B4b6f7F, then finished with an LP max-output withdrawal that caused the pool to mint an enormous forged LP balance to the vault and let the helper transfer those LP tokens plus real vault assets out.
The root cause is a broken fee-domain invariant in Velocore's constant-product pool implementation. Same-block withdrawal compounding allowed feeMultiplier to grow far above the intended 1e9 ceiling, and later unchecked fee arithmetic in ConstantProductPool.velocore__execute reused that out-of-range value in formulas that assume effectiveFee1e9 <= 1e9. Once the multiplier crossed that bound, the LP-growth path underflowed and returned a huge negative LP delta that the vault honored.
The victim address 0x1d0188c4b276a09366d05d6be06af61a73bc7535 is an unverified proxy. The collected source metadata shows it points to verified implementation 0x2e98ef87f7f0d31987a0d94051b8bc5d001152e8, whose source tree identifies the logic as SwapFacet. delegates pricing and delta computation to plugin pools via , then validates per-token maximums and applies the resulting deltas from the vault's balances.
SwapFacetISwap.velocore__executeVelocore constant-product pools maintain same-block withdrawal penalty state in packed storage slot 6. The matched source exposes fee1e9, lastWithdrawTimestamp, and feeMultiplier, and on same-block execution computes:
uint256 effectiveFee1e9 = fee1e9;
if (lastWithdrawTimestamp == block.timestamp) {
unchecked {
effectiveFee1e9 = effectiveFee1e9 * feeMultiplier / 1e9;
}
}
The same implementation also updates the multiplier on repeated withdraw-style calls:
if (additionalMultiplier > 1e9) {
if (caller.lastWithdrawTimestamp() == block.timestamp) {
caller.notifyWithdraw((additionalMultiplier * caller.feeMultiplier() / 1e9).toUint128());
} else {
caller.notifyWithdraw(additionalMultiplier.toUint128());
}
}
Pool LP tokens are standard ERC20 balances from the vault's perspective. When the plugin returns a negative LP delta for the user, the vault can end up transferring newly available LP inventory just like any other token balance.
This incident is an ATTACK, not a pure MEV unwind. The constant-product implementation violates its own fee-domain invariant: for every execution, effectiveFee1e9 = fee1e9 * feeMultiplier / 1e9 must stay in [0, 1e9] so downstream fee formulas remain non-negative. The first violating operation is the same-block compounding write in ConstantProductLibrary.velocore__execute, which stores an unbounded multiplier through notifyWithdraw. The loss-causing operation appears later in ConstantProductPool.velocore__execute, where unchecked arithmetic evaluates expressions that assume effectiveFee1e9 <= 1e9.
The critical unchecked growth expression is:
uint256 unaccountedFeeAsGrowth1e18 = k >= 1e18
? 1e18
: rpow(1e18 - ((1e18 - k) * effectiveFee1e9) / 1e9, _sumWeight - sumUnknownWeight - sumKnownWeight, 1e18);
When effectiveFee1e9 exceeds 1e9, 1e18 - ((1e18 - k) * effectiveFee1e9) / 1e9 can underflow in unchecked arithmetic. That wrapped value feeds rpow(...), inflates the requested growth factor, and causes the computed-LP branch to return a huge negative LP delta for the attacker. The vault then accepts those plugin deltas and settles them from its own balances.
The ACT opportunity exists at the publicly reconstructible Linea pre-state immediately before block 5079177, together with the earlier attacker-controlled helper deployment at block 5078839, the verified SwapFacet implementation, the drained plugin bytecode/decompilation artifacts, and the collected trace and balance-diff artifacts for the seed exploit transaction.
The adversary transaction sequence is short and permissionless:
0x599297512c6bc869e2381f2eed7189280fc258d3a26d5b4508b48acbfb0889a4 deploys helper 0xb7f6354b2cfd3018b3261fbc63248a56a24ae91a.0xed11d5b013bf3296b1507da38b7bcb97845dd037d33d3d1b0c5e763889cdbed1 calls helper function selector 0x23bec322, which executes the warmup withdrawals and final drain through the public vault.The helper deployment and ownership linkage are directly observable. The Linea tx list for 0x8cdc37... shows the deployment tx, and the helper bytecode constructor writes vault 0x1d0188... to slot 0 and owner 0x8cdc37... to slot 1. The seed metadata then shows the same EOA calling the helper in the exploit tx.
The on-chain trace confirms the warmup on plugin 0xbD4B27.... Three same-block withdraw-style calls are made with the helper as user before the final LP max-output call. The trace records slot-6 changes on the plugin:
@ 6: ...00002d76a1242405665ba2a90007a120ffffd323
-> ...0008e12b35ceedeb665ba2a90007a120ffffd323
@ 6: ...0008e12b35ceedeb665ba2a90007a120ffffd323
-> ...01bbf7394adb5abd665ba2a90007a120ffffd323
Those packed writes correspond to feeMultiplier growth from 999750665832 to 32759829555538, then 639840132168280, then 124965240123906749, as summarized in root_cause.json. With fee1e9 = 500000, the effective fee moves from 499875332 to 24993766445, then 1249687758141, then 62482620061953, which is far outside the intended domain.
The final drain uses the LP max-output path. In the seed trace, the last bD4B27::velocore__execute call is:
0xbD4B27...::velocore__execute(
0xb7f6354b2cfd3018b3261FbC63248a56A24AE91A,
[USDC, bD4B27 LP],
[-1000, 170141183460469231731687303715884105727],
0x
)
Immediately after that call, the trace shows a forged LP mint to the vault and then transfers from the vault to the helper:
emit Transfer(from: 0x0000000000000000000000000000000000000000,
to: 0x1d0188c4B276A09366D05d6Be06aF61a73bC7535,
value: 23399587659403732034928174213380)
emit Transfer(from: 0x1d0188c4B276A09366D05d6Be06aF61a73bC7535,
to: 0xb7f6354b2cfd3018b3261FbC63248a56A24AE91A,
value: 7863961973)
emit Transfer(from: 0x1d0188c4B276A09366D05d6Be06aF61a73bC7535,
to: 0xb7f6354b2cfd3018b3261FbC63248a56A24AE91A,
value: 9394729335205909844521)
emit Transfer(from: 0x1d0188c4B276A09366D05d6Be06aF61a73bC7535,
to: 0xb7f6354b2cfd3018b3261FbC63248a56A24AE91A,
value: 23399587659403723439593111287819)
This sequence matches the matched source exactly:
if (iLp != type(uint256).max && r.u(iLp) < 0) {
_simulateMint(uint256(int256(-r.u(iLp))));
}
The vault-side settlement path in SwapFacet then accepts the plugin output and emits the swap event after applying the deltas:
(int128[] memory deltaGauge, int128[] memory deltaPool) =
ISwap(opDst).velocore__execute(user, opTokens, opAmounts, op.data);
_verifyAndApplyDelta(cumDelta, IPool(opDst), opTokens, opTokenInformations, deltaGauge, deltaPool);
emit Swap(ISwap(opDst), user, opTokens, deltaGauge);
The exploit conditions are therefore concrete and complete:
type(int128).max, converting the underflowed growth term into a forged negative LP delta.The adversary cluster contains:
0x8cdc37ed79c5ef116b9dc2a53cb86acaca3716bf, which deployed and called the helper.0xb7f6354b2cfd3018b3261fbc63248a56a24ae91a, which stores the owner EOA and receives the drained assets.The end-to-end exploit flow is:
5078839.5079177, call helper selector 0x23bec322.0xbD4B27..., perform three warmup withdrawals with [USDC, wUSK, LP] and increasingly smaller USDC out values.[USDC, LP] and [-1000, type(int128).max] to force the corrupted computed-LP branch.The trace also shows the helper receiving native ETH directly from the vault during the transaction:
SM Address: 0xb7f6354b2cfd3018b3261fbc63248a56a24ae91a, caller:0x1d0188c4...
transfer:Transfer(155165503281178836250)
The exploit was not dependent on privileged access, attacker-owned preexisting contracts, or private orderflow. The observed helper includes an owner check, but that check is irrelevant to ACT classification because any unprivileged actor could deploy an equivalent helper and call the same public contracts with fresh addresses.
The balance-diff artifact for transaction 0xed11d5b013bf3296b1507da38b7bcb97845dd037d33d3d1b0c5e763889cdbed1 shows the vault lost native ETH and six ERC20 assets while helper 0xb7f6354... gained them:
ETH: 322763510715071356449 weiUSDC: 632705032352 unitsUSDT: 254960363616 unitswstETH: 40341960757766560011 unitsMENDI: 113049039828402264211503 unitsWBTC: 58291266 unitswUSK: 9394729335205909844521 unitsThe helper's native balance increased from 0 to 322763510715071356449 wei. The sending EOA paid only 159061698557196 wei in gas. root_cause.json therefore reports a lower-bound profit of 322.763351653372799253 ETH even before valuing the non-ETH tokens that were also drained.
The impact extends beyond a single isolated market. The seed trace shows the attacker warming up and draining multiple identical constant-product plugins in one transaction, so the vault lost inventory across several pool balances.
The validation and report are grounded in these concrete artifacts and contracts:
0xed11d5b013bf3296b1507da38b7bcb97845dd037d33d3d1b0c5e763889cdbed10x599297512c6bc869e2381f2eed7189280fc258d3a26d5b4508b48acbfb0889a40xb7f6354b2cfd3018b3261fbc63248a56a24ae91aSwapFacet source for implementation 0x2e98ef87f7f0d31987a0d94051b8bc5d001152e80xbD4B27Dd15bc467d5285c0D7435663935B4b6f7F/tmp/velocore-contracts/src/pools/constant-product/ConstantProductLibrary.sol
/tmp/velocore-contracts/src/pools/constant-product/ConstantProductPool.sol