Protocol Overview

Risk Engine

How Project 0's on-chain risk engine evaluates account health and protects protocol solvency.

Project 0 uses a deterministic, fully on-chain risk engine that evaluates every transaction in real-time. The risk engine ensures that borrowing and lending activities remain within acceptable parameters, protecting the protocol's solvency and lenders' deposits.

How Health Is Calculated

Account health is calculated as the weighted sum of assets minus the weighted sum of liabilities:

Health = Sum(Asset Value * Asset Weight) - Sum(Liability Value * Liability Weight)

Where asset values are priced using the lower bound of the oracle confidence interval (conservative for collateral), and liability values are priced using the upper bound (conservative for debt).

Two Health Checks

The risk engine performs two kinds of health checks using different weight tiers:

CheckWeights UsedPurpose
InitialLower asset weights, higher liability weightsDetermines if new borrows are allowed
MaintenanceHigher asset weights, lower liability weightsDetermines if account can be liquidated

An Account cannot borrow when its health is below zero using Initial weights. An Account is subject to liquidation when its health is below zero using Maintenance weights. Liquidations cannot make an account healthy again: a liquidator can only bring a user's health up to zero at most.

Worked Example

Consider a user with 2 Token A (worth $10 each) as collateral and 5.05 USDC in debt:

Token A: price $10, confidence $0.212
         Initial Asset Weight: 50%, Maintenance Asset Weight: 10%
USDC:    price $1, confidence $0.0212
         Initial Liability Weight: 100%, Maintenance Liability Weight: 100%

Initial health (can I borrow more?):

Health = (2 * $9.788 * 0.5) - (5.05 * $1.0212 * 1.0)
       = $9.788 - $5.157
       = $4.631

This account is healthy with $4.63 in remaining borrowing power.

Maintenance health (can I be liquidated?):

Health = (2 * $9.788 * 0.1) - (5.05 * $1.0212 * 1.0)
       = $1.958 - $5.157
       = -$3.199

The account is unhealthy under Maintenance weights. It can be liquidated, even though the raw asset value ($20) exceeds the raw debt ($5.05). This is by design: weights create a safety buffer that protects lenders.

A partial liquidation can restore health:

Liquidator fee = 2.5%
Insurance fee = 2.5%

Liquidator seizes 0.2 Token A (worth $2) from the borrower's account.
Liquidator must pay (in USDC):
  value of A minus liquidator fee (low bias): 0.2 * (1 - 0.025) * 9.788 = $1.90866
Borrower receives (in USDC debt repayment):
  value of A minus (liquidator fee + insurance) (low bias): 0.2 * (1 - 0.025 - 0.025) * 9.788 = $1.85972

Health after: ((2 - 0.2) * 9.788 * 0.1) - ((5.05 - 1.85972) * 1.0212 * 1.0) = -1.496

The user's health improved, but they are still eligible to be partially liquidated.

Just-In-Time Evaluation

Unlike some protocols that require "refresh" instructions before risk-sensitive operations, P0's risk engine evaluates just in time. No refresh cranks are needed (except for those required by integrated venues like Kamino).

When a risk check is required, the caller passes the necessary Bank and Oracle accounts in the transaction's remaining accounts:

[bank0, oracle0_1],
[bank1, oracle1_1, oracle1_2],
[bank2, oracle2_1],

Banks and their oracles are passed in sorted bitwise order by Bank public key, matching the order they appear in the user's Balances. Some Banks have multiple oracle accounts (e.g., Kamino Banks require a price oracle and the Kamino reserve).

Each oracle must not be stale. For Pyth oracles, the caller typically has no obligations since Pyth oracles are kept up-to-date by their administrator. For Switchboard oracles, the caller must send a crank instruction just before the transaction consuming that price. Some callers prefer to use bundles for this, but it typically suffices to send a crank instruction and briefly wait. When transaction size permits, callers might even prepend the Switchboard crank to the transaction consuming the oracle data, although this runs into account and CU constraints for larger transactions. For Kamino Banks, refresh_reserve must also execute within the same slot.

When borrowing, the caller can pass stale oracles for collateral positions that are not needed to demonstrate sufficient health. For example, if a user has $1 in Token A and $1,000 in Token B, and wants to borrow $100 in Token C, a stale oracle for Token A is acceptable because Token B alone provides sufficient collateral.

When Risk Checks Run

OperationRisk Check Required
DepositNo (deposits always improve health)
RepayNo (repayments always improve health)
BorrowYes (must pass Initial health check)
WithdrawYes (must pass Initial health check)
LiquidationYes (account must fail Maintenance health check)

Checking Your Health

You can query the risk engine's real-time assessment of your account by calling lending_account_pulse_health. This instruction uses the same oracles and weights that would be used in any actual risk check, giving you an accurate snapshot of your account's health for borrowing, liquidation, and bankruptcy purposes.

On this page