Wrong or manipulated data

Download Markdown

A protocol can keep answering while the number it gives is wrong. A bug can double a reported value, an oracle (a service that publishes prices on chain) can go stale or return nonsense, or an attacker can push a market price for the length of one transaction. This page explains the four defences the vault uses and shows what each one does.

The examples use the example vault: Alice holds 4,000 USDC of Senior, June holds 1,000 USDC of Junior, and 3,000 USDC is invested in one leg (one investment position of the vault). Each share is worth 1 USDC at the start.

The four defences

DefenceWhat it stops
Report checksReports that are malformed, of the wrong version, or not from the leg's adapter
Blocking flagsReports the adapter itself does not trust
Growth capA value that rises faster than a set yearly rate
Value bandsLeavers paid too much, or entrants charged too little, when the value is uncertain

Table: each defence acts at a different point between the adapter's answer and the share price.

Defence 1: report checks

What. Every valuation comes back as a ValuationReport, a short fixed-format record (53 bytes) with three values (value_low, value_mid, value_high), the cash the leg could withdraw now (liquid_usdc) and flags. See Accounts and data layouts for the byte layout.

How. The program refuses a report unless:

  • it was set as return data by the leg's own adapter program, otherwise MissingReturnData;
  • it decodes, and its version is the adapter interface version the program expects, otherwise InvalidValuationReport;
  • value_low ≤ value_mid ≤ value_high, and liquid_usdc ≤ value_mid, otherwise InvalidValuationReport;
  • no blocking flag is set, otherwise AdapterFlagged.

Example. An adapter sends a report whose low value (3,000) is above its book value (2,900), then a report with a version number the program does not expect, then random bytes. Each is refused with InvalidValuationReport. A report flagged PRICE_UNCERTAIN is refused with AdapterFlagged. A report set by a different program, even an honest-looking one, is refused with MissingReturnData. Once the leg goes stale, deposits fail with StaleValuation, exactly like an outage.

The takeaway is that a bad report is treated like no report. It never reaches the share price.

Defence 2: the growth cap

What. A leg's recognised value may rise by at most the vault's growth cap per year, measured from the last recognised value. The cap is set by the curator in basis points (one basis point is 0.01%). The program's default is 3,000 basis points, which is 30% a year. The curator can set up to 60,000, which is 600% a year, or 0 to turn the cap off.

Why. A sudden jump in a reported value is more likely a bug or an attack than real income. Booking it at once would let someone who caused it withdraw the fake profit.

How. On each refresh, the cap is:

cap = max(last value, cost basis) + USDC put in − USDC taken out
      + max(last value, cost basis) × growth cap in basis points × seconds since last refresh ÷ (10,000 × seconds per year)

The recognised value is the lower of the reported value and the cap. A drop is never capped: it is recognised in full at once (An honest loss explains why).

Example. A leg of 3,000 USDC suddenly reports 6,000, and the vault uses the default cap of 30% a year.

  • At the same moment, nothing extra is recognised.
  • A day later, at most one day's share of 30% a year is recognised: 3,000 × 0.30 ÷ 365 ≈ 2.47 USDC.
  • Alice withdraws 1,000 Senior shares and is paid at the recognised value, not the reported 6,000.

Example. A leg that inflates slowly, below 30% a year, is recognised in full. The cap does not stop small, slow errors. The bands and the guardian are the defence against those.

Example. A leg reports 0 for one slot (one Solana block, about 400 milliseconds) and then 3,000 again. The drop is booked at once: Junior loses its 1,000, Senior loses the other 2,000, and the vault enters Recovery, the state in which new deposits and investments stop while an incident is handled. On the next slot, the value comes back up to the leg's cost basis (the USDC put in minus the USDC taken out, here 3,000) at once, because the cap starts from max(last value, cost basis). During Recovery the comeback counts as recovery, not profit: Senior is repaid, then Junior. The one-slot glitch moves no value between the tranches.

Defence 3: value bands

What. Each report has a low, middle and high value. The middle is the book value. The low value is what the leg could be worth if sold under stress, and the high value is an upper estimate.

Why. When a leg's value is uncertain, the vault should not pay leavers the optimistic number or charge entrants the pessimistic one. Using the low value for withdrawals and the high value for deposits protects the holders who stay.

How.

  • Withdrawals are priced with every leg at its low value. The shortfall from book value is taken from Junior first.
  • Deposits are priced with every leg at its high value. The excess over book value is split between the tranches like a gain.
  • The band keeps its width around the recognised value. If the growth cap holds the value down, the low and high values move down with it.

Example. A 3,000 USDC leg reports a band of 1,500 (low) / 3,000 (book) / 3,600 (high).

  • At the low value, the vault is 3,000 − 1,500 = 1,500 below book. Junior's 1,000 absorbs the first part, and Senior the remaining 500, so Senior is worth 4,000 − 500 = 3,500 at the low value. A Senior share therefore leaves at 3,500 ÷ 4,000 = 0.875 USDC, give or take a tiny rounding effect of the share math (see NAV and share pricing).
  • At the high value, the vault is 3,600 − 3,000 = 600 above book. Bob, a new investor who buys Junior with 100 USDC, receives fewer shares than at the book price.

Defence 4: adapters that distrust their own inputs

Adapters for priced assets check their oracles and markets, and raise a flag when something looks wrong.

SituationWhat the adapter doesEffect on the vault
The oracle stops updating: Chainlink's syrupUSDC rate, Pyth's USDG price or Hastra's posted stake priceMaple fails with an error; Hastra and the token legs raise PRICE_UNCERTAINThe leg goes stale and the vault stops until a fresh reading, or until the guardian suspends the leg.
The oracle returns nonsense: 20 times the rate, or a negative numberRefused by an error or a blocking flagNo value is booked from it.
Someone pushes a market pool's price to halfNo adapter values a leg from a pool, so the book value does not move. Where the pool is a market guard (token legs, Maple, and the ONyc token leg, which compares OnRe's NAV with the pool), MARKET_STRESS lowers only the band's low value to the market price.Leavers are paid at the lower market price, Junior first. Entrants and the book value are unaffected.

Table: manipulating a pool can at most lower what leavers receive, never raise the book value.

The takeaway from the last row is that a price manipulation cannot make anyone richer. The worst it can do is make withdrawals cautious while it lasts.

Advanced details

  • MARKET_STRESS is not a blocking flag. While it is set, the program refuses trades on the leg (LegStressed), and the band's low value is the market value.
  • A MARKET_STRESS guard reading that is stale or cannot be verified becomes PRICE_UNCERTAIN, which does block.
  • liquid_usdc must not exceed value_mid, so an adapter cannot claim more cash than the leg is worth.
  • The growth cap uses wall-clock time, not slots.

On this page