Nolus Docs

Spot Margin

How Nolus Spot Margin positions work on Solana, the contracts behind them, the position parameters, and how to find every contract address on chain.

Spot Margin lets a user open a leveraged position with a down payment and a loan from a Nolus lending pool. The position holds the real asset on the spot market rather than a derivative, and the rate on its loan stays fixed for the life of the position.

Every Nolus market runs on Solana today, so a position's assets live in an account on Solana and its swaps run through Metis. The loan, the accounting and the risk checks run in the protocol contracts on Nolus, which hold receipts for the tokens on Solana. The pages in this section give the contracts, the parameters and the exact messages that a developer needs to quote, open, query, repay and close a position.

Markets and protocols

Nolus runs one protocol per market, as the protocol overview describes, and each protocol is named SOLANA-METIS-<LPN>, where the LPN, the pool's native currency, is the currency that its lending pool lends.

Whether a market is long or short depends on what its pool lends. A long market borrows USDC from the SOLANA-METIS-USDC pool and buys the position asset, so the position gains when the asset rises against USDC. A short market borrows the shorted token from that token's own pool, for example SOLANA-METIS-SOL, and holds USDC, so the position gains when the shorted token falls against USDC. The app at app.nolus.io shows the current markets and assets.

Contracts behind a position

Each protocol has its own set of contracts, and the table below gives the role that each of them plays in a position:

ContractRole in a position
LeaserQuotes and opens positions, keeps the registry of each user's positions, holds the position parameters.
LeaseOne contract per position. A state machine that holds the loan, tracks interest and LTV, and drives the position's account on Solana.
LPPThe lending pool. Lends the LPN to new positions at a fixed rate and receives repayments.
OraclePrices and price alarms that trigger LTV re-checks.
Remote lease controllerSends the Lease's instructions to Solana over IBC.
ProfitCollects margin interest. Margin interest funds NLS buybacks and the Nolus referral program.
ReserveCovers shortfalls when a closed or liquidated position does not cover its debt.

The architecture page shows how these contracts fit together with the platform contracts and the off-chain services.

Loan-to-value and the position parameters

A position's health is measured by its loan-to-value, or LTV, which is the total amount due divided by the value of the position, with both sides in the pool currency. The amount due is the principal together with any due and overdue interest.

The Leaser holds the parameters that every new position is opened with. The table gives an example value for each parameter and the field of lease_config that holds it:

ParameterFor exampleField in lease_config
Opening LTV80% (a position up to 5x the down payment, so the loan is up to 4x)position_spec.liability.initial
Healthy LTV, the target after a partial liquidation83%position_spec.liability.healthy
Warning levels85%, 86.5% and 88%position_spec.liability.first_liq_warn, second_liq_warn, third_liq_warn
Liquidation LTV90%position_spec.liability.max
Periodic LTV re-check, plus price alarms in betweenevery 5 daysposition_spec.liability.recalc_time
Minimum position size15 USDCposition_spec.min_asset
Minimum transaction size0.5 USDCposition_spec.min_transaction
Interest due period30 daysdue_period
Margin interest, on top of the loan rate4.5% a yearinterest_rate_margin
Maximum slippage for open, repay and close7%max_slippages.open, repay, close
Maximum slippage for liquidation10%max_slippages.liquidation

Check the live values with the Leaser's config query, which uses the $LEASER variable set in Find the contract addresses:

nolusd q wasm contract-state smart $LEASER '{"config":{}}' -o json | jq .data.config.lease_config

The loan rate comes from the pool's utilization curve instead, at the moment the position opens, and then stays fixed, as The loan rate explains.

As LTV crosses each warning level, the Lease emits a wasm-ls-liquidation-warning event. When LTV reaches the liquidation level, the position is partially liquidated back to the healthy LTV, or fully liquidated when a partial liquidation would leave less than the minimum position. Liquidations and risk describes the risk framework in full.

Worked example

A short example shows how these levels play out as the price moves. Suppose a user opens a SOL long with a 100 USDC down payment and borrows 400 USDC, so that the position holds 500 USDC worth of SOL and opens at an LTV of 400 / 500 = 80%. Ignoring interest, a 10% fall in SOL leaves the position worth 450 USDC, which puts its LTV at 400 / 450 = 88.9%, past the third warning level. If SOL falls 12% instead, the position is worth 440 USDC and its LTV reaches 400 / 440 = 90.9%, past the liquidation LTV, so the Lease sells enough SOL to bring LTV back to 83%.

Position lifecycle

Every position moves through the states below, which are the states that the Lease reports to its {"state":{}} query:

Repaying, a partial close and a partial liquidation all leave a position in opened. Whatever a position owes the user when it ends is paid out automatically, so the user never has a claim step to take.

Find the contract addresses

Every contract address can be discovered from the platform admin contract. After you configure nolusd once, two queries to the admin contract list the protocol names and resolve the contracts of one protocol:

nolusd config set client chain-id pirin-1
nolusd config set client node https://rpc.nolus.network:443

ADMIN=nolus1gurgpv8savnfw66lckwzn4zk7fp394lpe667dhu7aw48u40lj6jsqxf8nd

# list protocol names
nolusd q wasm contract-state smart $ADMIN '{"protocols":{}}'

# resolve one protocol
nolusd q wasm contract-state smart $ADMIN '{"protocol":"SOLANA-METIS-USDC"}' -o json

ADMIN holds the address of the admin contract, and the second query takes one of the protocol names that the first query returns. Its answer gives the network, the dex and the addresses of the protocol's contracts:

{
  "data": {
    "network": "Solana",
    "dex": "Metis",
    "contracts": {
      "leaser": "nolus1...",
      "lpp": "nolus1...",
      "oracle": "nolus1...",
      "profit": "nolus1...",
      "remote_lease": "nolus1...",
      "reserve": "nolus1..."
    }
  }
}

Each nolus1... value stands for a full contract address. The commands on the following pages read the Leaser, pool and oracle addresses from shell variables, which you can set from the same query:

P=$(nolusd q wasm contract-state smart $ADMIN '{"protocol":"SOLANA-METIS-USDC"}' -o json)
LEASER=$(echo "$P" | jq -r .data.contracts.leaser)
LPP=$(echo "$P" | jq -r .data.contracts.lpp)
ORACLE=$(echo "$P" | jq -r .data.contracts.oracle)

The messages on those pages follow the same conventions for values on the wire. Amounts are integers in the currency's smallest unit, rates and LTV levels are in permille, where 800 stands for 80%, and durations are in nanoseconds. Funds attached to a transaction use the currency's bank denom, which the oracle's {"currencies":{}} query maps from each ticker as bank_symbol, together with its decimal_digits. To print a template for every query and execute message of every Nolus contract, run nolusd contract-help.

Walk through the flow

The pages that follow take a position through its life, from the quote to the close, with the messages for each step.

On this page