Templar Protocol User Guide
This is a comprehensive user guide for the Templar Protocol smart contracts.
For definitions of key terms and concepts, refer to the Glossary.
Quick Navigation
- Smart Contract Addresses - Official contract addresses and verification
- Deploying a market - Declarative market deployment with
tmplrmgr - Protocol Governance - Administrative controls and upgrade mechanisms
- Stellar Vault Curator Guide - Deployment, governance, allocation, withdrawals, and keeper operations
- Oracle System - Price feed infrastructure and monitoring
- Security Reporting - Security practices and vulnerability reporting
- Monitoring and Risk Management - Protocol health monitoring systems
- Testing and Coverage - Comprehensive test suite documentation
Market Operations
- Market Overview - Core market functionality
- Supply Assets - How to supply assets to earn yield
- Borrow Assets - How to borrow against collateral
- Liquidations - Liquidation mechanisms and procedures
Additional Resources
- Implementation Notes - Technical implementation details
Smart Contracts
Market
A single Templar market represents a pair of collateral and borrow assets, such as BTC/USDC for Bitcoin-collateralized USDC loans.
Suppliers may deposit borrow assets into the market, and their funds will earn yield from the protocol fees paid by borrowers. Borrowers may borrow available supply assets from the market, paying a variable interest rate based on the supply utilization rate.
Markets support NEAR fungible asset contracts implementing the NEP-141 standard or the NEP-245 standard. The borrow and collateral assets do not need to implement the same standard.
Storage Management
Before interacting with a market (supplying, borrowing, depositing collateral, etc.), accounts must first register with the market contract by making a storage deposit. This is required because the market contract implements NEP-145 (Storage Management) to cover the storage costs of maintaining user positions on-chain.
Making a Storage Deposit
To register with a market and deposit the required storage cost:
near contract call-function as-transaction \
<market-id> storage_deposit \
json-args '{}' \
prepaid-gas '10.0 Tgas' \
attached-deposit '0.00125 NEAR' \
sign-as <account-id>
The minimum required deposit can be obtained by calling the storage_balance_bounds function:
near contract call-function as-read-only \
<market-id> storage_balance_bounds \
json-args '{}' \
network-config mainnet \
now
Note: This storage deposit step is handled automatically in the Templar frontend application, but must be done manually when interacting directly with the market contracts.
Interactions
Accounts can interact with markets in seven primary ways:
- Deposit supply
- Withdraw supply
- Deposit collateral
- Withdraw collateral
- Borrow supply
- Repay supply
- Liquidate borrow position
Configuration
A market's configuration is immutable after deployment. It can be obtained from the market contract by calling the get_configuration function.
Example
near contract \
call-function as-read-only ibtc-usdc.v1.tmplr.near get_configuration \
json-args {} \
network-config mainnet \
now
Output
{
"borrow_asset": {
"Nep141": "17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1"
},
"borrow_asset_maximum_usage_ratio": "0.99000000000000000000000000000000000001",
"borrow_interest_rate_strategy": {
"Piecewise": {
"base": "0",
"optimal": "0.90000000000000000000000000000000000001",
"rate_1": "0.08888888888888888888888888888888888889",
"rate_2": "2.40000000000000000000000000000000000001"
}
},
"borrow_maximum_duration_ms": null,
"borrow_mcr_liquidation": "1.19999999999999999999999999999999999999",
"borrow_mcr_maintenance": "1.25",
"borrow_origination_fee": {
"Proportional": "0.00099999999999999999999999999999999999"
},
"borrow_range": {
"maximum": null,
"minimum": "1"
},
"collateral_asset": {
"Nep245": {
"contract_id": "intents.near",
"token_id": "nep141:btc.omft.near"
}
},
"liquidation_maximum_spread": "0.05000000000000000000000000000000000001",
"price_oracle_configuration": {
"account_id": "pyth-oracle.near",
"borrow_asset_decimals": 6,
"borrow_asset_price_id": "eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a",
"collateral_asset_decimals": 8,
"collateral_asset_price_id": "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43",
"price_maximum_age_s": 60
},
"protocol_account_id": "revenue.tmplr.near",
"supply_range": {
"maximum": null,
"minimum": "40000"
},
"supply_withdrawal_fee": {
"behavior": "Fixed",
"duration": "0",
"fee": {
"Flat": "0"
}
},
"supply_withdrawal_range": {
"maximum": null,
"minimum": "40000"
},
"time_chunk_configuration": {
"BlockTimestampMs": {
"divisor": "600000"
}
},
"yield_weights": {
"static": {
"revenue.tmplr.near": 1,
"rewards.tmplr.near": 1
},
"supply": 1
}
}
Snapshots
Interest and yield on borrow and supply positions are calculated using a snapshot system.
After a "time chunk" (e.g. 1 hour) elapses, the contract takes a snapshot, recording such things as the total supply deposit, amount borrowed, timestamp, etc.
Whenever a borrow or supply position update requires, interest/yield calculations are triggered. (They can also be triggered explicitly using harvest_yield and apply_interest.) These calculations iterate from the snapshot at which the record was last updated until the most-recently-finalized snapshot unless a snapshot limit is provided.
Supply
Accounts may deposit assets to the market's supply to earn yield.
Deposit
To add funds to a market's supply, send the to the contract, specifying "Supply" as the transfer msg.
For example:
near contract call-function as-transaction \
<borrow-asset-contract-id> ft_transfer_call \
json-args '{
"receiver_id": "<market-id>",
"amount": "<amount>",
"msg": "\"Supply\""
}' \
prepaid-gas '100.0 Tgas' \
attached-deposit '1 yoctoNEAR' \
sign-as <account-id>
Withdraw
Since borrowers borrow the assets that suppliers have supplied, when a supplier wishes to withdraw their supply, there might not be enough available to withdraw at that time. However, through fees, interest, etc., as time passes, borrow assets should become available to withdraw again.
Because the market may not have sufficient borrow asset liquidity when a supplier wishes to withdraw, the market uses a queue-based withdrawal system.
In order to withdraw supply from the market, a supplier must first enter the supply withdrawal queue with their withdrawal request:
near contract call-function as-transaction \
<market-id> create_supply_withdrawal_request \
json-args '{"amount": "<amount>"}' \
prepaid-gas '100.0 Tgas' \
attached-deposit '0 NEAR' \
sign-as <account-id>
Now the account has a position in the queue. Should the account wish to update the amount of the request, it can call create_supply_withdrawal_request again, however, this will also reset its position to the end of the queue.
A supply withdrawal request can be cancelled via cancel_supply_withdrawal_request.
In order for an account's supply withdrawal request to be fulfilled, all of the requests that are ahead of it in the queue must be fulfilled first.
To execute the next withdrawal request, use the execute_next_supply_withdrawal_request function:
near contract call-function as-transaction \
<market-id> execute_next_supply_withdrawal_request \
json-args '{}' \
prepaid-gas '100.0 Tgas' \
attached-deposit '0 NEAR' \
sign-as <account-id>
This function is not permissioned; anyone may call it to advance the withdrawal queue.
Borrow
Accounts may borrow assets from the market's supply.
Borrow positions must be collateralized with a minimum amount of collateral asset determined by the market's configuration.
Deposit collateral
To add collateral to an account's position, transfer-call the market tokens with a msg of "Collateralize":
near contract call-function as-transaction \
<collateral-asset-contract-id> ft_transfer_call \
json-args '{
"receiver_id": "<market-id>",
"amount": "<amount>",
"msg": "\"Collateralize\""
}' \
prepaid-gas '100.0 Tgas' \
attached-deposit '1 yoctoNEAR' \
sign-as <account-id>
Withdraw collateral
The collateral withdrawal process is relatively straightforward as compared to the supply withdrawal process: simply call withdraw_collateral, passing the amount of collateral asset tokens you wish to withdraw:
near contract call-function as-transaction \
<market-id> withdraw_collateral \
json-args '{ "amount": "<amount>" }' \
prepaid-gas '100.0 Tgas' \
attached-deposit '0 NEAR' \
sign-as <account-id>
While this process is simple, collateral can only be withdraw so long as the value of the remaining collateral continues to satisfy the market's borrow_mcr_maintenance requirement.
Borrow
Once an account's position is collateralized, borrow asset can be withdrawn.
near contract call-function as-transaction \
<market-id> borrow \
json-args '{ "amount": "<amount>" }' \
prepaid-gas '100.0 Tgas' \
attached-deposit '0 NEAR' \
sign-as <account-id>
As long as the collateralization requirements are met, the borrow amount (minus fees) will be sent to the predecessor account.
Repay
As long as an account has a liability (principal + interest/fees), some or all of their collateral will be locked so that it cannot be withdrawn.
To unlock the collateral, the account must repay its liability to the market.
To perform a repayment, transfer-call tokens to the market with a msg of "Repay":
near contract call-function as-transaction \
<borrow-asset-contract-id> ft_transfer_call \
json-args '{
"receiver_id": "<market-id>",
"amount": "<amount>",
"msg": "\"Repay\""
}' \
prepaid-gas '100.0 Tgas' \
attached-deposit '1 yoctoNEAR' \
sign-as <account-id>
Liquidate
Liquidation is the process by which the asset collateralizing certain positions may be reappropriated e.g. to recover assets for an undercollateralized position.
A liquidator is a third party willing to send a quantity of a market's borrow asset (usually a stablecoin) to the market in exchange for the amount of collateral asset supporting a specific account's position. As compensation for this service, the liquidator receives an exchange rate that is slightly better than the current rate. This difference in rates is called the "liquidator spread," and the maximum liquidator spread is configurable on a per-market basis.
- The liquidator MUST ensure that its account is able to receive the collateral tokens. Usually this means opting-in to storage management (if the collateral token in question implements that standard). If the collateral transfer fails, the liquidator will not be refunded!
- It is the responsibility of the liquidator to calculate the optimal amount of tokens to attach to a liquidation call. The market will either completely accept or completely reject the liquidation attempt—no refunds!
A liquidator will follow this high-level workflow:
- The liquidator obtains a list of accounts borrowing from the market by calling
list_borrow_positions. - The liquidator checks the status of each account by calling
get_borrow_status. - If an account's status is
Liquidation, that means the liquidator can obtain a spread by sending an amount of borrow asset to the market. The maximum spread isliquidation_maximum_spreadin the market configuration. - To perform the liquidation, the liquidator transfer-calls the appropriate amount of borrow asset to the market. That is to say, the liquidator calls
ft_transfer_call/mt_transfer_callon the borrow asset's smart contract, specifying the market as the receiver. Themsgparameter indicates 1) that the transfer is for a liquidation, and 2) which account is to be liquidated.
Thus, the arguments to a liquidation call might look something like this:
{
"amount": "<amount>",
"msg": {
"Liquidate": {
"account_id": "<account-to-liquidate>"
}
},
"receiver_id": "<market-id>"
}
Example
near contract call-function as-transaction \
<borrow-asset-contract-id> ft_transfer_call \
json-args '{
"receiver_id": "<market-id>",
"amount": "<amount>",
"msg": "{ \"Liquidate\": { \"account_id\": \"<account-to-liquidate>\" } }"
}' \
prepaid-gas '100.0 Tgas' \
attached-deposit '1 yoctoNEAR' \
sign-as <account-id>
Registry
The registry is a contract that maintains a list of contract versions, deploys new contracts, and maintains a list of those deployments.
Market contracts are deployed through a registry, and thus appear as its subaccounts.
The account ID of the mainnet market registry is v1.tmplr.near.
Interactions
List available versions
near contract call-function as-read-only \
v1.tmplr.near list_versions \
json-args '{"offset":0,"count":100}' \
network-config mainnet \
now
Output:
[
"v1.0.0",
"v1.1.0"
]
List deployments
near contract call-function as-read-only \
v1.tmplr.near list_deployments \
json-args '{"offset":0,"count":100}' \
network-config mainnet \
now
Output:
[
"ibtc-usdc.v1.tmplr.near",
"stnear-usdc.v1.tmplr.near",
"ibtc-iethusdc.v1.tmplr.near",
"iethwbtc-iethusdc.v1.tmplr.near",
"ibtc-usdc-1.v1.tmplr.near",
"stnear-usdc-1.v1.tmplr.near"
]
LST Oracle
The LST oracle adapter enhances base oracle functionality by supporting a broader range of asset classes. The primary transformation supported by the LST oracle adapter is price normalization of a liquid staking token (LST).
Price normalization requires retrieving the price of the underlying asset and the conversion rate between the LST and the underlying asset and combining them to produce a price for the LST asset itself.
Example: stNEAR Price Calculation
Examine the transformer specification:
near contract call-function as-read-only \
lst.oracle.tmplr.near get_transformer \
json-args '{"price_identifier":"c23cb2430c81d475fbd1c235324d4987f2dd01431bf7ab3e7b9d69b9f6701470"}' \
network-config mainnet \
now
Output:
{
"action": {
"NormalizeNativeLstPrice": {
"decimals": 24
}
},
"call": {
"account_id": "meta-pool.near",
"args": "bnVsbA==",
"gas": "3000000000000",
"method_name": "get_st_near_price"
},
"price_id": "c415de8d2eba7db216527dff4b60e8f3a5311c740dadb233e13e12547e226750"
}
This specification describes the following flow:
- Retrieve the NEAR price from the Pyth oracle (asset ID
c415de8d2eba7db216527dff4b60e8f3a5311c740dadb233e13e12547e226750). - Retrieve the stNEAR redemption rate from the staking contract (
meta-pool.near->get_st_near_price({})). - Calculate
price_stnear = price_near * redemption_rate / 10^24.
Smart Contract Addresses
Deployed Templar Protocol contracts, and how to verify them. For deploying a new market, see Deploying a market.
Deployments
- Registry:
v1.tmplr.near - LST Oracle Adapter:
lst.oracle.tmplr.near
Markets
Market contracts are deployed dynamically through the registry. Each market represents a single asset pair (COLLATERAL → BORROW).
A selection of available markets is shown below:
| Account ID | Collateral Asset | Borrow Asset |
|---|---|---|
ibtc-iethusdc.v1.tmplr.near | Native BTC (via NEAR Intents) | USDC on Ethereum (via NEAR Intents) |
iethwbtc-iethusdc.v1.tmplr.near | wBTC on Ethereum (via NEAR Intents) | USDC on Ethereum (via NEAR Intents) |
ibtc-usdc-1.v1.tmplr.near | Native BTC (via NEAR Intents) | USDC on NEAR |
stnear-usdc-1.v1.tmplr.near | stNEAR on NEAR | USDC on NEAR |
ixlm-ixlmusdc.v1.tmplr.near | Native XLM (via NEAR Intents) | USDC on XLM (via NEAR Intents) |
Contract Verification
All smart contracts use reproducible builds. To verify deployed code:
near contract verify deployed-at <contract-id> mainnet now
Example output:
INFO The code obtained from the contract account ID and the code calculated from the repository are the same.
| Contract code hash: DaudmUa3nAym9dfQkn8mpNPZxkphSRGwEaTMgtymVhFE
| Contract version: 1.0.0
| Standards used by the contract: [nep330:1.2.0]
| View the contract's source code on: https://github.com/Templar-Protocol/contracts/tree/1d736e62a86424dd947284cbd8e83bef803fa9fb
| Build Environment: sourcescan/cargo-near:0.13.4-rust-1.85.0@sha256:a9d8bee7b134856cc8baa142494a177f2ba9ecfededfcdd38f634e14cca8aae2
| Build Command: cargo near build non-reproducible-wasm --locked
Deploying a market
A market is described by one spec file and deployed in two commands. There is
no shell script: the spec is the source of truth, and everything that used to
live in env.sh, market-args.json and proxy-*.json is derived from it.
deployments/alpha targets a mainnet registry, so both commands need
--network mainnet — the CLI defaults to testnet. NETWORK and SIGNER_ID
work in place of the flags.
tmplrmgr market plan deployments/alpha/<market>.toml --out plan.json \
--network mainnet --signer-id "$REGISTRY_OWNER" --public-key ed25519:…
tmplrmgr market apply --plan plan.json \
--network mainnet --signer-id "$REGISTRY_OWNER" --sign-with keychain
The signer is not a personal account: registry.deploy asserts the registry's
owner, and a proxy spec additionally requires it to equal governance.admin,
which the mainnet profiles set to the registry itself.
plan reads the chain and writes a file; it sends nothing and takes no
credential. apply sends what the file says.
Why two steps
The plan is a reviewable artifact. It lists every transaction, decodes the market configuration for reading, names the keys each new account will grant, and carries the results of every preflight check. Reviewing a deployment no longer means reading a shell script and trusting it matches four JSON files.
It is a record of a derivation, not an input: the file carries the spec it came
from, and apply re-derives the steps and refuses anything that does not match.
Editing the plan is therefore not a way to change a deployment — change the spec
and re-plan. For something no spec can express, run the transaction yourself
with the command that performs it (registry deploy, proxy-oracle governance create-proposal, storage deposit); each is typed and validated on its own.
Writing a spec
Shared values live in deployments/profiles/. A market file names the profiles
it extends and states only what differs. Abbreviated — a market also needs a
[borrow] leg and the [market] parameters the profiles above do not set; see
any file under deployments/ for a complete one:
extends = ["../profiles/alpha.toml", "../profiles/irs-standard.toml"]
name = "my-market"
[oracle.direct] # reads an oracle that already exists
account_id = "pyth-oracle.near"
[collateral]
asset = "nep141:usdc.near"
price_id = "eaa020c6…" # the oracle's own identifier
decimals = 6
Omit [oracle.direct] to deploy a dedicated proxy oracle instead. A proxy
market names sources per asset and the deployment creates a governance
contract, the oracle it owns, and the market — seven transactions, plus one
storage registration per NEP-141 asset, rather than one.
Amounts
Every amount states its unit, and the tool does the scaling:
[market]
borrow_range = { minimum = "1 atom" }
supply_range = { minimum = "0.04 tokens" }
supply_withdrawal_range = { minimum = "0.04 tokens", maximum = "1000 tokens" }
origination_fee = { Flat = "0 atoms" }
tokens counts whole units of the borrow asset, scaled by its decimals when
the plan is built — "0.04 tokens" is four cents of a stablecoin whether it
carries 6 decimals or 7. atoms counts the indivisible base units the chain
stores, so "1 atom" says "no real floor" in a way "0.0000001 tokens" does
not. All three ranges and both fees are denominated in the borrow asset;
nothing is stated in collateral.
The unit is mandatory. A bare number is refused rather than guessed at, as is a
tokens value with more decimal places than the asset can hold, or a fractional
atom. Both spellings parse (1 atom, 1 atoms); the tool writes the plural.
This is what schema 5 changed. A schema 4 file wrote the same amounts as bare
base-unit integers, which are still well-formed numbers — read as whole units
they would be 10^decimals too large — so such a file is refused by version and
must be re-authored, not renumbered.
Checking before and after
tmplrmgr spec check deployments/alpha/<market>.toml --network mainnet
tmplrmgr market verify <account-id> --network mainnet \
--governance-admin <account-id> \
--against deployments/alpha/<market>.toml
Both modes verify. A direct market reconstructs without proxies or governance,
so the two governance checks are skipped and everything else runs;
--governance-admin is still required and means nothing there.
verify re-runs the preflight against what is actually on chain and exits
non-zero on failure, so it can run on a schedule. That matters because the
governance call that configures a price feed is dispatched detached: it reports
success even when the oracle rejected the proxy, so deployed state is the only
witness that a market can price anything.
Reading the report
Checks are printed to stderr as they run, grouped by what is being read, then summarized. The summary leads with the failures, in full, and lists what was skipped separately — a check that did not run proves nothing, and must never be counted as one that passed.
→ registry versions
ok registry.version.market v1.3.0
FAIL registry.version.oracle `0.5.9` is not registered in v1.tmplr.near; the depl…
5 check(s): 3 passed, 1 skipped, 1 FAILED
FAILED
registry.version.oracle
`0.5.9` is not registered in v1.tmplr.near; the deploy would fail partway
Colour is used only on a terminal, and NO_COLOR turns it off. -q silences
the report. stdout stays the machine-readable channel throughout, so
spec check … >/dev/null leaves the report alone and … 2>/dev/null | jq
leaves the JSON alone.
--skip-check <id> suppresses one verdict — every other check still runs, and
the report records what the skip suppressed, so an override stays reviewable
rather than reading as a pass. An id that matches no check is an error, since a
typo would otherwise silently suppress nothing. Available on spec check,
market plan and market apply.
Resuming
apply journals each step beside the plan as it lands. If a run is
interrupted, re-running it skips what completed and continues from the first
incomplete step. A plan truncated to its completed prefix is refused rather than
reported complete: the re-derivation runs before the journal is consulted.
Oracles
Templar Protocol relies on external price oracles to determine asset valuations when calculating collateralization ratios and performing liquidations.
Pyth Network is the primary oracle provider (documentation).
Pyth is a pull oracle, meaning that the price feeds are updated as-needed instead of continuously. As such, interactions with Templar markets should always be preceded by a call to the appropriate oracle contract to update the necessary asset prices using a proof provided by Pyth.
More information about how to perform this update on NEAR can be found on Pyth's documentation site.
Oracle Addresses
| Network | Account ID |
|---|---|
| Testnet | pyth-oracle.testnet |
| Mainnet | pyth-oracle.near |
Price Identifiers
Price identifiers for Pyth Network assets can be found on their documentation site.
LST Oracle Adapter
For Liquid Staking Tokens (LSTs), Templar uses a custom oracle adapter (lst.oracle.tmplr.near) to derive the LST price from the underlying asset price(s).
Price Feed Configuration
Each market is configured with the following fields:
#![allow(unused)] fn main() { pub struct PriceOracleConfiguration { /// Account ID of the oracle contract. pub account_id: AccountId, /// Price identifier of the collateral asset in the oracle contract. pub collateral_asset_price_id: PriceIdentifier, /// Collateral asset decimals, to convert the oracle price. pub collateral_asset_decimals: i32, /// Price identifier of the borrow asset in the oracle contract. pub borrow_asset_price_id: PriceIdentifier, /// Borrow asset decimals, to convert the oracle price. pub borrow_asset_decimals: i32, /// Maximum price age to accept from the oracle, after which the price /// will be considered stale and rejected. pub price_maximum_age_s: u32, } }
Update Frequency and Freshness
- Update Frequency: As-needed (pull model)
- On-Chain Updates: Pulled on-demand by protocol operations
- Price Staleness: Configurable maximum age per market (typically 60 seconds)
Price Validation
Markets validate price freshness before use. If prices are stale, users must push fresh price data to the oracle contracts
- Operations that require prices (borrow, liquidate) will fail.
- Users must push updates for fresh price data.
There is currently no backup oracle available.
Oracle Security Measures
- Confidence Intervals: Pyth prices include confidence bands. The lower bound is used for collateral valuations, and the upper bound for liability valuations.
- Multiple Data Sources: Pyth aggregates from multiple price providers.
- Time-Weighted Averages: Market contracts use the exponentially-weighted moving average (EMA) price information.
- Maximum Age Limits: Markets reject stale price data using a configurable expiration duration.
Oracle Failure Scenarios
Temporary Outage
- The vast majority of operations will cease to function until fresh price data are available.
- Users can still withdraw collateral from positions with zero liability.
- No borrows or liquidations are supported until fresh price data are available.
Price Manipulation Attack
- Markets will reject stale prices automatically.
- Defensive asset valuations will protect markets from insolvency in most cases.
- The required maintenance MCR will protect borrowers from unexpected liquidation in most cases.
Protocol Governance
This document outlines the current administrative structure and governance controls of Templar Protocol.
Registry Contract
The registry contract is immutable once deployed and locked. It designates an owner account, which has permission to add new contract code versions and to deploy new contracts. There is no upgrade mechanism.
Market Contracts
Market contracts are immutable once deployed and locked. The configuration is immutable after deployment. New market versions can be deployed to new account IDs via a registry, but old versions cannot be overwritten. There is no upgrade mechanism. When a new version of the market contract is available, it will be uploaded to the registry contract. New markets can then be deployed using the updated code. However, old markets will not be upgraded, and funds will not be automatically migrated, so users will need to migrate their positions individually.
Market contracts have no administrative functions:
- Operate autonomously based on initial configuration.
- No ability to pause, upgrade, or modify market parameters.
Emergency Procedures
Markets are immutable once they are deployed. If a bug is discovered, a patched version of the code will be uploaded and affected markets will have new versions deployed. However, users will need to migrate their funds individually. To facilitate this process swiftly and securely, users are encouraged to monitor all official communication channels for announcements.
Transparency and Monitoring
Templar markets are open-source, and the source code currently available on GitHub. All completed audits will be made available as soon as possible. See the current list of audits.
Stellar Vault Curator Guide
This is the operator runbook for Templar vaults on Stellar/Soroban. The
tmplr-soroban-vault CLI and its deployment manifest are the supported curator
interface for deployment, governance, allocation, withdrawal servicing,
accounting maintenance, and TTL renewal.
Scope
The separate NEAR vault executor is not a deployed or audited curator surface and is not an operations reference for this guide. A Stellar vault adapter may represent a route that ultimately leaves Stellar, but the vault, shares, governance, accounting, and curator actions described here remain on Stellar.
Authoritative references
- Operator CLI:
tools/soroban-vault-cli/README.md- Stellar runtime mechanics:
contract/vault/soroban/README.md- Vault state machine:
contract/vault/README.md- Stellar threat model:
contract/vault/soroban/STRIDE.md
What a curator operates
A Templar Stellar vault is a single-asset vault with ERC-4626-compatible deposit, mint, withdraw, redeem, conversion, and limit semantics exposed through a Soroban proxy. Depositors supply one SEP-41 asset and receive transferable SEP-41 vault shares. Curators configure adapter-backed markets; allocators move pooled assets between the vault's idle balance and those markets.
A deployed stack contains:
- Vault runtime — canonical custody, accounting, state machine, RBAC, withdrawal queue, and applied policy state.
- Share token — the SEP-41 receipt token. Only the vault can mint and burn shares for vault flows.
- Governance contract — proposal submission, per-action timelocks, acceptance, revocation, and irreversible abdication.
- ERC-4626 proxy — user-friendly deposit, mint, atomic withdraw, redeem, and preview methods.
- Curator proxy — the typed curator-facing proxy included in a full stack.
- Adapters — one contract per market route, such as a Blend pool adapter or a custodial adapter.
The runtime uses the shared templar-vault-kernel state machine, but all
operational calls in this guide target the Stellar contracts through
tmplr-soroban-vault or stellar contract invoke.
Accounting model
The core invariant is:
total_assets = idle_assets + external_assets
idle_assetsare underlying tokens held by the vault. They fund deposits, atomic exits, and queued-withdrawal payouts.external_assetsare the aggregate market principals/NAV recorded from adapters. The vault does not create a per-user market position.- Direct transfers of the underlying asset to the vault are reconciled as idle assets for existing shareholders. They are not captured by the next depositor.
- Adapter NAV is not live-read by every preview. Run
curator refresh-marketsbefore relying on share-rate or fee-accounting views after a route's value changes.
Roles and authority
| Identity | Authority |
|---|---|
| Governance admin | Submits and accepts governance actions. This is the address passed as --admin and may be a Stellar account or a contract/multisig. |
| Vault curator | Runtime policy authority and an implicit allocator. A new stack uses the deployment --admin as the initial curator; governance can later replace it. |
| Allocator | Supplies to markets, recalls liquidity, refreshes adapter NAV, executes ready queued withdrawals, and may abort a stale Withdrawing operation. |
| Sentinel | Separate emergency backstop. It can pause, tighten restrictions, revoke specified operational/economic proposals, and use allocator-emergency recovery. It cannot unpause, relax restrictions, or accept proposals. |
The governance contract is not implicitly the Sentinel. Production deployments should separate governance, allocator, and emergency keys or contracts according to their operational risk.
Curator economics (fees)
There are two fee types. Both are minted as new SEP-41 shares to a configurable recipient.
| Fee | Basis | Cap |
|---|---|---|
| Management | Time-weighted on AUM (rate × AUM × elapsed / 1yr), accrues regardless of performance | 5% / year |
| Performance | AUM growth since the last accrual checkpoint; zero on flat or down periods | 50% of profit |
Rates are WAD-scaled (1e18 = 100%). Each fee has its own recipient, and they
can differ.
Operational details:
- Checkpoint, not all-time high-water mark. Stellar share-pricing paths
(
DepositWithMin,RefreshFees,ResyncIdleBalance) first reconcileidle_assetsagainst the live asset-token balance, then reset thefee_anchorto the reconciled total at the current ledger time. Profit is measured ascurrent_AUM − anchor_AUM; if AUM is flat or down, the performance fee is zero. Because the anchor resets after each interaction, a recovery following a loss is chargeable — this is "growth since the last checkpoint", not "above the all-time peak". When fees are active, a deposit first crystallizes elapsed fees before the post-deposit anchor is written, so deposit principal cannot erase accrued fees. - Growth-rate cap (
max_total_assets_growth_rateinternally and--max-growth-rate-wadin the CLI, optional). Caps how fast AUM is allowed to count for fee accrual:effective_AUM = min(current, last × (1 + max_rate × dt/yr)). Relaxing or removing this cap is timelocked. - Refresh order matters.
curator refresh-feesreconciles the live idle token balance, but it does not query every adapter. Refresh changed markets first, then crystallize fees against the resulting aggregate NAV.
tmplr-soroban-vault curator refresh-markets \
--caller GALLOCATOR... \
--markets 0,1
tmplr-soroban-vault curator refresh-fees
Set up the operator CLI
The examples below assume tmplr-soroban-vault is on PATH. From a source
checkout, run the same commands with:
cargo run -p templar-soroban-vault-cli -- <arguments>
The current stack uses stellar-cli v26 and Rust 1.92. Run the repository's
Stellar CLI installer or enter its devenv before operating a vault.
Create a public profile for repeatable network, RPC, manifest, and address defaults:
tmplr-soroban-vault profile init testnet
tmplr-soroban-vault --profile testnet doctor
Profiles must not contain seeds or secret keys. Keep signing material in the
Stellar keystore, select it with stellar keys use <identity>, or provide an
ephemeral secret through STELLAR_ACCOUNT. Never place a seed phrase or secret
key in --source-account.
The deployment manifest defaults to:
contract/vault/soroban/.deploy-state/manifest.json
It records contract IDs, constructor arguments, artifact hashes, initialization
state, and successful transaction audit records. Treat it as operational state,
back it up, and pass --state explicitly when operating more than one vault.
Deploy a Stellar vault stack
Plan the deployment before writing to the network:
tmplr-soroban-vault deploy plan stack \
--admin GCURATOR_OR_MULTISIG... \
--asset-token CASSET... \
--governance-timelock-ns 86400000000000 \
--blend-pool CBLENDPOOL...
Then deploy the same configuration:
tmplr-soroban-vault deploy stack \
--admin GCURATOR_OR_MULTISIG... \
--asset-token CASSET... \
--governance-timelock-ns 86400000000000 \
--blend-pool CBLENDPOOL...
tmplr-soroban-vault status
tmplr-soroban-vault reconcile --json
deploy stack checkpoints the manifest after each upload, deployment, import,
and initialization step. Reruns reuse recorded contract IDs and remotely
available WASM. Use --force-new only when fresh contract instances are the
explicit intent.
If deployment stops after one or more transactions:
tmplr-soroban-vault reconcile --json
tmplr-soroban-vault deploy repair --json
tmplr-soroban-vault deploy resume \
--governance-timelock-ns 86400000000000 \
--blend-pool CBLENDPOOL...
Resume only when reconciliation reports safe_to_resume: true. status reads
the manifest; reconcile compares it with chain state and is the stronger
check.
Mainnet writes require the global --allow-mainnet-write flag. A zero governance
timelock additionally requires --allow-zero-timelock and should be limited to
explicit local/test configurations.
Governance lifecycle
The deployment --admin becomes both the governance admin and initial vault
curator. Governance can later assign a different curator, Sentinel, and
allocator set.
A submission always returns a proposal ID. Directionally safe actions may be
executed during submission; timelocked actions remain in the pending queue. Do
not assume every returned ID needs a later accept call.
Inspect queued proposals before accepting them:
tmplr-soroban-vault governance queue
tmplr-soroban-vault governance explain --proposal-id 7
tmplr-soroban-vault governance accept \
--admin GCURATOR_OR_MULTISIG... \
--proposal-id 7
governance accept-ready is useful for routine automation, but exact proposal
IDs are safer for high-impact changes. In particular, inspect and accept market
cap proposals by ID; a textual cap filter can also match cap-group actions.
Exact timing rules
Governance timelocks are configured per action kind between 0 and 30 days. The initial value is chosen at deployment; there is no universal production default.
| Change | Contract behavior |
|---|---|
| Pause | Only the Sentinel can pause, and it is immediate. Governance submit-set-paused --paused is rejected. |
| Unpause | Governance proposal; timelocked under Pause. |
| Restrictions | Sentinel may apply only a tightening change immediately. Every governance-admin restrictions submission is timelocked. |
| Fees | A proposal containing only fee decreases and/or a tighter growth cap executes immediately if recipients do not change. Any fee increase, recipient change, or growth-cap relaxation/removal is timelocked. |
| Market cap | Lowering an existing cap, including setting it to 0, executes immediately. A new market or cap increase is timelocked. |
| Cap groups | A new group cap, a cap increase, and every membership change are timelocked. Decreasing a known absolute or relative group cap executes immediately. Relative caps cannot exceed 100%. |
| Supply queue, allowed adapters, allocators | Timelocked. |
| Curator, governance, admin, market removal, skim, upgrade, migration | Timelocked. |
| Sentinel appointment | The first appointment may execute immediately; replacing an existing Sentinel is timelocked. |
| Timelock configuration | Increasing a duration executes immediately. Decreasing one is queued under the TimelockConfig timelock. |
| Withdrawal and idle-resync cooldowns | Every change is timelocked. |
The governance admin can permanently disable an action kind with abdicate.
Abdication is irreversible; confirm the exact action kind and recovery
implications before submitting it.
Fees example
Fee values are WAD-scaled integers: 1e18 = 100%.
tmplr-soroban-vault governance submit-set-fees \
--admin GCURATOR_OR_MULTISIG... \
--performance-fee-wad 200000000000000000 \
--performance-recipient GPERFORMANCE... \
--management-fee-wad 20000000000000000 \
--management-recipient GMANAGEMENT...
The command prints a semantic old/new diff and requires interactive
confirmation or --yes. Inspect whether the proposal executed immediately or
entered the queue before running accept.
Cap groups
Cap groups limit correlated routes together. When both limits are configured, the effective ceiling is:
min(absolute_cap, relative_cap × total_assets)
Absolute caps use raw asset base units and relative caps use WAD:
tmplr-soroban-vault governance submit-set-group-cap \
--admin GCURATOR_OR_MULTISIG... \
--group blue-chip \
--cap 50000000000000
tmplr-soroban-vault governance submit-set-group-rel-cap \
--admin GCURATOR_OR_MULTISIG... \
--group blue-chip \
--relative-cap 400000000000000000
tmplr-soroban-vault governance submit-set-group-member \
--admin GCURATOR_OR_MULTISIG... \
--market-id 0 \
--group blue-chip
New group limits and all membership assignments are timelocked. Inspect and accept each proposal ID before relying on the group.
Sentinel emergency actions
Sentinel pause and restriction tightening are direct governance-contract entrypoints, not queued CLI governance proposals:
stellar contract invoke \
--id "$SOROBAN_GOVERNANCE" \
--source-account sentinel \
-- set_paused \
--caller GSENTINEL... \
--paused true
stellar contract invoke \
--id "$SOROBAN_GOVERNANCE" \
--source-account sentinel \
-- set_restrictions \
--caller GSENTINEL... \
--mode 1 \
--accounts '["GACCOUNT..."]'
Restriction modes are 0 = none, 1 = blacklist, and 2 = whitelist.
The governance contract rejects a Sentinel restriction change that relaxes the
current policy.
Restoring normal operation uses governance and waits for the configured timelock:
tmplr-soroban-vault governance submit-set-paused \
--admin GCURATOR_OR_MULTISIG...
tmplr-soroban-vault governance queue --kind pause
The CLI's boolean --paused flag defaults to false when omitted. Supplying the
flag requests true, which this governance submission path rejects.
Pausing also blocks ordinary allocation and refresh operations. If an incident
requires liquidity recall, decide whether to lower market caps and unwind routes
before a global pause. Allocator-emergency recovery such as
abort-withdrawing remains available while paused.
Add and activate market routes
Deploying an adapter does not make it usable by the vault. An active market requires three accepted governance states:
- The adapter contract is in the allowed-adapter set.
- The market ID has a nonzero cap in raw asset base units.
- The supply queue binds that market ID to the adapter address.
Add adapters to an existing or imported stack:
tmplr-soroban-vault deploy adapters \
--vault CVAULT... \
--governance CGOVERNANCE... \
--asset-token CASSET... \
--blend-pool CBLENDPOOL... \
--custodian GCUSTODIAN...
Then submit the policy in order:
tmplr-soroban-vault governance submit-set-allowed-adapters \
--admin GCURATOR_OR_MULTISIG... \
--adapters CBLENDADAPTER...,CCUSTODIALADAPTER...
# After the allowed-adapters proposal is ready:
tmplr-soroban-vault governance accept-ready \
--admin GCURATOR_OR_MULTISIG... \
--kind allowed-adapters
tmplr-soroban-vault governance submit-set-cap \
--admin GCURATOR_OR_MULTISIG... \
--market-id 0 \
--cap 1000000000
# After the cap proposal is ready, verify and accept its exact ID:
tmplr-soroban-vault governance explain \
--proposal-id CAP_MARKET_0_PROPOSAL_ID
tmplr-soroban-vault governance accept \
--admin GCURATOR_OR_MULTISIG... \
--proposal-id CAP_MARKET_0_PROPOSAL_ID
tmplr-soroban-vault governance submit-set-supply-queue \
--admin GCURATOR_OR_MULTISIG... \
--entry 0:CBLENDADAPTER... \
--entry 1:CCUSTODIALADAPTER...
# After the supply-queue proposal is ready:
tmplr-soroban-vault governance accept-ready \
--admin GCURATOR_OR_MULTISIG... \
--kind supply-queue
Each --entry is market_id:adapter_address. Market IDs are stable identities,
not queue positions:
- Reordering the queue does not remap a market to another adapter.
- An existing market ID cannot be rebound to a different adapter.
- Supply requires the bound adapter to remain allowed.
- Withdrawal keeps using the stored binding, so liquidity can be recovered after an adapter is removed from new supply.
- Queue entries must be unique, enabled markets with nonzero caps. Practical queue size is also bounded by Soroban transaction resource limits.
Adapter trust boundaries
- Blend adapter — queries and operates a configured Blend pool on Stellar.
- Custodial adapter — forwards assets to a configured custodian or multisig. The off-chain route, custody controls, NAV reporting, and liquidity-return procedure are part of the vault's trust boundary.
A custodial withdrawal only releases assets already returned to the adapter on Stellar; it does not initiate or prove an external unwind. Reported NAV updates must match both the current stored amount and the exact next nonce:
stellar contract invoke \
--id "$CUSTODIAL_ADAPTER_ID" \
--source-account custodian \
-- set_reported_assets \
--caller GCUSTODIAN... \
--asset CASSET... \
--expected_current 800000000 \
--amount 1000000000 \
--report_nonce 42
Before using a custodial route, document signer recovery, NAV cadence, report approval, reconciliation, and delayed-liquidity procedures.
Day-to-day allocation and accounting
Routine allocation commands use a positive amount and a stable market ID. The allocator does not choose an adapter at execution time.
tmplr-soroban-vault curator refresh-markets \
--caller GALLOCATOR... \
--markets 0,1
tmplr-soroban-vault curator allocate-supply \
--caller GALLOCATOR... \
--market 0 \
--amount 100 \
--asset-decimals 7
tmplr-soroban-vault curator allocate-withdraw \
--caller GALLOCATOR... \
--market 0 \
--amount 25 \
--asset-decimals 7
Decimal flags are converted without floating point. Automation can use
--amount-raw, --assets-raw, or --shares-raw for exact base units.
The accounting behavior differs by direction:
allocate-supplytransfers assets to the bound adapter, calls its supply method, readstotal_assets(asset), and stores the observed route NAV.allocate-withdrawrequests an amount from the adapter, verifies the actual vault token-balance delta matches the adapter's return value, and subtracts the realized amount. It does not refresh the adapter's remaining NAV.refresh-marketsreadstotal_assets(asset)for the selected routes and replaces their stored principals. Run it after yield, loss, or a custodial NAV report and before fee/share-rate decisions.
Two maintenance calls are permissionless even though they are grouped under
curator in the CLI:
tmplr-soroban-vault curator resync-idle
tmplr-soroban-vault curator refresh-fees
resync-idle requires the vault to be idle and is rate-limited by the idle
resync cooldown, which defaults to 120 seconds. refresh-fees reconciles the
live idle balance and advances the fee checkpoint. Neither call substitutes for
refresh-markets when adapter NAV has changed.
Withdrawal operations
The Stellar vault has two distinct exit paths.
Atomic idle-liquidity exit
user atomic-withdraw and user atomic-redeem use the ERC-4626 proxy's slippage-protected atomic
exit methods when the deployment manifest contains proxy_4626. The CLI first verifies that the
recorded proxy interface exposes both atomic entrypoints; legacy proxies must be replaced rather
than bypassed. Proxy-less imported deployments fall back to the vault's equivalent atomic commands.
Both routes complete in one transaction only when the vault has enough idle assets. They never pull
liquidity from an adapter. As a result, maxWithdraw and maxRedeem can be zero
while the user's shares still represent assets deployed to markets.
tmplr-soroban-vault user preview --owner GUSER...
tmplr-soroban-vault user atomic-withdraw \
--operator GUSER... \
--assets 25 \
--asset-decimals 7 \
--max-shares-burned 25 \
--share-decimals manifest
Queued withdrawal
Use the queued path when idle liquidity is insufficient:
The proxy-facing user withdraw and user redeem commands preserve its asynchronous
ERC-7540-style compatibility methods. Use request-withdraw for the lower-level vault request
surface with explicit share and minimum-asset inputs.
tmplr-soroban-vault user request-withdraw \
--owner GUSER... \
--shares 10 \
--share-decimals manifest \
--min-assets-out 9.9 \
--asset-decimals 7
# After cooldown, recall enough market liquidity if needed.
tmplr-soroban-vault curator allocate-withdraw \
--caller GALLOCATOR... \
--market 0 \
--amount 10 \
--asset-decimals 7
# The operator is an authorized allocator/curator, not the withdrawing user.
tmplr-soroban-vault user execute-withdraw \
--operator GALLOCATOR...
The queued path has these mechanics:
request-withdrawescrows shares and records a fixed asset claim at request time. The default cooldown is one hour.execute-withdrawservices the queue head; it does not select a request ID.- The caller must have allocator authority. The command is under
userbecause it completes a user flow, not because any user may execute it. - The head request must be cooled down and fully covered by idle assets. There is no partial payout.
- Finishing an allocation does not automatically progress the withdrawal queue;
call
execute-withdrawseparately. - The queue does not reserve idle assets against later atomic exits. Curators must monitor queued claims and maintain enough idle liquidity.
- There is no user cancellation path. Monitor request and payout events by request ID and alert on stalled heads.
If execution is already stuck in Withdrawing, an allocator, Sentinel, or
curator may abort the exact active operation:
tmplr-soroban-vault curator abort-withdrawing \
--caller GALLOCATOR_OR_SENTINEL... \
--op-id 42
This is an incident-recovery action, not a normal withdrawal tool. A successful
abort validates the active operation ID, restores collected idle accounting,
refunds escrowed shares, removes the affected queue head, emits a
WithdrawalStopped event, and returns the vault to Idle.
TTL and archival operations
Soroban contract data is not permanent. Every vault needs an automated TTL job; ordinary transaction traffic is not a substitute for a keeper schedule.
tmplr-soroban-vault extend-ttl
The CLI attempts the vault runtime, governance, ERC-4626 proxy, curator proxy, share token, and every adapter recorded in the manifest. The asset token has no deployment-wide TTL entrypoint and is reported as skipped. Treat a failed or unexpectedly skipped component as an operational alert.
Vault, governance, proxy, and custodial-adapter maintenance uses permissionless
contract entrypoints. Share-token and Blend-adapter TTL entrypoints are
admin-gated, so the aggregate command does not invoke them. It instead uses
Stellar protocol-level operations to extend each contract instance and its WASM
code. The configured source account signs and pays for those operations; no
vault or governance contract authorization is required. The legacy --caller
option remains accepted for backward compatibility but is ignored.
Each contract owns its own TTL. Extending the vault runtime does not extend
governance, proxies, share-token holder entries, adapter storage, or oracle
storage. Run the aggregate command before archival: an extend operation cannot
revive an archived entry. If a contract is already archived, restore both its
instance with stellar contract restore --id ... and its WASM code with
stellar contract restore --wasm-hash ... before rerunning extend-ttl.
Contract-specific persistent entries may require separate restore or renewal
operations.
Safety and automation
- Use
--dry-runto print redacted Stellar commands and manifest decisions without writes. - Every contract write is simulated before submission. Review auth, footprint, resource, fee, and contract-error output on stderr.
- Use
--jsonfor stable machine-readable responses or--json-linesfor long-running automation. The schema lives attools/soroban-vault-cli/schema/output.schema.json. - Mainnet writes require
--allow-mainnet-write. - Dangerous governance submissions print an old/new semantic diff and require
--yesor interactive confirmation. - Successful writes append transaction metadata to the manifest. Preserve that audit trail alongside external monitoring and event indexing.
- Run
reconcileafter interrupted deployment, unexpected RPC results, or any manual contract operation that may have diverged from the manifest. - Generate operator completions or a manpage with
completionsandmanrather than copying stale command snippets into private runbooks.
Before a policy or allocation change, verify the manifest/network, refresh any changed adapter NAV, inspect the current proposal queue, and identify the exact role that must authorize the action. Afterward, verify the transaction result, runtime state, adapter accounting, relevant events, and the manifest audit record.
Monitoring and Risk Management
Templar Protocol uses available tools and established practices for monitoring protocol health and managing risks.
Protocol Monitoring Tools
Available Monitoring
Bot Infrastructure
The protocol includes operational bots for automated tasks:
-
Liquidation Bot: Monitors positions and executes liquidations
- Configurable intervals and concurrency
- Market registry monitoring
- Oracle price feed integration
- Automated liquidation execution
-
Accumulator Bot: Handles interest accumulation
- Periodic interest calculations
- Multi-market support
- Configurable execution parameters
Gas Usage Monitoring
Gas analysis tools provide performance insights:
./script/gas-report.sh
This generates detailed reports on:
- Function execution costs
- Snapshot iteration limits
- Performance bottlenecks
Manual Monitoring Procedures
Protocol Health Checks
Regular checks can be performed using:
-
Market Status: Query market configurations and states
# Get market configuration near contract call-function as-read-only <market-address> get_configuration json-args {} network-config mainnet now # Check current market snapshot near contract call-function as-read-only <market-address> get_current_snapshot json-args {} network-config mainnet now # Get borrow asset metrics near contract call-function as-read-only <market-address> get_borrow_asset_metrics json-args {} network-config mainnet now # List all deployed markets from registry near contract call-function as-read-only v1.tmplr.near list_deployments json-args '{"offset": 0, "count": 100}' network-config mainnet now -
Oracle Health: Verify price feed freshness and accuracy
# Check oracle prices near contract call-function as-read-only pyth-oracle.near get_price json-args '{"price_identifier": "<asset-price-id>"}' network-config mainnet now # Check LST oracle adapter near contract call-function as-read-only lst.oracle.tmplr.near get_price_data json-args '{}' network-config mainnet nowPrice Feed Status: Monitor price feed health at Pyth Network Price Feeds
Market Data Analysis
Using available view functions:
-
Supply Positions: Monitor individual and aggregate supply positions
# Get supply positions near contract call-function as-read-only <market-address> list_supply_positions json-args '{"offset": 0, "count": 100}' network-config mainnet now -
Withdrawal Queue: Check pending withdrawal requests
# Check withdrawal queue status near contract call-function as-read-only <market-address> get_supply_withdrawal_queue_status json-args {} network-config mainnet now -
Historical Snapshots: Analyze market history
# Get finalized snapshots for historical analysis near contract call-function as-read-only <market-address> list_finalized_snapshots json-args '{"offset": 0, "count": 10}' network-config mainnet now -
Total Value Locked: Monitor TVL at DefiLlama - Templar Protocol
-
Utilization Rate: Calculate from borrow asset metrics
# Get borrow asset metrics to calculate utilization (borrowed / available) near contract call-function as-read-only <market-address> get_borrow_asset_metrics json-args {} network-config mainnet now -
Current Interest Rate: Monitor current rate for supply positions
# Get current yield rate for suppliers near contract call-function as-read-only <market-address> get_last_yield_rate json-args {} network-config mainnet nowNote: Historical interest rate analysis requires an indexer for time-series data
Risk Management
Economic Risk Assessment
Available Analysis Tools
- Market Configuration Review: Analyze MCR ratios and interest rate models TODO: Get parameters from each market deployed
- Oracle Price Monitoring: Track price volatility and feed reliability
- Individual feeds: Pyth Network Price Feeds
- Overall status: Pyth Network Status
- Liquidation Efficiency: Monitor liquidation success rates TODO: Get data from liquidator
- Position Analysis: Assess individual and aggregate position health
- Individual positions: My Account
- Aggregate analysis: TODO: Create from contract data
Risk Mitigation Strategies
- Conservative Parameters: Well-tested collateralization ratios
- Oracle Integration: Multiple validation layers for price feeds
- Liquidation Incentives: Economic incentives for timely liquidations
- Interest Rate Models: Dynamic models responding to market conditions
Operational Monitoring
For operational monitoring procedures, refer to:
- Smart Contract Health: See Protocol Health Checks for contract monitoring procedures
- Gas Efficiency: See Gas Usage Monitoring for performance analysis tools
- Network Dependencies: Monitor external service health using the links below:
- NEAR Network Performance: NEAR Status
- Oracle Provider Status: Pyth Network Status
Criminal Activity Monitoring
SEVERE Account Labels
- SEVERE Account Detection: Telegram notification of SEVERE accounts interacting with Templar contracts
- Monitoring Repository: Details available at templar-monitoring
Frontend Security
Templar Protocol is committed to maintaining robust security practices across our frontend infrastructure to protect users and their assets. The application hosted at app.templarfi.org implements multiple layers of defense to ensure the integrity, availability, and trustworthiness of the interface through which users interact with Templar's smart contracts.
Hosting & DDoS Protection
The Templar frontend is deployed on Vercel's edge network, which provides built-in distributed denial-of-service (DDoS) mitigation at the infrastructure level. Vercel's global CDN automatically absorbs and filters volumetric attacks, rate-limits abusive traffic, and ensures high availability across geographically distributed edge nodes. This architecture provides resilience against Layer 3, Layer 4, and Layer 7 attack vectors without requiring additional proxy configurations.
DNS Security
The templarfi.org domain is managed with the following protections in place:
- Registrar lock is enabled to prevent unauthorized domain transfers or modifications.
- DNSSEC validation is supported through our DNS provider to ensure the authenticity of DNS responses and prevent cache poisoning attacks.
- DNS records are configured to resolve exclusively to Vercel's verified edge infrastructure, minimizing the risk of DNS hijacking or man-in-the-middle redirection.
- Access to DNS management is restricted to authorized personnel with multi-factor authentication (MFA) enforced on all accounts with domain-level permissions.
Frontend Integrity & Modification Detection
Templar employs several practices to detect and prevent unauthorized modifications to the frontend application:
- Immutable deployments: Each deployment on Vercel produces an immutable, content-addressed build artifact. Previous deployments can be instantly promoted or rolled back, ensuring that any unauthorized change can be quickly identified and reversed.
- Build verification: The frontend is built from a version-controlled source repository with branch protection rules. All production deployments originate from reviewed and approved code changes.
- Subresource Integrity (SRI): Where applicable, external resources loaded by the frontend use integrity hashes to ensure that third-party scripts and stylesheets have not been tampered with.
- Content Security Policy (CSP): HTTP security headers are configured to restrict the sources from which scripts, styles, and other resources can be loaded, mitigating cross-site scripting (XSS) and code injection risks.
Intrusion Detection & Monitoring
The Templar frontend leverages monitoring and alerting mechanisms to detect suspicious activity:
- Vercel's built-in analytics and logging provide visibility into traffic patterns, error rates, and deployment activity, enabling rapid detection of anomalous behavior.
- Automated alerts are configured for deployment failures, unusual traffic spikes, and error rate thresholds.
- Access control: Administrative access to the deployment platform and associated infrastructure accounts is restricted by role-based permissions and protected by multi-factor authentication.
Client-Side Security Best Practices
The frontend application follows industry-standard security practices:
- No private key handling: The frontend never requests, stores, or transmits private keys. All transaction signing is delegated to the user's connected wallet.
- Strict input validation: All user inputs are validated and sanitized on the client side before any contract interaction to prevent injection attacks and malformed transaction data.
- HTTPS enforced: All connections to app.templarfi.org are served exclusively over TLS, with HTTP Strict Transport Security (HSTS) headers enforced to prevent protocol downgrade attacks.
- Minimal third-party dependencies: The frontend minimizes its dependency surface area, and all packages are reviewed and version-pinned to reduce supply chain risk.
- Wallet interaction safety: Transaction parameters are constructed transparently, enabling users to verify contract calls and parameters in their wallet interface before signing.
Incident Response
In the event that a frontend compromise is detected or suspected, Templar's response protocol includes:
- Immediate rollback to the last known-good immutable deployment.
- Revocation and rotation of any compromised credentials or API keys.
- Communication to users via official channels (Twitter, Discord, and documentation site) advising them to verify the application URL and refrain from signing transactions until the all-clear is issued.
- Post-incident review and publication of findings where appropriate.
For questions or to report a security concern related to the Templar frontend, see Security Reporting.
Testing & Code Coverage
Test Execution
Run the complete local suite through the same entrypoints used by CI:
just test
Use just test-fast for the complete non-node gate, including non-node
integration targets, or just test-sandbox for the node-backed gate. The sandbox recipe prebuilds NEAR contracts before
starting its pooled neard instances; pass --stale to reuse the contracts already built into target/near.
Run the artifact drift check separately when validating the release catalog — pure in-memory invariant checks, no builds:
./script/check-artifact-drift.sh
Local Testing
Running Tests with Coverage
# Generate and open HTML coverage for the fast library-test cut
just coverage
# Generate coverage.lcov
just coverage-lcov
Test Categories
- Unit tests: Module-level functionality
- Integration tests: Cross-module interactions
- Contract tests: Smart contract behavior
- End-to-end tests: Full workflow validation
Performance Testing
Gas Usage Analysis
Gas usage analysis is available through existing tools:
./script/gas-report.sh
This generates a gas report for market operations, including average gas costs for individual operations and snapshot iteration limits.
Test-Gate Timing
The node gate is the slowest thing we run, so its cost is measured rather than guessed. Two tools:
# Time the harness primitives on a dedicated neard: block-latency floor,
# per-transaction and per-patch costs, fixture setup.
just bench-sandbox
# Compare two whole-suite runs. `just test-sandbox` writes per-test timings to
# target/nextest/sandbox/junit.xml (see [profile.sandbox.junit] in
# .config/nextest.toml); copy it aside before and after a change.
./script/bench/junit-diff.py before.xml after.xml
junit-diff.py totals are summed per-test durations. The gate runs several
tests concurrently, so those intervals overlap: the totals measure work done,
not gate wall clock. Its per-test breakdown is the point — a change that halves
the suite can still make one test much worse.
What the measurements established, so it need not be re-derived:
- Node round-trips dominate; WASM payload is ~free. 570KB of extra contract
code adds ~18ms to a deploy. Installing contract code via
sandbox_patch_stateworks but is not faster, and it forfeits batching deploy+init into one transaction. Same verdict rules out global contracts. Don't retry either. sandbox_patch_statecosts ~200ms per call, not per record. Minting N accounts in one patch therefore costs roughly what minting one does — hence the harness's batchedcreate_accounts.- Block production delay is the other lever, and it is local-only. See the sandbox cadence note below.
Sandbox Block Cadence
Locally the harness runs neard at a 40ms min_block_production_delay instead
of the stock 120ms, which roughly halves the node gate. CI pins the stock 120ms
(NEAR_SANDBOX_BLOCK_MS in .github/workflows/test.yml): a 4-vCPU runner cannot
sustain four nodes producing blocks 3× as often, and attempting it caused
widespread transaction-finality failures. Reducing parallelism to compensate was
measured and is slower than stock.
Override the cadence with NEAR_SANDBOX_BLOCK_MS=<ms>. max_block_production_delay
is compensated in the opposite direction so that avg(min, max) stays fixed at
310ms — nearcore credits a sandbox_fast_forward as
delta_height × avg(min, max), so holding that average keeps every
time-sensitive test's simulated time unchanged whatever the real cadence.
Because local blocks are faster than CI's, a test that leans on incidental
block cadence to cross a time boundary will pass in one place and fail in the
other. Advance chain time explicitly with fast_forward rather than relying on
how long some operations happen to take.
Glossary
This glossary provides definitions for key terms used throughout the Templar Protocol documentation and smart contracts.
A
APY (Annual Percentage Yield): The total return on an investment over one year. In Templar, this represents the effective yearly return for suppliers or the cost for borrowers.
Asset Pair: The combination of collateral asset and borrow asset that defines a market (e.g., BTC/USDC means Bitcoin collateral, USDC borrowing).
B
Borrow Asset: The token that users can borrow from the market. Typically a stablecoin like USDC, but can be any supported token.
Borrow Position: A user's borrowing account containing collateral deposits, borrowed amounts, accumulated interest, and current status.
Borrower: A user who deposits collateral and borrows assets from the market, paying interest on the borrowed amount.
C
Collateral Asset: The token deposited by borrowers to secure their loans. Must be worth more than the borrowed amount due to over-collateralization requirements.
Collateralization Ratio (CR): The ratio of collateral value to borrowed value. A 150% ratio means $150 of collateral backs $100 of debt.
Compounding: The process of reinvesting earned yield to generate additional returns over time.
D
Debt: The total amount owed by a borrower, including principal plus accumulated interest and fees.
E
EMA: Exponentially-weighted moving average. A time-series smoothing technique that favors recency.
F
FMV (Fair Market Value): The current market price of an asset as determined by oracle price feeds.
G
Gas: The computational cost for executing transactions on the NEAR blockchain.
H
Harvest Yield: The action of claiming accumulated yield from a supply position, which can then be withdrawn.
I
Interest Accumulation: The process of calculating and adding accrued interest to a borrower's total liability.
L
Lending: The general practice of providing assets to borrowers in exchange for interest payments. In Templar, suppliers lend to the market pool.
Liability: The total debt owed by a borrower, including principal, accumulated interest, and fees.
Liquidation: The forced sale of a borrower's collateral when their position becomes undercollateralized or expires.
Liquidator: A third party who performs liquidations by repaying part of a borrower's debt in exchange for discounted collateral.
Liquidator Spread: The discount liquidators receive when purchasing collateral, serving as incentive for providing the liquidation service.
Liquidity: The availability of assets in the market for borrowing or withdrawal. When liquidity is low, withdrawal requests may need to wait in the queue.
Liquidity Pool: The combined supply of assets deposited by all suppliers in a market, available for borrowers to access.
M
Market: A smart contract managing lending and borrowing for a specific asset pair (e.g., BTC/USDC market).
Maximum Usage Ratio: The maximum percentage of supplied assets that can be borrowed from a market, preventing over-utilization and maintaining liquidity reserves.
MCR (Minimum Collateralization Ratio): The minimum ratio of collateral value to borrowed value required to maintain a position. Different MCR levels trigger maintenance requirements or liquidation.
MCR Liquidation: The minimum collateralization ratio below which a position becomes eligible for liquidation.
MCR Maintenance: The minimum collateralization ratio required for new borrows or collateral withdrawals.
N
NEAR Intents: A NEAR Protocol feature that allows users to express desired outcomes (intents) that can be fulfilled by solvers, enabling more flexible and efficient transaction execution. See the official NEAR Intents documentation.
NEP-141: The NEAR Protocol standard for fungible tokens, similar to Ethereum's ERC-20. See the official NEP-141 specification.
NEP-245: The NEAR Protocol standard for multi-token contracts, similar to Ethereum's ERC-1155. See the official NEP-245 specification.
O
Oracle: A service providing real-time price data for assets, essential for calculating collateralization ratios and liquidations.
Origination Fee: A fee charged when creating a new borrow position, can be flat amount or percentage-based.
Over-collateralization: The requirement for borrowers to deposit collateral worth more than the borrowed amount, providing a safety buffer against price volatility.
P
Partial Liquidation: Liquidating only enough collateral to bring a position back to the maintenance MCR, rather than liquidating the entire position.
Principal: The original amount borrowed or supplied, excluding accumulated interest and fees.
Protocol Revenue: Fees collected by the protocol from borrowers and suppliers, distributed to suppliers and other accounts according to configured yield weights.
Pyth Network: A decentralized oracle network providing high-frequency price feeds for various assets.
R
Registry: A smart contract that manages deployment and versioning of market contracts within the Templar Protocol.
Repay: The action of returning borrowed assets plus interest to reduce or eliminate a borrower's debt.
S
Snapshot: A point-in-time record of market state including interest rates, asset amounts, and yield distribution.
Stablecoin: A cryptocurrency designed to maintain stable value, typically pegged to a fiat currency like USD. Commonly used as borrow assets in lending protocols.
Static Yield: A fixed allocation of market revenue to specific accounts, independent of their supply activity. Defined in the market's yield_weights configuration, static yield is distributed proportionally to designated accounts and can be withdrawn using the withdraw_static_yield function.
Supply: The total amount of assets deposited by suppliers that are available for borrowing in a market.
Supplier: A user who deposits assets into the market to earn yield from borrower interest payments.
Supply Withdrawal Fee: A fee charged when suppliers withdraw their assets from the market, configured per market to manage liquidity.
T
Time Chunk: A configurable time period (based on blocks, epochs, or timestamps) that determines when new snapshots are created.
Transfer Call: A token transfer that includes data, allowing the receiving contract to execute logic based on the transfer.
U
Undercollateralized: A borrow position where the collateral value falls below the required minimum ratio, making it eligible for liquidation.
Utilization Rate: The percentage of supplied assets currently borrowed. Calculated as: borrowed_amount / total_supplied_amount.
W
Withdrawal Queue: A first-in-first-out system for processing supply withdrawals when market liquidity is insufficient.
Y
Yield: The return earned by suppliers on their deposited assets, generated from borrower interest payments and fees.
Security Reporting
Templar Protocol takes security seriously and encourages responsible disclosure of security vulnerabilities.
All smart contracts are open-source and use reproducible builds for maximum transparency.
Bug Bounty
Templar has partnered with Immunefi to reward up to $100k for in-scope smart contract vulnerabilities.
To report smart contract vulnerabilities, please participate in our Immunefi bug bounty.
Security Contact
For security vulnerabilities and sensitive issues, please email security@templarprotocol.com.
Security Alerts
Important security notices will be posted on the official Discord server, Telegram channel, and X (Twitter) account.
Audit Information
Audit reports are available on GitHub.
Responsible Disclosure
If you have discovered a security issue, please follow these steps:
- Report: Send vulnerability details to Immunefi bug bounty for smart contract vulnerabilities or security@templarprotocol.com for other security issues.
- Investigation: Security team will assess the report.
- Resolution: Fix development and deployment.
- Public Disclosure: Coordinated disclosure after fix.
Security reports should include:
- A clear description of the vulnerability.
- Steps to reproduce the issue.
Notes
Sending funds to the market contract
When sending funds to a contract, you must call the asset contract's *_transfer_call function with the market as the receiver_id. So, for a token contract that implements the NEP-141 (Fungible Token) standard, you must call ft_transfer_call, specifying the market account ID as the receiver_id argument. For a token contract that implements the NEP-245 (Multi Token) standard, you must call mt_transfer_call.
If the funds are not sent using a *_transfer_call function, the contract will not be able to respond to the transfer: the funds will not be tracked by the contract, they will not be added to the supply, and the funds cannot be returned or withdrawn.
Contract interaction syntax
Contract interactions will be shown using near-cli-rs syntax. It can be installed via:
cargo install near-cli-rs
Example
near contract call-function as-transaction \
ibtc-usdc.v1.tmplr.near borrow \
json-args '{ "amount": "1000" }' \
prepaid-gas '100.0 Tgas' \
attached-deposit '0 NEAR' \
sign-as account.near \
network-config mainnet \
sign-with-keychain \
send
This command calls the function borrow on the contract ibtc-usdc.v1.tmplr.near with the arguments payload:
{
"amount": "1000"
}
Large numbers are serialized as strings instead of numerical literals to ensure that the precision limitations of JSON parsers do not affect the values. (See "Notes on Serialization" on docs.near.org.)
The command attaches 100 teragas units and 0 NEAR to the call, signs the transaction as account.near using a key saved to the local keychain, and sends the transaction to NEAR mainnet.
Please refer to the near-cli-rs user guide for more details.