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.
| Field | Meaning | For example |
|---|---|---|
min_feeders | Share of registered feeders needed for a price, in permille. | 500 (50%) |
sample_period_secs | Length of one sample period, in seconds. | 50 |
samples_number | Number of sample periods in the validity window. | 12 (10 minutes) |
discount_factor | EMA 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:
- The position sends
add_price_alarmto the oracle after every re-check. - When the oracle price leaves the position's range, the alarm becomes due.
- The alarms dispatcher, an off-chain service, calls
dispatch_alarmson the oracle, which delivers due alarms to their positions. - 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:
| Query | Returns |
|---|---|
{"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.
NLS rewards
How the Nolus treasury distributes NLS rewards to lending pools by total pool value, how each lender's share accrues, and how to query and claim it.
Liquidations and risk
How Nolus protects positions and lenders with LTV thresholds, liquidation warnings, partial and full liquidation, slippage protection and the reserve.