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
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.
| Surface | Exposure | Control |
|---|---|---|
| Staleness rule | Global STALENESS of one hour against feeds with a 24-hour heartbeat | Per-feed maximum age set from the heartbeat |
| Fallback source | Spot reserves of one AMM pool (VF-02) | Fallback removed; borrowing against a stale price reverts |
| Sequencer restart | Prices accepted as soon as the sequencer is up (VF-05) | One-hour grace period after restart |
| Flash liquidity | Veyra’s own flash loans fund the price move | Unchanged; no longer reaches the oracle |
| Exchange-rate reads during callbacks | Depressed 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
- Attacker contract (adversary) to LendingPool: flashLoan(quote asset) (attack step)
- Attacker contract (adversary) to Fallback AMM pool: swap quote → collateral. spot price multiplied (attack step)
- Attacker contract (adversary) to LendingPool: deposit(collateral); borrow(max) (attack step)
- LendingPool (on-chain) to PriceOracle: getPrice(collateral). feed > 1 h old → spot fallback (normal step)
- Attacker contract (adversary) to Fallback AMM pool: swap back (attack step)
- Attacker contract (adversary) to LendingPool: repay flash loan + fee. bad debt remains (attack step)
| Spot price multiple | Input / quote reserve | Input (units) | Flash-loan fee (units) |
|---|---|---|---|
| 1.5× | 22.5% | 539,388 | 270 |
| 2× | 41.4% | 994,113 | 497 |
| 3× | 73.2% | 1,756,922 | 878 |
| 4× | 100.0% | 2,400,000 | 1,200 |
Timeline
Assessment begins
Detection
Fallback manipulation confirmed in a fork test
Containment
Borrowing against the two affected assets paused
Fix
Remediation commit removes the fallback
Verification
Fix verified against the fork test
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
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.
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.
| Component | Weight | Range | During case | Today | Score points |
|---|---|---|---|---|---|
| Code Security | 30% | 49.0 | 76.0 | +8.1 | |
| Operational Security | 20% | 78.0 | 78.0 | 0.0 | |
| Governance Risk | 15% | 80.0 | 80.0 | 0.0 | |
| Economic Security | 15% | 64.0 | 74.0 | +1.5 | |
| Infrastructure Resilience | 10% | 76.0 | 80.0 | +0.4 | |
| Transparency | 10% | 84.0 | 84.0 | 0.0 | |
| Quantix Risk Index | 100% | 67.9 | 77.9 | +10.0 |
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.
- 01Remove the AMM spot-price fallback
- 02Configure maximum price age per feed from its heartbeat
- 03Revert borrows against stale prices; keep deposits and repayments open
- 04Add a one-hour grace period after sequencer restart
- 05Keep the fork test as a regression test
-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
- 01
Review failure branches as carefully as the main path; attackers choose the branch.
- 02
Staleness thresholds belong to each feed, derived from its heartbeat.
- 03
A fallback price must be at least as hard to manipulate as the primary.
- 04
A protocol that offers flash loans must assume they will be used against its own oracle.

