# Glossary (/docs/glossary)
| Term | Meaning |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Lease** | A position on Nolus. Implemented as a per-position CosmWasm contract. |
| **Lease asset** | The asset purchased at open with down payment + LPP loan and held until close. The position holds this asset for its lifetime - it's what the borrower has long or short exposure to. |
| **Down payment** | The borrower's own collateral contribution to a position. Provided in any supported currency; swapped together with the borrowed amount into the lease asset at open. |
| **LPP** | Liquidity Provider Pool - the single-sided pool from which leases borrow. |
| **nLPP** | The receipt token minted to LPP depositors. Accrues value as the pool earns interest. |
| **Margin interest** | Interest accrued by an active lease against the LPP loan. |
| **Feeder** | A whitelisted off-chain operator that submits signed spot prices to the on-chain Oracle. |
| **EMA** | Exponential Moving Average - the price form the on-chain Oracle exposes to consumers. |
| **ICA** | Interchain Account - Nolus uses ICA on counterparty DEXs to execute swaps for leases. |
| **Profit** | The contract that routes protocol revenue. |
| **Reserve** | The protocol-owned backstop pool that covers shortfalls when a lease closes underwater. |
| **NLS** | The native staking and governance token of the Nolus chain. |
# Welcome (/docs)
**Nolus** is a spot margin protocol live on **Solana** - leveraged
positions at a fixed rate, backed by the asset itself. Solray, the
protocol's native Solana program, executes every position on Solana, with
swaps routed through the Metis aggregator; users connect with Phantom or
Solflare. This site is the developer reference for everything in and
around the protocol.
## Where to start [#where-to-start]
# Public APIs (/docs/public-apis)
Beyond the chain itself, several services expose public APIs that
applications integrate with.
| Service | Surface | Repo |
| -------------- | ----------------------------------------- | ------------------------------------------------------------ |
| Tendermint RPC | Block/tx queries, websocket subscriptions | [`nolus-core`](https://github.com/nolus-protocol/nolus-core) |
| Cosmos SDK LCD | REST gateway over chain queries | [`nolus-core`](https://github.com/nolus-protocol/nolus-core) |
| gRPC | Typed Cosmos SDK + CosmWasm queries | [`nolus-core`](https://github.com/nolus-protocol/nolus-core) |
| NLS Supply API | NLS supply data over HTTP | [`nls-supply`](https://github.com/nolus-protocol/nls-supply) |
Stub page - endpoint URLs, OpenAPI references, and rate-limit notes will be
added as services stabilize their public contracts.
# Overview (/docs/agentic-workflows)
Nolus exposes three machine-callable surfaces. They're complementary rather
than alternatives, and you pick the one that matches where your agent runs
and what it needs to do.
| Surface | Where it runs | Scope |
| ------------------------------------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| [stdio MCP server](/docs/agentic-workflows/mcp-server) | Local on the user's machine, via Cursor / Claude Desktop / any MCP client | Full read surface + transaction builders (open lease, repay, deposit LPP, withdraw, stake, vote) |
| [In-page WebMCP](/docs/agentic-workflows/webmcp) | Inside the `app.nolus.io` browser tab | Wallet-scoped reads + connect + navigate. No signing |
| REST + WebSocket API | Anywhere that speaks HTTP | General-purpose queries, pre-built unsigned tx payloads |
## Picking a surface [#picking-a-surface]
* *The agent runs on the user's desktop* (Cursor, Claude Desktop) and you
want trading actions: use the **stdio MCP** in `@nolus/nolusjs`. It builds
unsigned transactions; the user's wallet signs them.
* *The agent lives in the browser tab* alongside the connected wallet, and
you want it to inspect the live session or drive the UI: use the
in-page **WebMCP**. Tools auto-bind to the connected wallet - no address
argument needed. WebMCP intentionally does not sign; tx flows hand off to
the UI or the stdio MCP.
* *The agent runs anywhere and just needs HTTP*: hit the **REST API** at
`https://app.nolus.io/api/`. The OpenAPI 3.1 spec lives at
[`/api/openapi.json`](https://app.nolus.io/api/openapi.json) with 85
documented paths. The agent-card at
[`/.well-known/agent-card.json`](https://app.nolus.io/.well-known/agent-card.json)
describes the surface for agent autodiscovery.
## Signing boundary [#signing-boundary]
Across all three surfaces, the protocol's signing boundary is the same:
**Nolus never holds or signs with the user's private key.** Transaction
tools return unsigned payloads; the wallet popup (Keplr, Phantom, Solflare,
Ledger) is the single consent surface. WebMCP enforces this by design - it
deliberately exposes no signing tool. The stdio MCP returns CLI-ready
unsigned transactions that the user runs through `nolusd` or their wallet.
# MCP server (/docs/agentic-workflows/mcp-server)
# MCP Server [#mcp-server]
The Nolus MCP server exposes the `@nolus/nolusjs` library as **tools that AI assistants can use** to interact with the Nolus Protocol - through Cursor, Claude Desktop, or any client that speaks the [Model Context Protocol](https://modelcontextprotocol.io/).
The server runs locally on your machine and bridges the assistant to the chain via the SDK.
## Tools [#tools]
### Query tools (read-only) [#query-tools-read-only]
Fetch state from the chain. Safe to call without any user confirmation.
| Tool | What it returns |
| ---------------------- | ------------------------------------------------ |
| `get_protocols` | List of all registered protocols. |
| `get_protocol` | Protocol details and contract addresses. |
| `get_platform` | Platform-level contracts (treasury, timealarms). |
| `get_lease_quote` | Quote for opening a leveraged position. |
| `get_open_leases` | All active leases for a wallet. |
| `get_leaser_config` | Current Leaser configuration. |
| `get_lease_status` | Status of a specific lease. |
| `get_lpp_balance` | Liquidity pool balance. |
| `get_lpp_config` | Pool configuration. |
| `get_lpp_price` | nLPN receipt token price. |
| `get_lender_deposit` | Lender's deposit balance. |
| `get_lender_rewards` | Lender's pending rewards. |
| `get_deposit_capacity` | Remaining deposit capacity. |
| `get_lpn` | Pool's native asset ticker. |
| `get_oracle_prices` | All asset prices. |
| `get_asset_price` | Price of a specific asset. |
| `get_currencies` | All supported currencies. |
| `get_oracle_config` | Oracle configuration. |
| `calculate_rewards` | NLS rewards distribution. |
| `get_wallet_balance` | Token balance for an address. |
| `get_block_height` | Current block height. |
| `get_chain_id` | Chain ID. |
### Prepare tools (build unsigned transactions) [#prepare-tools-build-unsigned-transactions]
These tools **do not sign or broadcast**. They return a transaction message and a ready-to-use `nolusd tx ...` CLI command, which the user reviews and runs themselves.
| Tool | Builds |
| ----------------------------- | ------------------------------- |
| `prepare_open_lease` | Lease opening transaction. |
| `prepare_repay_lease` | Lease repayment transaction. |
| `prepare_close_lease` | Lease close (full or partial). |
| `prepare_change_close_policy` | Stop-loss / take-profit update. |
| `prepare_deposit_lpp` | Deposit into liquidity pool. |
| `prepare_withdraw_lpp` | Withdrawal from liquidity pool. |
| `prepare_claim_lpp_rewards` | NLS rewards claim. |
| `prepare_transfer_tokens` | Bank send transaction. |
## Network configuration [#network-configuration]
The server ships with built-in configs for both networks:
* **Mainnet (Pirin)** - `network: "pirin"` (default).
* **Testnet (Rila)** - `network: "rila"`.
Most tools accept an optional `network` parameter so you can target a specific chain per request. Omit it to use Pirin.
You can override the defaults via environment variables:
| Variable | Description | Network |
| --------------------------- | ----------------------------------------------- | ------- |
| `NOLUS_PIRIN_RPC_URL` | Pirin RPC endpoint | mainnet |
| `NOLUS_PIRIN_ADMIN_ADDRESS` | Pirin Admin contract address | mainnet |
| `NOLUS_PIRIN_CHAIN_ID` | Pirin chain ID (used in generated CLI commands) | mainnet |
| `NOLUS_RILA_RPC_URL` | Rila RPC endpoint | testnet |
| `NOLUS_RILA_ADMIN_ADDRESS` | Rila Admin contract address | testnet |
| `NOLUS_RILA_CHAIN_ID` | Rila chain ID | testnet |
## Connecting from Cursor or Claude Desktop [#connecting-from-cursor-or-claude-desktop]
Add the server to your MCP client's configuration (adjust the absolute path to the cloned `nolus.js` repo):
```json
{
"mcpServers": {
"nolus": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/nolus.js/src/mcp/server.ts"],
"env": {}
}
}
}
```
Restart your editor or Claude Desktop. The server will appear under MCP settings, and the assistant will surface the query and prepare tools listed above.
## Running it manually [#running-it-manually]
```sh
npm run mcp
```
To override the built-in network settings, export the env vars before running:
```sh
export NOLUS_PIRIN_RPC_URL=https://rpc.nolus.network
export NOLUS_PIRIN_ADMIN_ADDRESS=nolus1gurgpv8savnfw66lckwzn4zk7fp394lpe667dhu7aw48u40lj6jsqxf8nd
export NOLUS_PIRIN_CHAIN_ID=pirin-1
npm run mcp
```
## Reference prompt for AI agents [#reference-prompt-for-ai-agents]
Use this prompt as a system instruction for an agent that needs to interact with Nolus end-to-end (set up the server, install `nolusd`, configure the chain client):
```text
You are a Nolus Protocol agent. Before doing anything else, ensure the
environment is ready:
## Step 1: Set up the Nolus MCP server
- Clone the nolusjs library: `git clone https://github.com/nolus-protocol/nolus.js`
- Install dependencies: `cd nolus.js && npm install`
- Register the following MCP server in your client's configuration:
- Command: `npx tsx /src/mcp/server.ts`
- Environment variables:
- NOLUS_PIRIN_RPC_URL=https://rpc.nolus.network
- NOLUS_PIRIN_ADMIN_ADDRESS=nolus1gurgpv8savnfw66lckwzn4zk7fp394lpe667dhu7aw48u40lj6jsqxf8nd
- NOLUS_PIRIN_CHAIN_ID=pirin-1
- Reload your MCP tools so the server is available
## Step 2: Install nolusd if not already available
- Detect OS and architecture
- Download the appropriate binary from the latest release at
https://github.com/nolus-protocol/nolus-core/releases/latest
- Install it to a directory on your PATH and make it executable
- Verify with `nolusd version`
## Step 3: Configure nolusd
- `nolusd config set client chain-id pirin-1`
- `nolusd config set client node https://rpc.nolus.network:443`
Confirm setup is complete and list any existing keys with `nolusd keys list`.
You are now ready to interact with Nolus Protocol.
```
# In-page WebMCP (/docs/agentic-workflows/webmcp)
The Nolus webapp ships with a [WebMCP](https://webmachinelearning.github.io/webmcp/)
integration. Tools are registered via `navigator.modelContext.registerTool`
inside the `app.nolus.io` browser tab, so an agent running alongside the
page (a browser extension, an embedded model, Chrome's WebMCP EPP) can
inspect the user's live session and drive the UI without going through the
HTTP API.
## When to use it [#when-to-use-it]
Reach for WebMCP when the agent runs **in the same browser tab** as the
dApp and the user wants it to:
* See what wallet is connected, what balances and positions that wallet
holds, and what oracle is reporting right now.
* Trigger the wallet-connect flow when no wallet is active.
* Navigate the user to a specific page on the dApp (lease, earn, staking,
governance).
For everything else, fall back to the [stdio MCP](/docs/agentic-workflows/mcp-server)
(desktop agents, full trading instrument) or the REST API (general-purpose
HTTP).
## Scope boundary [#scope-boundary]
WebMCP intentionally does **not** expose any transaction-signing tool. No
open-lease, no deposit, no stake, no vote. Signing stays inside the
explicit user-driven UI flow with the wallet popup as the single consent
surface. If an agent needs to act on the user's behalf, it hands the
intent off to the dApp UI or to the stdio MCP - it never bypasses the
wallet popup.
## Tools [#tools]
All tools are wallet-scoped: when a wallet is connected, they auto-bind to
it, so there's no address parameter to pass.
| Tool | What it returns / does |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `get_connected_wallet` | The currently connected Nolus wallet (bech32 address) and connection status. |
| `get_balances` | The connected wallet's token balances across all supported assets. |
| `get_open_leases` | The connected wallet's open Nolus leases (margin positions) with full lifecycle detail. |
| `get_earn_positions` | The connected wallet's positions in the Nolus earn pools, plus the pool-level state. |
| `get_staking_delegations` | NLS staking delegations, unbonding entries, and claimable rewards. |
| `get_prices` | Current oracle prices for all supported assets, keyed by `ticker@protocol`. |
| `connect_wallet` | Opens the user's wallet extension to connect. Optional `mechanism` argument picks the extension (keplr, phantom, solflare, ledger). |
| `navigate` | Routes the user to one of the dApp's main pages by route name. |
The implementation lives in
[`src/common/webmcp/`](https://github.com/nolus-protocol/webapp/tree/main/src/common/webmcp)
in the webapp repo. Each tool is a thin adapter over the dApp's Pinia
stores, so calls cost no extra backend hits.
## Detecting WebMCP from an agent [#detecting-webmcp-from-an-agent]
The dApp gates tool registration behind a feature detection of
`navigator.modelContext`. A browser without WebMCP support simply doesn't
register the tools. From the agent side:
1. Open `https://app.nolus.io/` in the tab where the agent runs.
2. After page load, the dApp registers the eight tools above through
`navigator.modelContext.registerTool`.
3. The agent enumerates the registered tool surface and calls into it the
same way it would for any other WebMCP-enabled page.
The dApp also publishes its agent card at
[`/.well-known/agent-card.json`](https://app.nolus.io/.well-known/agent-card.json)
and an agent-skills index at
[`/.well-known/agent-skills/index.json`](https://app.nolus.io/.well-known/agent-skills/index.json)
for autodiscovery alongside WebMCP.
# Overview (/docs/etl/overview)
The Nolus ETL (extract-transform-load) is a Rust service that
ingests Nolus chain state in real time, transforms it into normalized
records, and serves a read-only HTTP API. The webapp and other Nolus
services consume it to surface indexed protocol data - prices,
positions, lender state, treasury flows - without each having to run
its own node + indexer.
## What's in there [#whats-in-there]
The ETL aggregates blockchain events, smart-contract state, and
oracle prices into a PostgreSQL store and exposes the result as 70+
REST endpoints. Below is a tour of the data surface grouped by
concern.
### Configuration [#configuration]
Which protocols and currencies the chain currently understands.
| Path | What it returns |
| ------------------------------ | -------------------------------------- |
| `GET /api/protocols` | All protocols, active and deprecated. |
| `GET /api/protocols/active` | Active protocols only. |
| `GET /api/protocols/{name}` | A single protocol by name. |
| `GET /api/currencies` | All currencies, active and deprecated. |
| `GET /api/currencies/active` | Active currencies only. |
| `GET /api/currencies/{ticker}` | A single currency by ticker. |
### Treasury and incentives [#treasury-and-incentives]
Protocol-revenue side: where NLS flows in and out.
| Path | What it returns |
| ---------------------------- | --------------------------------------------- |
| `GET /api/revenue` | Total protocol revenue to date. |
| `GET /api/distributed` | Total distributed rewards. |
| `GET /api/buyback` | Buyback history (`?period=3m\|6m\|12m\|all`). |
| `GET /api/buyback-total` | Total buyback amount. |
| `GET /api/incentives-pool` | Current incentives-pool balance. |
| `GET /api/earnings?address=` | Earnings for one address. |
### Platform-wide metrics [#platform-wide-metrics]
The numbers you'd put on a dashboard.
| Path | What it returns |
| ------------------------------------ | --------------------------------------- |
| `GET /api/total-value-locked` | Platform TVL. |
| `GET /api/supplied-funds` | Total supplied funds. |
| `GET /api/borrowed` | Total borrowed (optional `?protocol=`). |
| `GET /api/open-interest` | Open-interest value. |
| `GET /api/supplied-borrowed-history` | Historical series for both. |
### Positions and leases [#positions-and-leases]
Per-position state, scoped by address or pulled in aggregate.
| Path | What it returns |
| ------------------------------ | -------------------------------- |
| `GET /api/positions` | All currently open positions. |
| `GET /api/leases?address=` | Leases owned by a given address. |
| `GET /api/liquidations` | Liquidation history. |
| `GET /api/historically-opened` | Historical position openings. |
### Liquidity pools [#liquidity-pools]
LPP utilisation and lender state.
| Path | What it returns |
| -------------------------------------- | ----------------------------------- |
| `GET /api/pools` | All pools with utilisation and APR. |
| `GET /api/utilization-level?protocol=` | Pool utilisation history. |
| `GET /api/current-lenders` | Active lenders. |
| `GET /api/historical-lenders` | Lender history. |
### Pricing, blocks, subscriptions [#pricing-blocks-subscriptions]
Cross-cutting endpoints for oracle prices, chain head, and
push-notification subscriptions used by the webapp.
| Path | What it returns |
| ---------------------------- | --------------------------------------- |
| `GET /api/prices` | Current oracle prices. |
| `GET /api/blocks/...` | Indexed blocks. |
| `GET /api/subscriptions/...` | Webapp push-notification subscriptions. |
## Filtering and exports [#filtering-and-exports]
Most list endpoints share a few query-string conventions:
* `?format=csv` - return CSV instead of JSON.
* `?period=3m|6m|12m|all` - time-window filter on history endpoints.
* `?from=` - incremental sync filter; only rows updated
since the given timestamp.
* `?export=true` - streaming CSV export of the full result set, for
bulk pulls that would otherwise paginate.
## Caching [#caching]
Responses are cached at the API layer with per-endpoint TTLs. Treat
the ETL as a near-real-time view of chain state - subsecond for live
positions and prices, slightly stale for aggregates that don't need
real-time updates. If you need strictly authoritative chain state,
go to RPC/gRPC directly; the ETL is optimised for analytics-style
queries that an RPC node would answer slowly or not at all.
# Overview (/docs/ibc/overview)
Nolus reaches its host networks over IBC to execute swaps as part of
opening, repaying, and closing margin positions. The primary market is
**Solana**, served natively by [Solray](/docs/ibc/solray), the protocol's
own IBC program there. On Cosmos counterparty chains the same role is
played by **Interchain Accounts (ICA)**.
## How a packet flows [#how-a-packet-flows]
Two chains don't talk to each other directly. A relayer process watches
both ends, picks up packets emitted on one chain, builds the
corresponding message, and submits it to the other - then carries the
acknowledgment back.
Running a Hermes relayer for Nolus is documented under
[Network > IBC relayer](/docs/network/relayer).
Anyone can run a relayer - see [Network > IBC relayer](/docs/network/relayer)
for the operator side.
## Counterparty integration [#counterparty-integration]
Each supported DEX is wired up as a *protocol* in the money-market
contract workspace, with the channel/connection configuration baked into
the network's genesis or a governance upgrade. On Solana the integrated
DEX is Metis, a swap aggregator, reached through Solray's Remote Lease
channel.
# Solray (/docs/ibc/solray)
Solray is the
IBC implementation Nolus runs **natively on Solana** - a single on-chain
program that hosts the IBC core state (client, connection, channels) and two
IBC applications on top of it:
* **ICS-20 token transfer** - moves tokens between Nolus and Solana in both
directions: Solana-native tokens are locked in per-token escrow accounts,
and tokens arriving from Nolus (such as NLS) are minted as "mirror" SPL
tokens.
* **Remote Lease** - executes lease operations that a Nolus lease contract
requests on Solana: opening a position, swapping via a DEX aggregator
(currently Jupiter), transferring out, and closing. A *lease* is a
[margin position](/docs/protocol/borrowing) - the same Lease described
under Protocol > Spot Margin; "Remote" because the position's assets live
on Solana while the contract governing it runs on Nolus.
As on every IBC connection, off-chain relayer processes carry the packets:
they pick up what one chain emits, submit it to the other, and return the
acknowledgement.
This page is a reference for the **production deployment on Solana mainnet**:
the accounts the program controls, what each does, and its concrete address.
## Production deployment [#production-deployment]
| What | Address |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Program ID | [`9ATDq9dpDcM517hncjHnbWsBXzsNxKvqdfJ4L5qHJYy2`](https://solscan.io/account/9ATDq9dpDcM517hncjHnbWsBXzsNxKvqdfJ4L5qHJYy2) |
| ProgramData (upgradeable loader) | `BNQ6uyEmDmEUTG2wrxePFKC9w6z7GQZVFzPP5Zo1A3k9` |
| NLS token (mirror mint) | [`B9Vhg6XrNswZvLdKgpKhZ9Fj9yV7j2RxQKUaq5fmuphv`](https://solscan.io/token/B9Vhg6XrNswZvLdKgpKhZ9Fj9yV7j2RxQKUaq5fmuphv) |
| Statics address lookup table | `75nJQPCfAwcrxHJG2kbcbozNEBKJtUZF8L22KNXxnRtJ` |
The program is deployed through Solana's upgradeable BPF loader; upgrades are
controlled by the upgrade authority recorded in the ProgramData account.
## How addresses are derived [#how-addresses-are-derived]
Every account the program owns is a Program Derived Address (PDA) under the
program ID. Seeds are short, fixed two-byte designators chained from parent
to child, with 2-byte big-endian ordinals for numbered entities. The
production instance uses **instance ordinal 0**, so every path starts with
`["pr", 0u16]`.
| Entity | Seed path |
| --------------------------------------- | ------------------------------------------------------------------- |
| Program (instance) | `["pr", ordinal]` |
| Vault authority | `["pr", 0, "va"]` |
| Statics table pointer | `["pr", 0, "st"]` |
| Client N | `["pr", 0, "cl", N]` |
| Client N's connection list | `["pr", 0, "cl", N, "cc"]` |
| Connection N | `["pr", 0, "cn", N]` |
| Connection N's channel list | `["pr", 0, "cn", N, "co"]` |
| Channel N | `["pr", 0, "ch", N]` |
| Per-channel stores | channel seeds + `"ns"` / `"pq"` / `"rc"` / `"pa"` / `"tc"` / `"le"` |
| Escrow (per token) | channel seeds + `["es", mint pubkey]` |
| Mirror mint (per token) | channel seeds + `["mt", sha256(symbol)]` |
| Lease (per position) | channel seeds + `["ls", lease address]` |
| In-flight transfer counter (per sender) | `["pr", 0, "in", sender pubkey]` |
Anyone can re-derive and verify every address below with the standard
`find_program_address` over these seeds, or query them with the `solray` CLI's
`derive` and `query` commands.
## Program-wide accounts [#program-wide-accounts]
| Account | Address | What it does |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Program state | `6SJgbqjAFxvv3KnVjEjD54H8JEG74Y7ANksrXh1cj1Q7` | Records the instance configuration and the next client / connection / channel ordinals. |
| Vault authority | [`6xDAvqaePki3Re9NN8YHLhrN2e1cZ4a2nzL2VXeDawWp`](https://solscan.io/account/6xDAvqaePki3Re9NN8YHLhrN2e1cZ4a2nzL2VXeDawWp) | The program's signing identity - a PDA with no private key that only the program can sign for. It is the mint authority of every mirror mint (including NLS), the owner of every escrow account, the authority of the address lookup tables, and the SOL source that funds rent for accounts the program creates. |
| Statics table pointer | `78Wx6LowbsBdvECQGYBuoFrfZD3boQhbi36kS2ia9Svf` | Holds the address of the deployment-wide statics lookup table (a lookup table's address depends on the slot it was created in, so it cannot be derived - this PDA is where it is discovered). |
| Statics lookup table | `75nJQPCfAwcrxHJG2kbcbozNEBKJtUZF8L22KNXxnRtJ` | A Solana address lookup table holding the accounts every transaction needs (program state, channels, packet stores, token programs) plus each registered token's mint and escrow. Lets packet-delivery transactions fit Solana's transaction size limit. Owned by the lookup-table program; its authority is the vault authority. |
## IBC entities [#ibc-entities]
One IBC client, one connection, and two channels are live:
| Entity | Address |
| -------------------------- | ---------------------------------------------- |
| `07-tendermint-0` (client) | `H66HFVTja92BRvYHPaFAB99JGzTLuhRoPSXU7SxdjdRG` |
| Client's connection list | `3Nhy9pmkdKcYK6Xi5La5nCE779V76HnW41aY5iF9Dnp6` |
| `connection-0` | `2GDasyhvUAbHmSC18Xxcdpkxnz71GEYBzVnMUfcNvpvq` |
| Connection's channel list | `AVRfVWARk7K6xMbTHhmbn43BeG7Vzj6A16hXUViuudvv` |
### channel-0 - ICS-20 transfer [#channel-0---ics-20-transfer]
The token-transfer channel: port `transfer`, version `ics20-1`, connected to
Nolus `channel-61957`.
| Account | Address | What it does |
| ----------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Channel end | `9PVMGoqDKSZYCPyCoA6ZWusbme78eZkQ1tTxXLqMQCuF` | Channel state and counterparty identity |
| Next send sequence | `B1sHvbsuSUBzN3Anma2vdhgddkNY9yWyRmqAS9nCtjvN` | Ordinal for the next outgoing packet |
| Packet commitments | `Aomi2CxYwtbkR71B9yJPyVZBh1GoQEGupfTEKpeipBb4` | Commitments of sent, not-yet-acknowledged packets |
| Packet receipts | `ugqGrE1HqQBfo1dmXwfAq546qK3k3bDdVuSxtULj9Lh` | Receipts of received packets (replay protection) |
| Packet acknowledgements | `2tprvXyrbe4EMgJMs4dMKN9U5ggKbXWAVirtmv6qvPB9` | Acknowledgements written for received packets |
| Token census | `91gUYsWAiCbnMJLzacBaXaFLhFcTMuaUZuHpMXQxhw8b` | Counts the channel's live escrows, mirror mints, and bound lease channels - the teardown gates that keep the channel from closing while tokens are still keyed under it |
### channel-1 - Remote Lease [#channel-1---remote-lease]
The lease channel: version `nls-remote-lease.v1+transfer=channel-0`, connected
to Nolus `channel-61961` on the Nolus protocol's contract port
(`wasm.nolus1xd9sgdu3lvwm0pzrxg3c3lxwvuwkc6jgzk43zkgzjnndrdp5shvsfv3uzp`).
The version string names the transfer channel it is paired with: lease
operations move funds over `channel-0`.
| Account | Address | What it does |
| ----------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Channel end | `8swVa22QfN6rX5Tb5DmTV234KMyR9esKgHKkKkba9s21` | Channel state and counterparty identity |
| Lease channel binding | `GGyDtSVRtJ7harZmwd2z4Y12e4ULVZYiE2Cp8sLJmR7N` | Binds the lease channel to its transfer channel (`channel-0`) and records the swap engine the channel is served by (`jupiter`) |
| Next send sequence | `DvLjrrvAYSXh8tEhhNpbPhYLAamnZUkDJ4cWwnJ1nr1k` | Ordinal for the next outgoing packet |
| Packet commitments | `CFsMZSMTadBjUdF3yj8GB6caDEgqaKT85sFuneyJvuEE` | Commitments of sent, not-yet-acknowledged packets |
| Packet receipts | `8o28mjzqXce4GJXyLtYHUjJBZf1QRYKtDwhEut4V7V1T` | Receipts of received packets (replay protection) |
| Packet acknowledgements | `7hzirijfq5JJCYvYGh7mq4tRGy4xxnpsjR3hL3jsEE7W` | Acknowledgements written for received packets |
| Token census | `F3Eb5eAFtvd7HdNMVsrRum8Fug9S2WXQJLjkW2LoSRiZ` | Counts the channel's live leases - the channel cannot be finalized while any lease is still open |
Each open lease (margin position) additionally owns a small set of dynamic
accounts keyed by the Nolus lease contract address: the Lease PDA itself, a per-lease authority
PDA whose associated token accounts hold the position's funds, and a
per-lease address lookup table. These are created when a lease opens and
closed (rent reclaimed) when it closes.
While a sender has ICS-20 transfers awaiting settlement, the program
additionally keeps a per-sender in-flight transfer counter PDA (seed
designator `in`), keyed by the packet sender's address - the lease authority
for lease-flow sends, the funding wallet for wallet sends. It is created at
the sender's first in-flight send and closed once the count returns to zero;
a lease cannot be closed while its authority's counter still exists.
## Tokens [#tokens]
### NLS on Solana [#nls-on-solana]
NLS arriving from Nolus over `channel-0` is minted as a mirror SPL token:
| What | Value |
| -------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Mint | [`B9Vhg6XrNswZvLdKgpKhZ9Fj9yV7j2RxQKUaq5fmuphv`](https://solscan.io/token/B9Vhg6XrNswZvLdKgpKhZ9Fj9yV7j2RxQKUaq5fmuphv) |
| Decimals | 6 (same as `unls` on Nolus) |
| Mint authority | `6xDAvqaePki3Re9NN8YHLhrN2e1cZ4a2nzL2VXeDawWp` (the vault authority) |
| Derivation | `channel-0` seeds + `["mt", sha256("unls")]` |
The supply on Solana is exactly the amount transferred over IBC and not yet
returned: the program mints on each inbound transfer and burns on each
outbound one. No wallet holds minting rights - only the program, via the
vault authority PDA.
### Registered Solana-native tokens [#registered-solana-native-tokens]
Solana-native tokens transfer to Nolus by being locked in a per-token escrow
account - an SPL token account owned by the vault authority. Registered on
`channel-0`:
| Token | Mint | Escrow |
| -------------- | ---------------------------------------------- | ---------------------------------------------- |
| USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | `A57C37YLZz412hQs6vPNYBUucRGkX5fe9bbKnwQDeRhs` |
| Wrapped SOL | `So11111111111111111111111111111111111111112` | `9GUpG4SwCjEjZfHxkXbB5HchmWhvNLnij5VNbrGv8S7u` |
| cbBTC | `cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij` | `DXvMe3wow8H5y2w4gPgEQBX19CpMQp7h3tK7u6nWKvzE` |
| ETH (Wormhole) | `7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs` | `DpCN5TnWgSmdV5YQh97ThKa4fC1vA5ZVK697JYgEEQXP` |
| HYPE | `98sMhvDwXj1RQi5c5Mndm3vPe9cBqPrbLaufMXFNMh5g` | `EkBvfVYmpfYMPThZygDu61QH2VVNqJKeKm2QiMaLXyib` |
## Program IDL [#program-idl]
A machine-readable IDL for the program is published at
[`/idl/solray.json`](/idl/solray.json) in the Anchor 0.30 JSON format. It
describes all 36 instructions - discriminators, argument layouts, ordered
account lists with signer/writable flags - plus the decodable account-state
layouts, the PDA seed constants, and the program's error codes, so explorers
and indexers can decode Solray transactions and accounts. The file's own
top-level `docs` array carries the decoding rules an integrator needs (wire
framing, the engine account tails, the lease packet envelope, error-log
parsing).
Every address on this page is either directly readable on-chain or
re-derivable from the seed table above with the standard
`find_program_address` - the seed constants in the IDL carry the exact
designator bytes.
# Chain overview (/docs/network/chain-overview)
Nolus is a Cosmos SDK chain built with CometBFT. The chain binary is
`nolusd`, produced from the
[`nolus-core`](https://github.com/nolus-protocol/nolus-core) repository.
## Prerequisites [#prerequisites]
* [Go](https://golang.org/) (matches the version pinned in `go.mod`)
* [tomlq](https://tomlq.readthedocs.io/en/latest/installation.html) and
[jq](https://stedolan.github.io/jq/) for the helper scripts
## Build [#build]
```sh
make install
```
Compiles and installs `nolusd` locally.
## Initialize and run a local single-node chain [#initialize-and-run-a-local-single-node-chain]
`scripts/init-local-network.sh` generates a local network setup, deploys the
platform contracts, and configures Hermes.
First, generate the mnemonic Hermes will use:
```sh
nolusd keys mnemonic
```
Then initialize and start (run with `--help` for all options):
```sh
./scripts/init-local-network.sh \
--reserve-tokens \
--hermes-mnemonic \
--dex-admin-mnemonic \
--store-code-privileged-account-mnemonic
```
Notes:
* Make sure
[`nolus-money-market`](https://github.com/nolus-protocol/nolus-money-market)
is checked out as a sibling to `nolus-core`.
* Kill any running `nolusd` and `hermes` processes before re-running the
init script.
* `nolusd` logs land in `~/.nolus`.
## Run an already-configured node [#run-an-already-configured-node]
```sh
nolusd start --home "networks/nolus/local-validator-1"
```
## Run a full node with Docker [#run-a-full-node-with-docker]
The
[`nolus-networks`](https://github.com/nolus-protocol/nolus-networks)
repository carries genesis files, seed/peer lists, and a docker-based
runner for each public network.
## Reproducible binary [#reproducible-binary]
The CI builder image is publicly available:
```sh
docker run --rm -it -v "$(pwd)":/code \
public.ecr.aws/nolus/builder: \
make build -C /code
```
Use the tag matching the release you're reproducing.
# RPC, LCD, and gRPC endpoints (/docs/network/rpc-endpoints)
The Nolus team operates a public set of endpoints for the Pirin mainnet.
They are rate-limited but free to use; if you need production-grade
throughput, run your own node (see [Run a full node](/docs/network/run-a-node))
or use a community provider listed in
[`nolus-networks`](https://github.com/nolus-protocol/nolus-networks).
## Pirin (mainnet) [#pirin-mainnet]
| Kind | URL |
| -------------- | ---------------------------- |
| Tendermint RPC | `https://rpc.nolus.network` |
| LCD (REST) | `https://lcd.nolus.network` |
| gRPC | `https://grpc.nolus.network` |
## Endpoint kinds [#endpoint-kinds]
* *Tendermint RPC* - block/tx queries, websocket subscriptions; used by
the SDK to talk to the chain.
* *LCD (REST)* - gateway over the Cosmos SDK queries; useful for plain
HTTP clients without a Cosmos SDK dependency.
* *gRPC* - typed queries against Cosmos SDK and CosmWasm modules.
* *JSON-RPC (EVM)* - not applicable; Nolus is a CosmWasm chain.
# Validators (/docs/network/validators)
## Create a Validator [#create-a-validator]
### 1. Create (Or Restore) A Local Key Pair [#1-create-or-restore-a-local-key-pair]
Prior to creating your validator, you must first create your application key. Note that this is not your consensus key and will not be used for signing consensus votes. Instead, it is used to sign transactions.
Create a new key pair
```
nolusd keys add
```
Restore an existing Nolus wallet by providing a BIP39 mnemonic
```
nolusd keys add --recover
```
Get your public address from the keystore
```
nolusd keys show -a
```
Feel free to replace `` with a name of your choice.
After creating a new key, its information will be shown alongside the seed phrase. Make sure to write it down, as it is the only way to restore your keys.
### 2. Upgrade to a Validator [#2-upgrade-to-a-validator]
Do not attempt to upgrade your node to a validator until the node is fully in sync after you have started the nolusd binary, as shown in the previous section.
Since you need to send a transaction to be part of the validator set, you need to have a small amount of NLS tokens in the wallet address you are using on your keyring. Once you have a positive balance, you can send the create-validator transaction. For this, you would need to specify validator-related details, such as self-stake and commission, in a JSON file (named validator\_details.json in the example below) that is to be passed as a parameter in the command:
```
Contents of validator_details.json:
{
"pubkey": ,
"amount": "1000000unls",
"moniker": "",
"identity": "",
"website": "",
"security": "",
"details": "",
"commission-rate": "0.05",
"commission-max-rate": "0.1",
"commission-max-change-rate": "0.01",
"min-self-delegation": "1"
}
```
```
nolusd tx staking create-validator validator_details.json --chain-id "pirin-1" --fees 1000unls --from
```
If you need further explanation for each of these validator-related parameters:
* the amount is the amount you will place in your own validator in unls as self-stake (in the example, 1000000unls is 1 NLS)
* the commission rate is the rate you will charge your delegates (in the example above, 5 percent)
* the commission-max-rate is the most you are allowed to charge your delegates (in the example above, 10 percent)
* the commission-max-change-rate is how much you can increase your commission rate in a 24-hour period (in the example above, 1 percent per day until reaching the max rate)
* min-self-delegation is a strictly positive integer that represents the minimum amount of self-delegated voting power your validator must always have. In the example above, this is equal to 1 NLS or 1000000unls
* the pubkey is the validator public key that could be obtained via running "nolusd tendermint show-validator"
* the chain-id is whatever chain-id you are working with (in the Nolus mainnet case, it is pirin-1)
* the fees is the gas fee used to send this create-validator transaction
* the from flag is the KEY\_NAME you created when initializing the key on your keyring
### 3. Confirm That Your Validator Is Active [#3-confirm-that-your-validator-is-active]
If running the following command returns something, your validator is active:
```
nolusd query tendermint-validator-set | grep "$(nolusd tendermint show-address)"
```
You are looking for the bech32 encoded address in the `~/.nolusd/config/priv_validator.json` file with a nolusvalcons prefix.
### 4. Secure Your Keys [#4-secure-your-keys]
In general, a Nolus validator needs to do two things:
* Sign and commit blocks (using the Tendermint Consensus key)
* Conduct on-chain operations such as voting on Governance proposals (using an Application Operator Key)
Take good care of those two keys to mitigate hardware or software failures.
### Restore a Validator [#restore-a-validator]
A validator can be completely restored on a new Nolus node with the following set of keys:
* The Consensus key, stored in `~/.nolus/config/priv_validator.json`
* The mnemonic to the validator wallet (application key)
**Danger!** Before proceeding, ensure that the existing validator is not active. Double voting has severe slashing consequences.
To restore a validator:
* Set up a full Nolus node synced up to the latest block. Check the previous section.
* Replace the `~/.nolus/config/priv_validator.json` file of the new node with the associated file from the old node, then restart your node.
### Unjail a Validator [#unjail-a-validator]
If your validator fails to sign at least 5% of the last 10000 blocks, it will get jailed. To unjail it so that it can continue to sign blocks and respectively earn staking rewards, you can submit the following transaction:
```
nolusd tx slashing unjail --from --chain-id pirin-1 --fees 500unls
```
# Architecture (/docs/protocol/architecture)
Nolus runs on three layers: the **chain** (Go, Cosmos SDK + CometBFT), the
**contracts** (Rust, CosmWasm) on top of it, and the **off-chain services**
(price feeders, SDKs, frontends) that drive both.
## Layers [#layers]
1. **Chain layer** - [`nolus-core`](https://github.com/nolus-protocol/nolus-core)
(Go, Cosmos SDK + CometBFT). Custom modules include vesting, mint, and
tax handlers.
2. **Contract layer** - [`nolus-money-market`](https://github.com/nolus-protocol/nolus-money-market)
(Rust, CosmWasm). The workspace splits into `platform/` (network- and
protocol-agnostic) and `protocol/` (per-network/DEX). See the
[components table](/docs/protocol/overview#the-moving-parts) for a
per-contract index.
3. **Off-chain services** - the price feeder daemon, the
in-page WebMCP and REST surfaces, and frontends like
[app.nolus.io](https://app.nolus.io).
## Upgrades and the Admin contract [#upgrades-and-the-admin-contract]
The [Admin](https://github.com/nolus-protocol/nolus-money-market/tree/main/platform/contracts/admin)
contract is the migration coordinator for the rest of the system. It
holds the registry of every platform and per-protocol contract address
and is the only place that can roll out a new code ID across them as a
single atomic operation.
The contract is governance-driven, not key-controlled:
* Upgrades arrive as `SudoMsg::MigrateContracts` (or `ChangeDexAdmin`,
`RegisterProtocol`) messages, which CosmWasm sudo restricts to the
chain itself - so they only land via on-chain governance proposals
that pass.
* A separate `dex_admin` address holds permission for operational
registry edits (`Instantiate`, `RegisterProtocol`,
`DeregisterProtocol`) but can never move user funds: the Admin
contract is the registry, not a wallet. NLS lives in Treasury, LPN
in LPP, lease assets in their Lease contract.
If you ever query Nolus for "what are the current contract addresses
on chain X?", the Admin contract is the answer source.
## Cross-chain interaction [#cross-chain-interaction]
Nolus connects to counterparty chains over IBC and uses **Interchain
Accounts (ICA)** on the host chain to execute swaps as part of opening,
repaying, and closing positions. The Lease contract on Nolus is the
controller; the ICA on the host chain holds the funds during a swap and
returns them via the established IBC channel. See
[IBC overview](/docs/ibc/overview) for the channel inventory and the
packet-flow walkthrough.
# Oracle (/docs/protocol/oracle)
The Nolus oracle is a two-layer system:
1. **On-chain Oracle contract** - accepts signed spot prices from a
whitelisted set of feeders, aggregates them into an EMA, and serves
the EMA to consumers. Source:
[`nolus-money-market/protocol/contracts/oracle`](https://github.com/nolus-protocol/nolus-money-market/tree/main/protocol/contracts/oracle).
2. **Off-chain price feeder** - a Rust daemon that pulls spot prices
from configured sources (DEXs, CEX APIs), signs them, and feeds them
to the Oracle contract. Source:
[`oracle-price-feeder`](https://github.com/nolus-protocol/oracle-price-feeder).
The on-chain Oracle is the real-time price source for the rest of the
protocol. Consumers (Lease contracts, SDK clients, frontends) read EMA
prices from it directly; they don't talk to feeders.
## Market Data Price Feeder [#market-data-price-feeder]
The Market Data Price Feeder collects denomination pairs from the Market Data Price Oracle and fetches prices from the integrated DEXes for the supported pairs. It averages the retrieved prices and updates the Oracle with the latest data feed at intervals, typically every few seconds. Only governance-approved feeders are permitted to actively contribute price data to the Oracle.
Supported currency pairs are configurable through governance, with the feeder's base currency set at initialization and unchangeable afterward. Pairs can be requested in reverse (e.g., A/B data is valid for a B/A request) and can span a resolution path (e.g., \[A/C, B/C], or \[A/C, C/B]).
## Price Calculation Algorithm [#price-calculation-algorithm]
Price feeders use an Exponential Moving Average (EMA) algorithm to determine cryptocurrency pair prices, adding a security layer by prioritizing recent data. A discounting factor between 0 and 1 is applied, with higher values discounting older observations more quickly, unlike the equal-weight approach of a Standard Moving Average (SMA).
## Feeder responsibilities [#feeder-responsibilities]
* Pull spot prices from configured providers.
* Apply per-source weights and outlier filtering.
* Sign and submit spot prices within each oracle feed window.
* Recover gracefully from RPC errors and rate limits.
# Overview (/docs/protocol/overview)
## What Nolus is [#what-nolus-is]
Nolus is a sovereign blockchain with a CosmWasm money market focused on
**fixed-rate, asset-backed spot margin**. Instead of issuing a generic
collateralized loan that can be margin-called and liquidated, Nolus opens a
*lease* - a position that holds the asset the borrower takes exposure to,
until the borrower closes it. The same primitive supports both longs and
shorts.
Opening a position combines two pieces of capital:
* a **down payment** the borrower puts up as collateral, in whichever
currency they choose
* a **borrowed amount** drawn from the Liquidity Provider Pool (LPP)
The down payment and the borrowed amount are swapped together into the
**lease currency** - the asset the position holds for its lifetime.
The lease is its own on-chain CosmWasm contract instance. It tracks debt,
margin interest, and the lifecycle (open → active → close) without ever
forcing a liquidation in the traditional sense.
## Markets [#markets]
The primary market runs on **Solana**: positions are executed there
natively by [Solray](/docs/ibc/solray), the protocol's on-chain program,
with swaps routed through **Metis**, a swap aggregator on Solana. Users
connect with **Phantom** or **Solflare**. The assets currently available
to trade are listed under
[Spot Margin](/docs/protocol/borrowing#supported-assets).
## The moving parts [#the-moving-parts]
The protocol surfaces as a small set of CosmWasm contracts and a few
chain-level pieces:
| Component | Source | Role |
| --------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Leaser** | `nolus-money-market/protocol/contracts/leaser` | Factory that instantiates new Lease contracts and tracks the open set. |
| **Lease** | `nolus-money-market/protocol/contracts/lease` | One contract per position. Holds the lease asset (post-swap), accrues interest, handles repayment, close, and liquidations. |
| **LPP** (Liquidity Provider Pool) | `nolus-money-market/protocol/contracts/lpp` | Single-sided lending pool - lenders deposit, the pool extends loans to leases. |
| **Oracle** | `nolus-money-market/protocol/contracts/oracle` | On-chain price oracle. Receives spot prices from feeders and produces an EMA used by leases. |
| **Profit** | `nolus-money-market/protocol/contracts/profit` | Routes protocol revenue. |
| **Reserve** | `nolus-money-market/protocol/contracts/reserve` | Backstop pool, used when a lease closes underwater. |
| **Treasury** | `nolus-money-market/platform/contracts/treasury` | Holds protocol-owned NLS, drives long-term economic flows. |
| **TimeAlarms** | `nolus-money-market/platform/contracts/timealarms` | Generic on-chain scheduler - contracts subscribe to time-based callbacks. |
| **Admin** | `nolus-money-market/platform/contracts/admin` | Registry of every platform and per-protocol contract; coordinates atomic CosmWasm migrations across them. Driven by chain governance (`SudoMsg`), holds no user funds. |
| **Oracle Price Feeder** | `oracle-price-feeder` | Off-chain daemon that publishes signed spot prices to the on-chain Oracle. |
| **Nolus Core** | `nolus-core` | The chain itself - Cosmos SDK + CometBFT plus custom modules. |
## Why this design [#why-this-design]
Two practical consequences:
* **Partial liquidations first.** When a position's LTV crosses the max
threshold, the protocol sells just enough of the lease asset to bring the
ratio back to the healthy band. Full liquidation only kicks in when a
partial sale can't restore the invariant. See
[Risk](/docs/protocol/risk) for the full mechanism.
* **Self-contained accounting.** Each Lease is its own contract, so debt,
interest, and lifecycle state stay isolated per position. The Leaser
tracks the active set; everything else lives in the Lease.
## Where to go next [#where-to-go-next]
# Liquidations & risk (/docs/protocol/risk)
A position is healthy as long as its liability stays below the factory's
configured threshold. As price moves against the borrower, the position
climbs through three warning levels before reaching the liquidation
trigger. The percentages below are the defaults; each factory sets its
own via governance.
## Liquidations [#liquidations]
If the borrower has open liabilities that do not meet the desired conditions set within the protocol, the system needs to sell (partially or fully) the borrower's collateral so that it remains solvent. These conditions are defined in two invariants:
### 1. Interest covered [#1-interest-covered]
If the borrower has an outstanding margin or loan interest left after the defined interest payment period has passed, the invariant is broken, and the repayment algorithm is conducted using the leased amount.
### 2. Liability covered [#2-liability-covered]
If the current liability of the position is greater than or equal to the max liability, then the invariant is broken, and a market price alarm notification message is sent. Given that the healthy liability is less than the max liability of the Lease, the liquidation amount is either the lease amount itself or the amount that is enough to guarantee a healthy position after the liquidation event has occurred. This liquidation amount is then swapped to LPN and is used as an input to the repayment algorithm. If an excess amount is returned, it is transferred to the Profit contract.
**Note**: If the lease is fully liquidated, then it is closed.
## Market Anomaly Guard (MAG) [#market-anomaly-guard-mag]
To protect users from unfair liquidations during volatile market conditions, the protocol implements Market Anomaly Guard (MAG) - a price-protective algorithm that evaluates liquidation risk before executing swaps.
### The Challenge [#the-challenge]
During periods of high volatility, asset prices can temporarily diverge across exchanges and liquidity pools. While arbitrage mechanisms typically correct these discrepancies, a brief window may exist where a DEX displays prices significantly below the market average. This can result in:
* Over-liquidation of user positions
* Unnecessary losses for users
* Potential bad debt for the protocol
### MAG Implementation [#mag-implementation]
The Market Anomaly Guard operates through the following process:
### 1. Liquidation Trigger [#1-liquidation-trigger]
When a position crosses the liquidation threshold (determined by an Exponential Moving Average to avoid reacting to momentary price dips), a liquidation attempt is initiated.
### 2. Minimum Output Validation [#2-minimum-output-validation]
Before executing the swap, the protocol simulates the trade. If the expected output falls below a defined safety threshold relative to the oracle-based EMA price, the liquidation is paused.
### 3. Dynamic Response [#3-dynamic-response]
MAG monitors market conditions during the pause:
* If prices recover and the swap output improves, the liquidation resumes and finalizes
* If the asset price rises back above the liquidation trigger threshold, the liquidation is canceled entirely
This mechanism provides several key benefits:
* Protects users from liquidations at temporarily depressed prices
* Reduces unnecessary liquidation events during volatile market conditions
* Prevents protocol losses from poorly-priced swaps
* Allows positions to recover when market anomalies are temporary
## Asset-Specific Risk Framework [#asset-specific-risk-framework]
To maintain the integrity of the protocol and protect users, Nolus enforces a dynamic risk management framework designed to prevent situations where open margin positions could outstrip the available liquidity necessary for their liquidation. This is especially critical for **long positions**, where declining asset prices often coincide with diminishing stablecoin liquidity in decentralized exchange (DEX) pools, making it less likely that liquidity providers will inject fresh capital.
## Liquidity and Exposure Limits [#liquidity-and-exposure-limits]
To mitigate this risk, Nolus actively monitors the ratio of active loans to available liquidity in the integrated DEX pools. A core principle of this framework is that no asset should have outstanding loans that exceed a prudent percentage of the pool's available liquidity. While a general benchmark is that active loans should not exceed 20% of the liquidity in a given pool, this threshold is adjusted on a per-asset basis depending on the asset's broader liquidity profile.
Key factors that influence this assessment include:
* **High Market Capitalization**: A higher market cap typically indicates deeper, more resilient markets.
* **Exchange Diversity**: Assets listed across multiple exchanges benefit from greater liquidity and arbitrage activity.
* **Deep Liquidity**: Widely traded assets require more capital to move the price, reducing manipulation risks.
Thus, an asset with broad exchange coverage and deep global liquidity may be allowed a more relaxed exposure threshold, compared to a niche asset with liquidity concentrated in a single DEX pool.
## Temporary Delisting Mechanism [#temporary-delisting-mechanism]
When an asset reaches its predefined risk threshold:
* It is temporarily delisted for opening new long or short positions.
* Existing positions remain fully manageable by users and are not forcibly closed.
* Nolus continues to monitor the asset's loan-to-liquidity ratio.
If this ratio improves - i.e., loans decrease or liquidity increases - the asset can be reinstated for new margin operations.
# Security (/docs/protocol/security)
Nolus Protocol prioritises security and is transparent about it: every
contract address and balance is publicly verifiable on chain, the
codebases are open-source, and the money-market and chain core have been
audited by Oak Security and Halborn (reports below). On top of the
audits the team runs a bug bounty for community researchers.
## Audits [#audits]
| Repository | Auditor | Report |
| ------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Nolus Core | Oak Security | [Audit report](https://github.com/oak-security/audit-reports/blob/master/Nolus/2022-12-12%20Audit%20Report%20-%20Nolus%20Core%20v1.1.pdf) |
| Nolus Money Market | Oak Security | [Audit report](https://github.com/oak-security/audit-reports/blob/master/Nolus/2023-01-27%20Audit%20Report%20-%20Nolus%20Money%20Market%20v1.1.pdf) |
| Nolus Money Market | Halborn | [Audit report](https://github.com/HalbornSecurity/PublicReports/blob/master/CosmWasm%20Smart%20Contract%20Audits/Nolus_Money_Market_CosmWasm_Smart_Contract_Security_Assessment_Report_Halborn_Final.pdf) |
## Reporting a vulnerability [#reporting-a-vulnerability]
Email **[security@nolus.io](mailto:security@nolus.io)**. Do not open a public issue or PR for security
matters - that's true for every Nolus repository.
The canonical policy lives at
[`nolus-protocol/.github/SECURITY.md`](https://github.com/nolus-protocol/.github/blob/main/SECURITY.md);
the same text is mirrored below for convenience.
## Scope [#scope]
The bug bounty covers the Nolus blockchain, the smart contracts, the Solana
program (ibc-solray), and the webapp. It focuses on:
* Theft or freezing of principal funds
* Theft or freezing of unclaimed yields
* Disruptions to webapp uptime
* Unauthorized access to restricted pages
* Deletion or tampering with user data
## What to include in your report [#what-to-include-in-your-report]
* A clear description of the vulnerability and its impact.
* A **reproducible proof-of-concept** - code, transaction hashes, or a
recorded video. Theoretical findings without a working reproducer will
be closed.
* Affected versions, commits, or deployed addresses.
* A suggested fix, if you have one.
## Response [#response]
Legitimate submissions get acknowledged and triaged within a reasonable
timeframe. We reserve the right to close, without response, any report
that appears to be spam, fabricated, speculative, LLM-generated without
human verification, or otherwise not a good-faith disclosure.
## Out of scope [#out-of-scope]
* Social engineering of staff or users
* Physical attacks
* Volumetric denial-of-service against public infrastructure
* Vulnerabilities requiring privileged access you were never granted
* Findings in third-party dependencies without a working exploit against
Nolus components
## AI-assisted reports [#ai-assisted-reports]
AI tools are welcome as part of your research, but:
* Reports generated by LLMs or automated scanners **without human
verification will be closed without triage**. If you used AI, confirm
in writing that a human has reproduced and understood the finding.
* Reports that are paraphrased CVEs, raw scanner output, or speculative
code analysis without a concrete Nolus impact will be closed.
* Submitting AI-generated spam reports may result in your account being
blocked from the organization.
## Safe harbour [#safe-harbour]
Good-faith research that follows this policy will not result in legal
action.
## Contributing [#contributing]
Most Nolus-Protocol repositories are limited to organization members and
explicit collaborators; external pull requests and issues on those repos
are blocked at the platform level. Drive-by typo PRs, documentation
tweaks, and trivial formatting changes are not accepted - they're
typically airdrop-farming attempts. LLM-generated PRs or issues without
human review are closed without triage.
If you want to contribute regularly, reach the team on
[Discord](https://discord.com/invite/nolus-protocol). The full policy
lives at [`nolus-protocol/.github/CONTRIBUTING.md`](https://github.com/nolus-protocol/.github/blob/main/CONTRIBUTING.md).
# IBC transfer channels (/docs/network/relayer/channels)
| Source | Source chain ID | Source channel | Destination | Destination chain ID | Destination channel |
| ------------------- | --------------- | -------------- | --------------- | -------------------- | ------------------- |
| Nolus Pirin Mainnet | pirin-1 | channel-0 | Osmosis Mainnet | osmosis-1 | channel-783 |
In addition, Nolus opens new ICA channels for each margin position
opened - those are created on demand and not tracked here.
# Hermes setup (/docs/network/relayer/hermes)
## 1. Install Rust dependencies [#1-install-rust-dependencies]
```
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
sudo apt-get install pkg-config libssl-dev
sudo apt install librust-openssl-dev build-essential git
```
## 2. Build and configure Hermes [#2-build-and-configure-hermes]
Install the Hermes CLI, prepare the working directory, and pin to a
known-good release:
```
cargo install ibc-relayer-cli --bin hermes --locked
mkdir -p $HOME/hermes
git clone https://github.com/informalsystems/ibc-rs.git hermes
cd hermes
git checkout v1.7.1
```
Create the Hermes home + keys directories and copy the bundled example
config in:
```
mkdir -p $HOME/.hermes $HOME/.hermes/keys
cp config.toml $HOME/.hermes
```
Verify the install:
```
hermes version
```
### Hermes config.toml [#hermes-configtoml]
Edit `$HOME/.hermes/config.toml`. Remove the default example `[[chains]]`
block and replace it with the Pirin and Osmosis entries below. The
example assumes you're running both a Pirin and an Osmosis full node on
the same host - adjust the RPC/gRPC endpoints if not.
```toml
[mode.clients]
enabled = true
refresh = true
misbehaviour = false
[mode.connections]
enabled = true
[mode.channels]
enabled = true
[mode.packets]
enabled = true
[[chains]]
id = 'pirin-1'
rpc_addr = 'http://127.0.0.1:26657'
event_source = { mode = 'push', url = 'ws://127.0.0.1:26657/websocket', batch_delay = '500ms' }
grpc_addr = 'http://127.0.0.1:9090'
rpc_timeout = '10s'
account_prefix = 'nolus'
key_name = 'hermes-nolus'
store_prefix = 'ibc'
default_gas = 5000000
max_gas = 15000000
gas_multiplier = 1.1
max_msg_num = 30
max_tx_size = 2097152
clock_drift = '5s'
max_block_time = '30s'
trusting_period = '14days'
memo_prefix = ''
[chains.trust_threshold]
numerator = '1'
denominator = '3'
[chains.gas_price]
price = 0.025
denom = 'unls'
[chains.packet_filter]
policy = 'allow'
list = [
['transfer', 'channel-0'],
['ica*', '*'],
]
[chains.address_type]
derivation = 'cosmos'
[[chains]]
id = 'osmosis-1'
rpc_addr = 'http://127.0.0.1:26557'
event_source = { mode = 'push', url = 'ws://127.0.0.1:26557/websocket', batch_delay = '500ms' }
grpc_addr = 'http://127.0.0.1:9091'
rpc_timeout = '10s'
account_prefix = 'osmo'
key_name = 'hermes-osmosis'
store_prefix = 'ibc'
default_gas = 5000000
max_gas = 15000000
gas_multiplier = 1.1
max_msg_num = 20
max_tx_size = 209715
clock_drift = '20s'
max_block_time = '10s'
trusting_period = '10days'
memo_prefix = ''
[chains.trust_threshold]
numerator = '1'
denominator = '3'
[chains.gas_price]
price = 0.025
denom = 'uosmo'
[chains.packet_filter]
policy = 'allow'
list = [
['transfer', 'channel-783'],
['ica*', '*'],
]
[chains.address_type]
derivation = 'cosmos'
```
### Relayer wallet keys [#relayer-wallet-keys]
Add your relayer wallet to Hermes' keyring (stored under
`$HOME/.hermes/keys`). The wallet needs a positive balance on every
chain because the relayer pays gas to submit IBC transactions.
Use the same mnemonic across all networks, and don't use those
addresses for anything else - account sequence errors will follow.
```
hermes keys add --chain pirin-1 --mnemonic-file
hermes keys add --chain osmosis-1 --mnemonic-file
```
## 3. Validate the configuration [#3-validate-the-configuration]
```
hermes config validate
hermes health-check
```
A healthy output looks like:
```
INFO ThreadId(01) [pirin-1] chain is healthy
INFO ThreadId(01) [osmosis-1] chain is healthy
```
## 4. Run Hermes [#4-run-hermes]
Once the underlying nodes are fully synced, start the daemon:
```
hermes start
```
# IBC relayer (/docs/network/relayer)
An IBC (Inter-Blockchain Communication) relayer is an off-chain process
that ferries messages between two chains. Blockchains don't talk to each
other directly; they emit data that a relayer collects, packages into
IBC messages, and submits to the destination chain over a dedicated
channel. The relayer is a mandatory actor in the IBC architecture.
This section walks through running [Hermes](https://hermes.informal.systems/),
a Rust-based relayer. TypeScript and Go implementations exist but are
less preferred for production use.
## Hardware requirements [#hardware-requirements]
* 8-core (4 physical core) x86\_64 CPU
* 32 GB RAM (or equivalent swap)
* 1 TB+ NVMe storage
The relayer process itself is lightweight; the full nodes it relays
between are what need the resources.
Before you start, make sure you have a Nolus full node running (see
[Run a full node](/docs/network/run-a-node)). You can relay on the
same machine or connect to the Osmosis and Nolus full nodes over the
network. Install `build-essential` and `git` on the relayer host.
Download the latest Osmosis binary - the
[Osmosis docs](https://docs.osmosis.zone/) have the current setup steps.
## Walk through the setup [#walk-through-the-setup]
# Build nolusd (/docs/network/run-a-node/build)
## 1. Clone the repository [#1-clone-the-repository]
```
git clone https://github.com/Nolus-Protocol/nolus-core
cd nolus-core
```
## 2. Check out the latest stable release [#2-check-out-the-latest-stable-release]
To sync from block 1, check out the initial mainnet version (`v0.3.0`)
and upgrade the binary at each upgrade height. To sync from a recent
snapshot, check out the latest tagged release. The current version is
listed at the bottom of the [Ping.pub explorer page](https://ping.pub/nolus).
```
git checkout [latest version]
```
## 3. Compile and install [#3-compile-and-install]
```
make install
```
## 4. Verify the install [#4-verify-the-install]
```
nolusd version --long
```
You should see output similar to:
```
name: nolus server_name: nolusd version: [version] commit: [hash] build_tags: netgo ledger go: go version go1.21.5 linux/amd64
```
If `nolusd: command not found`, the Go binary path is not on your `PATH`:
```
export PATH=$PATH:$(go env GOPATH)/bin
```
# Configure (/docs/network/run-a-node/configure)
## 1. Initialize the node [#1-initialize-the-node]
```
nolusd init [custom_moniker]
```
Example: `nolusd init "My First Nolus Node"`.
This creates a `.nolus` directory under `$HOME` with configuration files
(`config.toml`, `app.toml`) you'll edit in the next steps.
Use ASCII characters for the moniker, not Unicode. The moniker is visible
to other participants if you later become a validator. You can update it
by editing the `moniker` field in `~/.nolus/config/config.toml`.
## 2. Fetch genesis and set persistent peers [#2-fetch-genesis-and-set-persistent-peers]
Get the genesis state file from the Nolus Networks repository:
```
wget https://raw.githubusercontent.com/nolus-protocol/nolus-networks/main/mainnet/pirin-1/genesis.json
mv ./genesis.json ~/.nolus/config/genesis.json
```
Live peers are published at
[`nolus-networks/.../peers.txt`](https://github.com/nolus-protocol/nolus-networks/blob/main/mainnet/pirin-1/peers.txt).
Polkachu also maintains an [alternate live peers list](https://polkachu.com/live_peers/nolus).
Connect to at least one. Peer entries are of the form
`@:`. Export them in a variable:
```
PEERS="@:,@:,..."
```
Then write them into the config:
```
sed -i -e "s|^persistent_peers *=.*|persistent_peers = \"$PEERS\"|" $HOME/.nolus/config/config.toml
```
## 3. Edit configuration files [#3-edit-configuration-files]
### Minimum gas price [#minimum-gas-price]
Open `~/.nolus/config/app.toml` and set the minimum gas price threshold
your node accepts (anti-spam):
```
minimum-gas-prices = "0.025unls"
```
A portion of the fees paid by users gets diverted to the protocol as
tax (40% at genesis). Factor this into your fee setting.
### Pruning (optional) [#pruning-optional]
Default pruning keeps application data for the unbonding period (21 days
in blocks) and prunes the rest. To prune everything (smallest disk):
```
pruning = "everything"
```
To keep all history (archive node):
```
pruning = "nothing"
```
# Cosmovisor (/docs/network/run-a-node/cosmovisor)
Using Cosmovisor is optional. It monitors the governance module and swaps
in the upgraded binary automatically, cutting downtime around chain
upgrades. Without it, you'd manually download the new binary, stop the
current process, switch binaries, and restart.
## 1. Install [#1-install]
Install the latest Cosmovisor from the Cosmos SDK:
```
go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@latest
```
## 2. Environment variables [#2-environment-variables]
Append to `~/.profile`:
```
export DAEMON_NAME=nolusd
export DAEMON_HOME=$HOME/.nolus
export MONIKER_NAME=
```
Reload:
```
source ~/.profile
```
## 3. Folder layout [#3-folder-layout]
`$DAEMON_HOME/cosmovisor` is fully managed by Cosmovisor. The `current`
subdirectory is a symlink to the active version (genesis or upgrades).
Create the base directories:
```
mkdir -p $DAEMON_HOME/cosmovisor/genesis/bin
mkdir -p $DAEMON_HOME/cosmovisor/upgrades
```
## 4. Genesis binary [#4-genesis-binary]
Copy your compiled `nolusd` into the place Cosmovisor expects:
```
cp /home//go/bin/nolusd $DAEMON_HOME/cosmovisor/genesis/bin
```
## 5. systemd service [#5-systemd-service]
Create `/etc/systemd/system/cosmovisor.service` (substitute your user
account where shown):
```
[Unit]
Description=cosmovisor
After=network-online.target
[Service]
User=
ExecStart=/home//go/bin/cosmovisor run start
Restart=always
RestartSec=3
LimitNOFILE=4096
Environment="DAEMON_NAME=nolusd"
Environment="DAEMON_HOME=/home//.nolus"
Environment="DAEMON_ALLOW_DOWNLOAD_BINARIES=false"
Environment="DAEMON_RESTART_AFTER_UPGRADE=true"
Environment="DAEMON_LOG_BUFFER_SIZE=512"
[Install]
WantedBy=multi-user.target
```
# Run a full node (/docs/network/run-a-node)
## System configuration [#system-configuration]
You should be able to run a node on most modern Linux distributions and
recent versions of macOS. This guide uses Ubuntu LTS commands - if you're
on something else, adjust accordingly. Running a full node is
resource-intensive: it stores all chain state and stays in sync with the
network. It's also a prerequisite for running a validator.
## Hardware requirements [#hardware-requirements]
Recommended minimums for a regular full node:
* 4+ vCPU
* 16+ GB RAM
* 240+ GB SSD
Archive nodes (full historical state from genesis) need substantially
more disk.
Prerequisites: Go 1.23.0 (Linux amd64) and a build toolchain:
```
sudo apt-get install -y build-essential
```
`nolusd` (the Nolus Daemon) is the CLI and daemon you use to interact
with the chain. The official Go reference implementation lives in
[`nolus-core`](https://github.com/nolus-protocol/nolus-core).
## Walk through the setup [#walk-through-the-setup]
# Run the node (/docs/network/run-a-node/run)
## Without Cosmovisor [#without-cosmovisor]
```
nolusd start
```
## With Cosmovisor [#with-cosmovisor]
Enable and start the service you created earlier:
```
sudo systemctl daemon-reload
sudo systemctl enable cosmovisor
sudo systemctl start cosmovisor
```
Check status:
```
sudo systemctl status cosmovisor
```
Tail logs:
```
journalctl -u cosmovisor -f
```
## Check sync status [#check-sync-status]
Once `nolusd` is running, the chain starts syncing. Sync time varies with
your setup and the current chain size and can be very long without a
snapshot.
```
curl http://localhost:26657/status | jq .result.sync_info.catching_up
```
* `true` - still syncing.
* `false` - caught up. Your node is now ready, and you can promote it
to a validator.
# Bootstrap from a snapshot (/docs/network/run-a-node/snapshot)
A snapshot contains a compressed copy of the Nolus chain data and wasm
directories. It lets a new node join quickly by restoring application
state instead of replaying every block from genesis. Recommended for new
participants. Always double-check the source.
The Nolus-provided snapshot is pruned. It's suitable for a validator
node but not for an RPC/archive node.
The snapshot index lives at
[`snapshots.nolus.network/backup/snapshots/`](https://snapshots.nolus.network/backup/snapshots/).
## Steps [#steps]
1. Stop the `nolusd` process or service:
```
nolusd stop
```
2. Download the latest snapshot:
```
wget -O nolus_snapshot.tar.gz https://snapshots.nolus.network/backup/snapshots/pirin-1_XXXX-XX-XX_XX-XX-XX.tar.gz --inet4-only
```
3. If you're validating, back up `priv_validator_state.json` first
(otherwise skip):
```
cp ~/.nolus/data/priv_validator_state.json ~/.nolus/priv_validator_state.json
```
4. Reset node state:
```
nolusd tendermint unsafe-reset-all --home $HOME/.nolus --keep-addr-book
```
5. Remove the current data and wasm directories:
```
rm -r ~/.nolus/data
rm -r ~/.nolus/wasm
```
6. Unpack the snapshot into the node directory:
```
tar -xzf nolus_snapshot.tar.gz --strip-components=1 -C $HOME/.nolus/
```
7. If you backed up `priv_validator_state.json`, restore it (otherwise
skip):
```
cp ~/.nolus/priv_validator_state.json ~/.nolus/data/priv_validator_state.json
```
8. Restart `nolusd`. Make sure persistent peers and `genesis.json` are
still in place:
```
nolusd start
```
# Spot Margin (/docs/protocol/borrowing)
A spot margin position is opened with borrowed capital: the protocol
borrows the loan amount from the [lending market](/docs/protocol/lending)
on the borrower's behalf and repays it, with fixed interest, as the
position is serviced and closed. The pools that lenders fund are the same
pools every position draws from.
Each supported denomination has a dedicated Liquidity Providers' Pool (LPP)
instance that handles all borrow requests and repayments in that currency.
The LPP calculates a fixed interest rate for each loan, tracks total
principal and interest due, and maintains records of all active loans. An
LPP operates exclusively in its native currency, called the Liquidity
Pool's Native (LPN) denomination, and does not interact with other assets.
A factory contract creates all margin positions, with one factory deployed
per integrated market. Each factory is connected to a single LPP - and by
extension, to a single LPN. The LPN can be a stablecoin or a volatile
token. For instance, if a user wants to borrow USDC to go long, they
submit a request to the factory linked to the LPP whose LPN is USDC.
## Supported assets [#supported-assets]
Assets currently available to long on Solana, the primary market:
| Asset | Direction |
| ----- | --------- |
| SOL | Long |
| cbBTC | Long |
| HYPE | Long |
| WETH | Long |
More assets are on the way.
## Factory configuration [#factory-configuration]
The factory smart contract is configured with parameters that define the
behavior and constraints of every position it creates.
| Parameter | Definition |
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Code ID | The identifier of the margin-position contract used to initialize new positions. |
| LPP Address | The LPP smart contract tied to this factory instance. |
| Protocol Interest Rate | The fixed interest portion collected by the protocol (e.g., 3%); 100% is used for buybacks. |
| Max Liability | Maximum allowed liability ratio. Crossing this triggers liquidation eligibility (e.g., 90%). |
| Healthy Liability | Target liability after a liquidation restores the position (e.g., 83%). |
| Initial Liability | Maximum LTV at position creation. Must be lower than the healthy liability (e.g., 60%). |
| Minimum Position Size | Minimum value, in LPN, that a position must maintain to stay alive (e.g., 15 USDC). |
| Minimum Transaction Amount | Smallest allowable amount for any transaction within the position (e.g., 0.01 USDC). |
| Interest Due Period | Window in which outstanding interest must be repaid. Past the deadline, owed interest is deducted via partial liquidation. |
| Liability Reevaluation Interval | How often (in seconds) the "liability covered" invariant is checked (e.g., every 2 seconds). |
| First / Second / Third Liquidation Warning Threshold | Three warning levels that signal escalating liquidation risk (e.g., 83.5% / 85% / 87.5%). Each must be strictly lower than the next, and all below the max liability. |
The "liability covered" invariant a position must satisfy at every check:
```
Current Liability < Max Liability × Position Value (in LPN)
```
### Worked example [#worked-example]
* Borrowed amount (loan): 150 USDC
* Initial position value: 250 USDC
* Liability: 150 / 250 = 0.60
* Max allowed liability: 0.90
If the position value drops to 200 USDC, liability becomes 150 / 200 =
0.75 - still within the limit. If it drops further to 165 USDC, liability
becomes 150 / 165 ≈ 0.91, exceeding the 0.90 cap and breaking the
invariant. Once breached, the position is flagged for liquidation.
## Walk through the flow [#walk-through-the-flow]
# Opening a position (/docs/protocol/borrowing/opening)
To open a new margin position, the user interacts with the appropriate
factory contract and provides collateral. The maximum borrowable amount is
determined by the collateral and the configured initial liability ratio
(e.g., 60%).
## 1. Collateral deposit [#1-collateral-deposit]
The user initiates a position by submitting collateral to the factory.
There are three ways to provide it:
* *Directly in the target asset.* If the collateral matches the desired
final asset of the position (e.g., a volatile token for longs, a
stablecoin for shorts), no swap is required.
* *In the LPN currency.* The collateral is swapped via an integrated DEX
(e.g., Metis, a swap aggregator on Solana) into the target asset.
* *In a different volatile asset.* The collateral is also swapped via the
DEX into the target asset.
The resulting asset determines the final size of the position. For long
positions, the target asset is typically a volatile token; for short
positions, it's a stablecoin.
## 2. Position creation [#2-position-creation]
Upon receiving the collateral, the factory instantiates a new position
contract. Only the factory has permission to do so, and this is verified
during contract creation.
Each position contract stores a fixed set of parameters at creation: the
borrower's address, the loan currency (LPN), the initial interest period,
the max and healthy liability ratios, the initial LTV, the liability
reevaluation interval, the annual protocol interest, the LPP address, and
the next interest due date.
These values come from the factory configuration and remain unchanged for
the lifetime of the position. Governance changes to a factory's config
only affect new positions opened after the change.
The maximum borrowable amount is computed from the initial liability ratio
and the provided collateral:
```
loan = initial_liability × (loan + collateral)
loan = (initial_liability × collateral) / (1 - initial_liability)
```
The total position size is the swapped sum of the borrowed amount and the
collateral.
## 3. Loan request [#3-loan-request]
Once the margin position contract is created, it requests a loan from the
corresponding LPP. As a security measure, the LPP first verifies that the
caller is a valid position contract created by the factory.
The LPP determines a fixed loan interest rate based on current pool
utilization and the size of the requested loan:
```
Quoted Loan Interest Rate = Base Rate + (Utilization Factor / Optimal Utilization) × Add-on Rate
```
Where the utilization factor is `Utilization / Idle Assets Portion`, the
base rate is the LPP-defined minimum, and the add-on rate is a slope
parameter that adjusts sensitivity beyond the base.
Worked example with utilization 60%, base rate 8%, add-on rate 2%,
optimal utilization 70%:
```
Quoted Loan Interest Rate = 8% + (1.5 / 0.7) × 2% ≈ 12.29%
```
The protocol interest rate (e.g., 4%) is added on top:
```
Effective Fixed Interest Rate = 12.29% + 4% = 16.29%
```
Nolus caps the rate above the optimal utilization threshold - in this
example, the maximum utilization factor becomes \~2.33, capping the
borrower's effective rate at \~18.67%.
Once the rate is calculated, the LPP transfers the borrowed amount
directly to the position contract. The LPP records the loan with the
position address, principal due in LPN, the fixed annual interest rate,
and a timestamp updated on every interest payment.
## 4. Cross-chain setup [#4-cross-chain-setup]
Once the borrowed funds are deposited into the position contract, a
remote account is established on the target host network. On Solana - the
primary market - this is a set of per-lease accounts owned by the
[Solray](/docs/ibc/solray) program, controlled via IBC by the position
contract on Nolus. On Cosmos host chains the same role is played by an
Interchain Account (ICA): a standard account on the host chain controlled
via IBC from a designated controller account on Nolus, in this case the
position contract.
Remote-account registration requires an IBC acknowledgment to confirm the
message landed. If it times out, the protocol automatically retries the
next block.
After registration, a "transfer out" moves both the collateral and the
loan amount from the position contract to the host chain via the
established IBC channel. Like registration, this step requires
acknowledgment before proceeding. Once the funds arrive on the host
chain, the swap into the target asset runs. When that completes, the
position is officially open.
Remote-account channels are unordered by default. If an action times out, the
channel stays open - this significantly reduces the load on relayer
providers managing cross-chain operations between Nolus and the
supported networks.
## 5. Active [#5-active]
Once activated, the position is in the `Opened` state. From here on, two
things can happen: the borrower repays the debt, or the position
undergoes liquidation.
If the user repays with the same asset as the loan's LPN (e.g., USDC for
a USDC-denominated loan), the outstanding amount is reduced immediately.
Repayment in a different asset goes through a transfer to the remote
account, a host-chain swap into LPN, and an IBC bridge back to Nolus,
with acknowledgments at each step.
Liquidation can be partial or full. In both cases, position assets are
swapped to the owed LPN on the host chain and bridged back. A partial
liquidation leaves the position active; a full liquidation marks the debt
as repaid.
## 6. Settlement [#6-settlement]
Once the total debt - principal and interest - is fully repaid or
liquidated, the position status changes from `Opened` to `Paid`. The user
then closes the position by transferring the remaining assets to their
wallet. Once that transfer is acknowledged, the position transitions to
`Closed`.
# Querying a position (/docs/protocol/borrowing/querying)
A position's status can be queried at any time. There are four observable
states:
* `Opened` - active, with outstanding interest and/or principal.
Liquidation can occur. Call `Repay` to reduce or settle.
* `Paid` - fully repaid, principal and interest, but the remaining margin
hasn't been withdrawn yet. Call `Close` to finalize.
* `Closed` - fully repaid and all assets withdrawn. Contract is inactive.
* `Liquidated` - fully liquidated. Contract is inactive.
## Returned fields [#returned-fields]
| Field | Explanation |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Position Amount | Current asset balance held in the position contract. Sum of collateral and borrowed assets post-swap, minus any liquidations or partial closings. |
| Loan Interest Rate | Interest rate paid to lenders, expressed in permille (‰). |
| Protocol Interest Rate | Protocol's interest rate (used for buybacks), expressed in permille (‰). |
| Principal Due (in LPN) | Remaining principal that must be repaid, excluding accumulated interest. |
| Protocol Interest Overdue | Unpaid protocol interest past the due period. Deducted via partial liquidation if left unpaid. |
| Loan Interest Overdue | Unpaid loan interest past the due period. Same treatment. |
| Protocol Interest Due | Total protocol interest accrued to the current point in time. |
| Loan Interest Due | Total loan interest accrued to the current point in time. |
| Take Profit Threshold | LTV at which the position automatically closes to realize profit. Empty by default. |
| Stop Loss Threshold | LTV at which the position automatically closes to preserve capital. Empty by default. |
# Quoting a position (/docs/protocol/borrowing/quoting)
Users can query the factory contract to estimate loan terms based on a
prospective collateral amount. The quote previews what a margin position
would look like under current conditions.
Quoted parameters may change by the time the position is opened because
they depend on real-time LPP utilization, which affects the interest
rate.
The returned quote includes:
* *Total* - the full size of the position (collateral + borrowed amount).
* *Borrowed* - the amount the protocol will lend, in LPN.
* *Interest rate* - the combined fixed rate, composed of the loan
interest rate (to lenders) and the protocol interest rate (for
buybacks).
# Repaying and closing (/docs/protocol/borrowing/repaying-closing)
The protocol applies any repayment amount in a strict order:
1. Overdue protocol interest
2. Overdue loan interest
3. Current-period protocol interest
4. Current-period loan interest
5. Principal repayment to the LPP
A partial repayment or partial close that uses position-internal funds
settles outstanding liabilities; the borrower must call `Claim` afterward
to withdraw leftover margin. A full close - via full repayment or full
liquidation - settles automatically and returns the remaining funds
without a manual claim.
## Manual repayment [#manual-repayment]
1. *Initiation.* The borrower calls `Repay` on the position contract. The
payment must currently be in LPN.
2. *Query outstanding debt.* The contract fetches principal due and total
loan interest due from the LPP, and initializes two working variables:
`change` (remaining repayment after each step) and `loan payment`
(amount destined for the LPP).
3. *Interest due period enforcement.* Each borrower has a window in which
outstanding interest must be paid. Paying both protocol and loan
interest in full extends the next due date by the full period; a
partial payment extends it proportionally (e.g., a 30-day period
extends by 15 days if half the interest is paid). Missing the
deadline triggers a partial liquidation to cover the unpaid amount.
4. *Protocol interest settlement.* If `change` covers the protocol
interest, it's paid in full. Otherwise a portion is paid and the rest
stays due.
5. *Loan interest settlement.* If `change` remains after protocol
interest, the position queries the LPP for the loan interest owed and
pays as much as possible.
6. *Update working variables.* If loan interest is fully covered, the
interest due period is renewed. `loan payment` is updated.
7. *Principal repayment.* Any remaining `change` is applied to the
outstanding principal.
8. *Refund and cleanup.* Any surplus is returned to the user, the loan
record is deleted from the LPP, and the borrower can withdraw the
remaining margin from the position contract via `Claim`.
## Market close (full) [#market-close-full]
In a market close, the position is fully settled using only the funds
already held in the contract - no external capital from the borrower.
1. *Initiation.* The user or the protocol triggers the full closure.
2. *Asset conversion.* All position assets are swapped to LPN on the host
DEX and bridged back via IBC, with acknowledgments at each step.
3. *Debt assessment.* The contract computes protocol interest due, loan
interest due, and principal due.
4. *Repayment from internal funds.* Debt is settled in the standard order
(overdue protocol interest, overdue loan interest, current protocol
interest, current loan interest, principal) - same as manual
repayment, but sourced entirely from the position's internal balance.
5. *Loan finalization.* If internal funds cover all debts, the position
is `Paid`; the loan entry is deleted from the LPP. No follow-up
`Repay` call is needed.
6. *Automatic asset distribution.* The remaining LPN is returned to the
borrower automatically - no manual claim required.
7. *Status update.* Position transitions directly from `Opened` to
`Closed`.
## Partial close [#partial-close]
In a partial close, the borrower uses a portion of the position's
internal funds to settle part or all of the debt. Unlike a full close,
this does not automatically close the position.
1. *Initiation.* The user triggers a partial close, specifying the
portion of the position to apply to repayment. No external funds
involved.
2. *Internal asset conversion.* The specified portion is swapped to LPN
on the host DEX (if not already LPN) and bridged back via IBC.
3. *Debt assessment.* The contract computes protocol interest due, loan
interest due, and principal due.
4. *Repayment execution.* The converted LPN is applied in order:
protocol interest, loan interest, principal. If the full debt is
covered, the position transitions from `Opened` to `Paid`. Otherwise
it stays `Opened` with reduced liability.
5. *Remaining balance.* If the converted amount exceeded the debt (e.g.,
$120 to cover $100), the surplus stays in the position contract. The
unused portion of the position (e.g., the remaining $80 in volatile
or collateral assets) also stays.
6. *Manual claim.* Since the position isn't auto-closed, the borrower
calls `Claim` to retrieve any excess LPN and the remaining
unutilized assets. Until claimed, those funds stay locked and the
position remains `Paid`. Once claimed, status moves to `Closed`.
# Deposit and withdraw (/docs/protocol/lending/deposit-withdraw)
## Deposit LPN (mint nLPN) [#deposit-lpn-mint-nlpn]
To participate as a lender in an LPP, users must hold the LPP's LPN
currency (for example, USDC) in their wallet. Deposits are submitted as
a transaction whose input message is processed by the Cosmos SDK bank
module, which transfers the funds into the LPP smart contract.
Upon deposit, lenders receive a receipt token - a CW20 derivative called
nLPN - representing their share in the pool. These tokens serve both as
proof of deposit and as the mechanism to withdraw the underlying assets
later.
The LPP contract tracks a price index representing the current exchange
rate between nLPN and LPN. When the pool is empty (e.g., at genesis),
the initial exchange rate is `1 nLPN = 1 LPN`.
Once deposits and borrowing activity begin, the price of nLPN evolves
based on:
* Newly deposited LPN tokens
* Interest payments made by borrowers
As time progresses and interest accrues, the value of each nLPN token
increases relative to LPN; the lender's share of the pool grows.
The lender's current balance (in LPN terms):
```
Lender LPN Balance = Lender nLPN Balance × Price per nLPN
```
The number of nLPN tokens minted for each deposit is determined by the
LPP smart contract using the current price index, tracked under the
lender's wallet address.
## Withdraw LPN (burn nLPN) [#withdraw-lpn-burn-nlpn]
On withdrawal, the LPP first computes the LPN the user is entitled to
based on the current nLPN-to-LPN exchange rate:
```
Claimable LPN = Provided nLPN × Price per nLPN
```
Once the claimable amount is computed:
* The user's nLPN balance is reduced accordingly.
* The LPP's LPN balance is updated to reflect the outflow.
If the user withdraws their entire nLPN balance, the contract
automatically includes any accrued interest - effectively closing out
their lending position. They no longer hold any nLPN, and they're no
longer a participant in the pool.
If the user opts for a partial withdrawal (e.g., 50% of their position),
only the corresponding portion of nLPN is burned and the remainder
continues to accrue interest.
# Lending (/docs/protocol/lending)
For each supported denomination there's a dedicated Liquidity Providers'
Pool (LPP). Each LPP operates with a single LPN (Liquidity Pool's Native
currency) and serves all lenders who deposit liquidity in that currency.
When a user deposits assets into an LPP, they receive interest-bearing
CW20 receipt tokens called nLPN (e.g., nUSDC). These represent the
lender's share in the pool and accrue interest over time through an
index-based accrual: the price of nLPN rises proportionally as borrowers
repay interest into the pool. Each nLPN reflects a fraction of the total
pool value, including both the original liquidity and accrued interest
from active loans.
On redemption, the lender gets back their share of the pool (principal
plus accrued interest) at the current nLPN/LPN exchange rate.
The LPP is a hub: lenders deposit LPN and mint nLPN (a receipt token
whose price relative to LPN drifts up as borrowers pay interest into the
pool); Lease contracts draw loans against that liquidity and return
principal + margin interest; the Treasury periodically tops up NLS
incentives that lenders claim alongside the underlying yield.
## Walk through the flow [#walk-through-the-flow]
# NLS rewards (/docs/protocol/lending/rewards)
Beyond the borrower interest that flows into the pool, lenders are
eligible for additional NLS rewards. The Treasury smart contract
distributes these incentives to each LPP, tracked internally in uNLS
(the smallest unit of NLS).
Reward distribution is based on:
* The total value locked (TVL) in a specific LPP.
* The aggregate TVL across all LPPs.
* TVL threshold brackets, configured via governance in the Treasury
contract.
Each time the Treasury transfers uNLS to an LPP, a global reward index
is updated to reflect the new incentive distribution across all nLPN
holders.
Rewards per nLPN (denominated in uNLS) are tracked as part of the
global deposit state managed by the LPP contract. The actual uNLS
balances are maintained by the Cosmos SDK bank module. Each lender
is represented by an internal record with their wallet address,
current nLPN balance, last recorded reward-per-nLPN value, and any
pending uNLS rewards accumulated since their last interaction.
## Distribute incentives [#distribute-incentives]
The global reward per nLPN, denominated in uNLS, increases each time the
Treasury transfers new uNLS rewards to the LPP. This value accumulates
over time, proportionally raising the value of each nLPN held by
lenders.
## Update and claim [#update-and-claim]
Whenever a lender deposits or withdraws, the contract calculates the
rewards accrued up to that point based on the difference between the
global reward per nLPN and the lender's last recorded reward per nLPN.
The result is added to the lender's pending rewards balance. This update
is triggered automatically on deposit, withdrawal, or claim.
On claim, the pending uNLS is transferred to the lender's address by
default (or to a specified recipient). The lender's reward index is then
synced with the current global reward per nLPN. If the lender withdraws
their full LPN balance, any remaining rewards are claimed automatically
and they cease to hold any nLPN.
## Treasury [#treasury]
The Treasury is a single-instance contract responsible for calculating
uNLS rewards across all LPPs based on their individual TVLs and the
network-wide aggregate. Each LPP reports its TVL directly to the
Treasury, which uses governance-defined threshold intervals to determine
the applicable APR. Thresholds start at a minimum and step up as higher
tiers are reached. Reward distribution runs periodically, with duration
measured in hours.