Skip to content
Quantix Security Labs

Oracle integrity on an L2 money market

Every path a price takes into Veyra Finance: a staleness rule that did not match the feed, a spot-price fallback that the protocol’s own flash loans could move, and the minutes after a sequencer restart.
HighOracle manipulationResearch assessment · 9 min read
Illustration: an oracle tower and a spot-price pool both feeding a money market, the pool spiked by a flash loan, and a sequencer clock beside them.

The case at a glance

Veyra Finance

Base · LendingSecurity profileResearch case · Quantix Protocol Research

During the case
B67.9
Current profile
BBB77.9
To containment
3 h 50 min
To verified fix
27.1 days
  1. 03Problem

    The question was narrow: can anyone make Veyra price collateral at a value the market would not trade at, for long enough to borrow against it or liquidate others?

    Read section
  2. 06Vulnerability

    HighOracle manipulation

    The flaw is a combination. A staleness rule that did not match the feed’s heartbeat made the fallback active most of the day, and the fallback read the one source in the system that is cheapest to move within a transaction.

    Read section
  3. 08Remediation

    1. 01Remove the AMM spot-price fallback
    2. 02Configure maximum price age per feed from its heartbeat
    3. 03Revert borrows against stale prices; keep deposits and repayments open

    and 2 more steps

    Read section
  4. 09Outcome

    VF-02 and VF-05 were fixed and verified in the remediation review. The fork test that demonstrated the manipulation now reverts at the oracle, and a stale feed pauses borrowing only against the affected asset.

    Risk Index B → BBB, +10.0 points

    Read section

Background

Veyra Finance is a pooled money market on Base. Collateral is valued by a PriceOracle that reads an aggregated push feed per asset, checks the L2 sequencer-uptime feed, and, at the reviewed commit, fell back to the spot price of a configured AMM pool whenever the primary feed looked stale.

Veyra also offers flash loans from its own pools. That makes the oracle’s fallback part of its economic security: anything the oracle reads must be expensive to move within a single transaction.

Architecture

LendingPool computes borrowing power from prices returned by PriceOracle; Liquidator uses the same prices for health checks. Flash loans are served by LendingPool and repaid in the same transaction.

The assessment traced each price from source to use: primary feed, staleness rule, sequencer check, fallback, and how each failure reaches the pool.

Figure 1Veyra Finance: components and paths involved in this case

Veyra Finance: architecture relevant to this case8 elements and 8 connections. Highlighted components are directly involved in the case; dashed red arrows are attack paths, dotted brass arrows are control paths.latestRoundData()statusgetReserves()getPrice()health factormint / burnflashLoan()swapPrimary price feedpush feed, 24 h heartbeatSequencer uptime feedL2 status, startedAtFallback AMM poolspot reservesPriceOracleAggregated price feeds withsequencer-uptime checkLendingPoolDeposits, borrows,repayments and flash loansLiquidatorLiquidation auctions forunhealthy positionsvTokenInterest-bearing depositreceiptsFlash borrowerborrows, swaps, borrows
Protocol componentInvolved in this caseExternal partyAttack pathControl path

Elements:

  • Primary price feed (external party): push feed, 24 h heartbeat
  • Sequencer uptime feed (external party): L2 status, startedAt
  • Fallback AMM pool (external party): spot reserves
  • PriceOracle (protocol component, involved in this case): Aggregated price feeds with sequencer-uptime check
  • LendingPool (protocol component, involved in this case): Deposits, borrows, repayments and flash loans
  • Liquidator (protocol component): Liquidation auctions for unhealthy positions
  • vToken (protocol component): Interest-bearing deposit receipts
  • Flash borrower (adversary): borrows, swaps, borrows

Connections:

  • Primary price feed to PriceOracle: latestRoundData() (data or call flow)
  • Sequencer uptime feed to PriceOracle: status (data or call flow)
  • Fallback AMM pool to PriceOracle: getReserves() (data or call flow)
  • PriceOracle to LendingPool: getPrice() (data or call flow)
  • LendingPool to Liquidator: health factor (data or call flow)
  • LendingPool to vToken: mint / burn (data or call flow)
  • Flash borrower to LendingPool: flashLoan() (attack path)
  • Flash borrower to Fallback AMM pool: swap (attack path)

Components in the Veyra Finance profile

  • LendingPool(involved in this case)

    Deposits, borrows, repayments and flash loans

  • vToken

    Interest-bearing deposit receipts

  • PriceOracle(involved in this case)

    Aggregated price feeds with sequencer-uptime check

  • Liquidator

    Liquidation auctions for unhealthy positions

  • Treasury

    Reserve factor accrual and withdrawals

Security challenge

The question was narrow: can anyone make Veyra price collateral at a value the market would not trade at, for long enough to borrow against it or liquidate others?

Answering it meant modelling the failure branches, not just the main path: a feed that looks stale, a sequencer outage and restart, and the fallback that activates in those moments.

Attack surface

Each price input was assessed for who can move it, how quickly, and at what cost.

Attack surface examined in Veyra Finance
SurfaceExposureControl
Staleness ruleGlobal STALENESS of one hour against feeds with a 24-hour heartbeatPer-feed maximum age set from the heartbeat
Fallback sourceSpot reserves of one AMM pool (VF-02)Fallback removed; borrowing against a stale price reverts
Sequencer restartPrices accepted as soon as the sequencer is up (VF-05)One-hour grace period after restart
Flash liquidityVeyra’s own flash loans fund the price moveUnchanged; no longer reaches the oracle
Exchange-rate reads during callbacksDepressed vToken rate visible to other contracts (VF-01)Reentrancy-aware view functions

Technical investigation

The primary path was sound. The weakness was in the branch: when block.timestamp − updatedAt exceeded a global constant of 3,600 seconds, getPrice returned _fallbackPrice, computed from the current reserves of an AMM pool.

Two listed collateral assets had primary feeds with a 24-hour heartbeat. Those feeds update only on deviation or once a day, so they were older than an hour for most of every day while still correct — and for most of every day those assets were priced from spot reserves.

A constant-product pool’s spot price can be moved by trading against it. Multiplying the base asset’s price by k takes x × (√k − 1) of the quote-side reserve x. The table applies that formula to the reference pool depth used in the review; Veyra’s own flash loans could supply the input.

A fork test confirmed the path end to end: in one transaction the attacker borrowed from Veyra, bought the collateral in the fallback pool, deposited a small amount of it, borrowed against the inflated value, sold back and repaid. The position was left with bad debt. The separate sequencer finding (VF-05) was confirmed by replaying a forty-minute outage in which collateral fell 9%.

Figure 2Fallback manipulation funded by a Veyra flash loan

Fallback manipulation funded by a Veyra flash loanAttacker contractLendingPoolFallback AMM poolPriceOracle1. flashLoan(quote asset)2. swap quote → collateralspot price multiplied3. deposit(collateral); borrow(max)4. getPrice(collateral)feed > 1 h old → spot fallback5. swap back6. repay flash loan + feebad debt remains
Ordinary stepAttack step
  1. Attacker contract (adversary) to LendingPool: flashLoan(quote asset) (attack step)
  2. Attacker contract (adversary) to Fallback AMM pool: swap quote → collateral. spot price multiplied (attack step)
  3. Attacker contract (adversary) to LendingPool: deposit(collateral); borrow(max) (attack step)
  4. LendingPool (on-chain) to PriceOracle: getPrice(collateral). feed > 1 h old → spot fallback (normal step)
  5. Attacker contract (adversary) to Fallback AMM pool: swap back (attack step)
  6. Attacker contract (adversary) to LendingPool: repay flash loan + fee. bad debt remains (attack step)
Quote asset needed to multiply the fallback pool’s spot price, for a reference pool holding 2,400,000 units of the quote asset. Constant-product formula, swap fees ignored; flash-loan fee of 5 bps.
Spot price multipleInput / quote reserveInput (units)Flash-loan fee (units)
1.5×22.5%539,388270
2×41.4%994,113497
3×73.2%1,756,922878
4×100.0%2,400,0001,200

Timeline

  1. Assessment begins

  2. Detection

    Fallback manipulation confirmed in a fork test

  3. Containment

    Borrowing against the two affected assets paused

  4. Fix

    Remediation commit removes the fallback

  5. Verification

    Fix verified against the fork test

  6. Assessment report published

Root cause

The flaw is a combination. A staleness rule that did not match the feed’s heartbeat made the fallback active most of the day, and the fallback read the one source in the system that is cheapest to move within a transaction. Either alone would have been a configuration issue; together they were a funded manipulation path.

Class
Oracle manipulation
Severity
High
Components involved
PriceOracleLendingPool
Findings
VF-02VF-05
src/oracle/PriceOracle.sol (reviewed commit)solidity
uint256 public constant STALENESS = 1 hours;   // feeds with a 24 h heartbeat exceed this daily function getPrice(address asset) public view returns (uint256) {    (, int256 answer,, uint256 updatedAt,) = feeds[asset].latestRoundData();    if (answer <= 0) revert InvalidPrice(asset);    if (block.timestamp - updatedAt > STALENESS) return _fallbackPrice(asset);    return _scale(uint256(answer), feeds[asset].decimals());} function _fallbackPrice(address asset) internal view returns (uint256) {    (uint112 r0, uint112 r1,) = fallbackPools[asset].getReserves();   // spot reserves    return uint256(r1) * 1e18 / r0;}

Risk evaluation

All findings were open at the start of the assessment, including the two high-severity issues. Economic Security is reduced while a funded manipulation path existed, and Infrastructure Resilience while sequencer restarts were unhandled. The after state is Veyra’s current profile.

67.9BElevated risk
During the caseB67.9
77.9BBBModerate risk
Current profileBBB77.9
CodeSecurityOperationalSecurityGovernanceRiskEconomicSecurityInfrastructureResilienceTransparencyDuring the case, Code Security: 49During the case, Operational Security: 78During the case, Governance Risk: 80During the case, Economic Security: 64During the case, Infrastructure Resilience: 76During the case, Transparency: 84Current profile, Code Security: 76Current profile, Operational Security: 78Current profile, Governance Risk: 80Current profile, Economic Security: 74Current profile, Infrastructure Resilience: 80Current profile, Transparency: 84
During the caseCurrent profile

The score moves from 67.9 (B) to 77.9 (BBB), a change of +10.0 points. The largest single contribution is Code Security: restoring it from 49.0 to 76.0 alone moves the score from 67.9 to 76.0.

Basis for the case state: Spot fallback active for two assets; no sequencer grace period; all findings open.
ComponentWeightRangeDuring caseTodayScore points
Code Security30%49.076.0+8.1
Operational Security20%78.078.00.0
Governance Risk15%80.080.00.0
Economic Security15%64.074.0+1.5
Infrastructure Resilience10%76.080.0+0.4
Transparency10%84.084.00.0
Quantix Risk Index100%67.977.9+10.0
During the caseCurrent profileBasis for the case state: Spot fallback active for two assets; no sequencer grace period; all findings open.

Scores use the six weighted components of the Quantix Risk Index. Open findings reduce Code Security by a fixed penalty per severity; the current profile is the protocol’s entry in Quantix Intelligence.

Mitigation strategy

The spot fallback was removed. Each feed now has its own maximum age, set slightly above its documented heartbeat, and a stale price makes borrowing against that asset revert instead of substituting another source. Deposits and repayments remain possible.

After the sequencer reports up, prices are rejected for a one-hour grace period measured from the feed’s startedAt, so liquidations cannot land before borrowers can react.

  1. 01Remove the AMM spot-price fallback
  2. 02Configure maximum price age per feed from its heartbeat
  3. 03Revert borrows against stale prices; keep deposits and repayments open
  4. 04Add a one-hour grace period after sequencer restart
  5. 05Keep the fork test as a regression test
src/oracle/PriceOracle.sol (remediation commit)diff
-uint256 public constant STALENESS = 1 hours;+mapping(address => uint256) public maxStaleness; // per feed: heartbeat + buffer  function getPrice(address asset) public view returns (uint256) {+    _checkSequencer();     (, int256 answer,, uint256 updatedAt,) = feeds[asset].latestRoundData();     if (answer <= 0) revert InvalidPrice(asset);-    if (block.timestamp - updatedAt > STALENESS) return _fallbackPrice(asset);+    if (block.timestamp - updatedAt > maxStaleness[asset]) revert StalePrice(asset);     return _scale(uint256(answer), feeds[asset].decimals()); }  function _checkSequencer() internal view {-    (, int256 answer,,,) = sequencerFeed.latestRoundData();+    (, int256 answer, uint256 startedAt,,) = sequencerFeed.latestRoundData();     if (answer != 0) revert SequencerDown();+    if (block.timestamp - startedAt <= GRACE_PERIOD) revert GracePeriodNotOver(); }

Security outcome

VF-02 and VF-05 were fixed and verified in the remediation review. The fork test that demonstrated the manipulation now reverts at the oracle, and a stale feed pauses borrowing only against the affected asset.

Veyra accepted a deliberate trade-off: during a feed outage, borrowing against that collateral is unavailable rather than priced from a weaker source.

Detection to containment
3 h 50 min
Detection to first fix
19.9 days
Detection to verified fix
27.1 days
Risk Index change
B → BBB (+10.0)

Lessons learned

  1. 01

    Review failure branches as carefully as the main path; attackers choose the branch.

  2. 02

    Staleness thresholds belong to each feed, derived from its heartbeat.

  3. 03

    A fallback price must be at least as hard to manipulate as the primary.

  4. 04

    A protocol that offers flash loans must assume they will be used against its own oracle.

Have a problem like this one?

Tell us what you are building and where it runs. We will reply with the review plan we would use for your architecture.