Spot-price oracle manipulation in staking vaults

How a reserve spot price becomes a staking input

A reserve-based oracle reads the current AMM balance and treats it as a price. In a Uniswap V2-style pair, that is just a snapshot of reserves at the moment the contract asks for them. If an attacker can move those reserves in the same transaction, the price moves with them.

That is the weak point in spot-price oracle manipulation. A vault that sizes fixed-USD deposits from raw reserves is not pricing against a market view, it is pricing against whatever happens to be in the pool right then. Flash loans make that cheap. Temporary liquidity and a large swap can shove the reserve ratio far enough to make the required token deposit look far smaller than it should be.

Where stake sizing is fixed in USD, the vault turns that distorted price into a deposit amount. A $25,000 package should demand a sensible amount of token collateral. With manipulated reserves, it can be cut to a fraction of that, while still being recorded as the full USD value.

Why same-block reserves are easy to skew

AMM reserves are easy to bend because the oracle reads them before the transaction ends. There is no delay, no time-weighted average, and no check that liquidity stayed healthy long enough to matter. An attacker only needs the distorted state to exist long enough for the contract call.

Flash loans fit this pattern neatly. Borrow, push the pair price, call the vault, then unwind the trade and repay the loan. The pool may be back to normal by the time anyone looks at it. The vault has already trusted the wrong number.

Where fixed-USD stake sizing turns on that number

If the stake path converts a USD package value into token input by calling the live spot price, the manipulation lands straight on the deposit amount. That is where deposit inflation starts. The attacker deposits too little token value and receives full credit for the package size.

A guard such as msg.sender == tx.origin does little here. It blocks some contract-mediated calls, but not a direct user transaction funded by borrowed liquidity. The weak point is the price source, not the caller type.

How the vault turns a short-lived price move into lasting damage

The damage survives because the vault stores the inflated USD principal after the market has moved back. The oracle only needed to be wrong for one call. The accounting stays wrong until the stake is settled.

That is the ugly part of deposit inflation. The manipulated price does not need to remain in place. It only needs to be present when the contract writes userStakedUsd, totalActiveStakedUsd, or whatever internal balance tracks the package value. Once that state is stored, later reward logic treats it as real.

The deposit path that stores inflated principal

A stake entry that persists packageUsd or a derived USD amount from manipulated reserves creates a durable false record. The attacker can repeat the same package many times while the reserve ratio stays distorted. Each entry records a larger principal than the token deposit actually justified.

In the incident pattern behind this class of exploit, one package size was repeated one hundred times. The vault recorded millions of dollars of staking principal from a much smaller token outlay. That is the point where a price manipulation bug stops looking like a pricing quirk and starts behaving like a balance sheet bug.

The reward path that pays out after the market has moved back

Reward settlement often looks up the live price again. If the vault later converts USD-denominated rewards back into the token using a fresh spot reserve, it can pay out far more token than the original deposit would support. The vault then pays against an inflated principal that no longer matches the market, and the attacker pockets the difference.

This is where the mismatch bites: the deposit was priced with one bad reserve snapshot, while the reward is priced with a later, normal snapshot. The contract has no clean way to reconcile the two because it already trusted the wrong input at entry.

What to harden in the contract and the control plane

Raw reserves should not drive staking maths. A delayed price source, a TWAP, or a sanity-checked oracle is the minimum boundary. If the contract must accept a spot-derived value, add liquidity checks, slippage bounds, and a fail-closed path when the pair moves too sharply in one block.

The control plane matters as well. Alert on sharp reserve shifts before stake entry, and treat repeated deposits from the same address under abnormal pool conditions as hostile. If a package can be repeated many times, the repetition itself is a signal.

Replace raw reserves with delayed or sanity-checked pricing

A reserve-based oracle needs a second opinion. TWAPs do not stop every manipulation, but they force the attacker to hold the distortion over time instead of one call. That raises cost and makes flash-loan abuse much harder to execute cleanly.

If delayed pricing is not practical, sanity checks can still blunt the attack. Reject price moves that exceed a tight threshold, reject low-liquidity pairs, and reject entries when the reserve ratio has changed too fast for the observed pool depth. A bad quote should stop the stake, not pass through as if nothing happened.

Test the dump path, not just the deposit maths

Many vault tests stop at the happy path. That misses the useful failure. The contract can calculate the right amount for a normal deposit and still be exploitable if the same maths runs after a manipulated swap.

Test the price source under a forced reserve skew, then repeat the stake call in the same block. Test reward conversion after the market has normalised. Test repeated package creation, entry persistence, and later claim conversion as a single flow. If the dump path turns a temporary reserve move into lasting principal inflation, the maths is only half right.

Related posts

Immich | v3.1.0

Immich v3 1 0: web and mobile UX and accessibility improvements, workflow EXIF and path filters, auth and admin security updates, CLI and bug fixes

headscale | v0.29.3

headscale v0 29 3: fixes tagging and re registration, stabilizes ephemeral reconnects, adds registration security checks, upgrade notes, min TS client v1 80 0

Immich | v3.1.0

Immich v3 1 0: web UX and accessibility, path and EXIF workflow filters, OAuth admin security, mobile updates and iOS14 drop, bug fixes and contributors