Nolus Docs

Oracle

How Nolus price feeders quote Metis, how the oracle turns their feeds into prices with a quorum, sampling and an EMA, and how price alarms reach positions.

Every Nolus protocol has its own oracle contract, which is the single price source for that protocol. Positions use it to compute their loan-to-value (LTV), set their swap limits and decide when to liquidate, and integrators use it to read the same prices that the protocol acts on.

The oracle works in two parts. Price feeders, which are off-chain services, take prices from Metis quotes on Solana and push them to the oracle contract on Nolus. The contract accepts prices only from its registered feeders, requires a quorum, smooths the prices into an exponential moving average (EMA), and fires price alarms for positions.

Price feeders

Several independent price feeders serve each oracle, and each of them repeats the same cycle. A feeder reads the oracle's supported currency pairs and asks Metis, Jupiter's routing engine on Solana, for a quote on each pair. It refuses any quote with excessive price impact, so that a thin route cannot set the price, and sends the prices to the oracle in a feed_prices transaction.

A feeder does no averaging or weighting of its own, because all aggregation happens on chain, as the next section describes.

How the oracle computes a price

The oracle turns the raw feeds into one price per currency in three steps, which are a quorum check, sampling and an exponential moving average.

A price exists only if enough distinct feeders have fed it within the validity window, and the quorum is a share of the registered feeders. Without a quorum the oracle returns no price, and nothing that depends on it proceeds.

Once a quorum is in place, the oracle groups the feeds into sample periods and takes the average of the feeds in each period as that period's price. A price stays valid for a window of samples, and feeds older than the window are ignored.

Finally, the oracle smooths the samples with an exponential moving average, computing for each period price = discount x period average + (1 - discount) x previous price. The discount factor is the weight of the newest period while the history carries the rest, so a higher discount reacts faster and a lower one smooths more. The result is the price that every consumer reads, positions included.

Oracle configuration

The quorum, sampling and EMA settings are the fields under price_config in the oracle's configuration. The table gives example values, and the following query returns the live values:

nolusd q wasm contract-state smart <oracle> '{"config":{}}'

Replace <oracle> with the protocol's oracle address, which the admin contract returns as Architecture describes.

FieldMeaningFor example
min_feedersShare of registered feeders needed for a price, in permille.500 (50%)
sample_period_secsLength of one sample period, in seconds.50
samples_numberNumber of sample periods in the validity window.12 (10 minutes)
discount_factorEMA weight of the newest period, in permille.400 (40%)

Base currency and pairs

Each oracle prices every currency against its base currency, which is the protocol's pool currency (LPN). The oracle of SOLANA-METIS-USDC, for example, quotes every price in USDC. The supported currencies and the pairs that the feeders quote form a swap tree rooted at the base currency, and prices compose along the tree.

Price alarms and the alarms dispatcher

Rather than polling the oracle, each open position registers a price alarm with its protocol's oracle. The alarm sets a price below which the position needs to re-check itself, and optionally a price above which it does the same, with the range derived from the position's next LTV zone and its take profit and stop loss. A price alarm then runs through the following cycle:

  1. The position sends add_price_alarm to the oracle after every re-check.
  2. When the oracle price leaves the position's range, the alarm becomes due.
  3. The alarms dispatcher, an off-chain service, calls dispatch_alarms on the oracle, which delivers due alarms to their positions.
  4. The position reads the price, re-computes its LTV and acts on the result, which can be a warning, a liquidation, a take profit or stop loss close, or nothing at all. It then registers a new alarm.

Positions also register a time alarm with the platform's time alarms contract for a periodic re-check, for example every 5 days, and the dispatcher triggers those alarms in the same way. To see whether alarms are waiting to be dispatched, run the oracle's {"alarms_status":{}} query.

Query prices

Integrators read prices with the following queries, which all run against the protocol's oracle address:

QueryReturns
{"base_currency":{}}The ticker every price is quoted in.
{"prices":{}}The current price of every supported currency.
{"base_price":{"currency":"<ticker>"}}The current price of one currency.
{"currencies":{}}Every supported currency with its ticker, bank denom on Nolus, decimals and group.
{"supported_currency_pairs":{}}The pairs the feeders quote.
{"swap_tree":{}}The pairs as a tree rooted at the base currency.
{"config":{}}The quorum, sampling and EMA settings above.
{"feeders":{}}The registered feeder accounts.
{"alarms_status":{}}Whether price alarms are waiting to be dispatched.

A price comes back as two amounts, each in its currency's smallest unit, as this query for the price of NLS shows:

nolusd q wasm contract-state smart <oracle> '{"base_price":{"currency":"NLS"}}'

With <oracle> replaced by the oracle address, the query returns a response like this one:

{
  "data": {
    "amount": { "amount": "122070312500000", "ticker": "NLS" },
    "amount_quote": { "amount": "447463532743", "ticker": "USDC" }
  }
}

To get a human price, divide each amount by 10^decimals, with the decimals taken from the currencies query, and then take the ratio amount_quote / amount. Both currencies here use 6 decimals, so the price is 447463532743 / 122070312500000, about 0.0037 USDC per NLS. If a currency has no quorum, the query returns an error instead of a price.

On this page