Cho Markets Documentation
Overview
Cho Markets is an isolated lending protocol for Solana. Each market is a separate lending pool with its own loan token, collateral token, oracle, and risk parameters.
Program ID: 5LYM6PdjBQ9Fr6Y3PV5aFBfxVCncup41ZGEQ2q73tm4F
Name
The name Cho is a precise romanization of the Japanese cho, a high-utility signifier for transformation (蟼), transcendence (超), and the summit (頂).
Architecture
┌─────────────────────────────────────────────────────────┐ │ Cho Markets Program │ │ Program ID: 5LYM6PdjBQ9Fr6Y3PV5aFBfxVCncup41ZGEQ2q73tm4F │ ├─────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Market │ │ Market │ │ Market │ │ │ │ USDC/SOL │ │ USDC/PYUSD │ │ EURC/SOL │ │ │ │ LLTV: 80% │ │ LLTV: 90% │ │ LLTV: 75% │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Position │ │ Position │ │ Position │ │ │ │ (per user) │ │ (per user) │ │ (per user) │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘
Accounts
Market
PDA: ["market", loan_token, collateral_token, collateral_adapter, loan_adapter, irm_id, lltv, nonce]
| Field | Type | Description |
|---|---|---|
| nonce | u64 | PDA uniqueness nonce (default 0, increment for new markets) |
| loan_token | Pubkey | Loan token mint |
| collateral_token | Pubkey | Collateral token mint |
| collateral_oracle | MarketOracleConfig | Oracle config for collateral price |
| loan_oracle | MarketOracleConfig | Oracle config for loan price |
| oracle_risk_params | OracleRiskParams | Staleness, confidence, price bounds |
| irm_id | Pubkey | IRM identifier (builtin PDA or IrmConfig PDA) |
| irm_cache_type | u8 | IRM cache type (0-2 builtin, 255 custom) |
| lltv | u64 | Liquidation LTV in bps (max 9800) |
| total_supply_assets | u128 | Total supplied assets |
| total_supply_shares | u128 | Total supply shares |
| total_borrow_assets | u128 | Total borrowed assets |
| total_borrow_shares | u128 | Total borrow shares |
| total_collateral | u128 | Total collateral deposited |
| last_update | i64 | Unix timestamp of last accrual |
| fee | u64 | Protocol fee in bps (max 2500) |
| rate_at_target | i128 | Current rate for AdaptiveCurve IRM |
| fee_shares | u128 | Unclaimed protocol fee shares |
| health_factor_buffer_wad | u64 | Buffer for liquidation threshold (default 0) |
| bad_debt | u128 | Accumulated bad debt |
| loan_vault | Pubkey | Token account holding loan assets |
| collateral_vault | Pubkey | Token account holding collateral assets |
| fee_authority | Pubkey | Authority that can update market fee |
| bump | u8 | PDA bump seed |
Position
PDA: ["position", market, user]
| Field | Type | Description |
|---|---|---|
| market | Pubkey | Market this position belongs to |
| user | Pubkey | Position owner |
| supply_shares | u128 | User's supply shares |
| borrow_shares | u128 | User's borrow shares |
| collateral | u128 | Collateral deposited |
| last_update | i64 | Last interaction timestamp |
| bump | u8 | PDA bump seed |
ProtocolConfig
PDA: ["protocol_config"]
Governance-controlled allowlists for LLTV values and IRM IDs.
IrmRegistry
PDA: ["irm_registry"]
Governance-controlled registry tracking IrmConfig accounts and enabled IRM IDs.
| Field | Type | Description |
|---|---|---|
| next_index | u64 | Next IrmConfig index to use |
| enabled_irm_ids | [Pubkey; 64] | Allowlist of enabled IRM IDs |
| enabled_irm_count | u8 | Number of enabled IRMs |
IrmConfig
PDA: ["irm_config", index_le_bytes]
Parametric interest rate model configuration for custom IRMs.
| Field | Type | Description |
|---|---|---|
| index | u64 | Sequential config index |
| curve_type | u8 | 0=Linear, 1=Kinked, 2=Adaptive |
| base_rate_wad | u128 | Base rate at 0% utilization (WAD) |
| slope1_wad | u128 | Slope below kink (WAD) |
| slope2_wad | u128 | Slope above kink (WAD) |
| kink_utilization_wad | u128 | Kink point (0 to WAD) |
| min_rate_wad / max_rate_wad | i128 | Rate bounds |
Liquidation Incentive
Cho Markets uses an LLTV-dependent liquidation incentive formula. Higher LLTV markets have lower liquidation bonuses to protect high-leverage positions.
Formula
incentive = min(1.15, 1 / (1 - 0.3 * (1 - LLTV)))
Incentive by LLTV
| LLTV | Liquidator Bonus |
|---|---|
| 50% | 15.0% (capped) |
| 75% | ~11.4% |
| 80% | ~6.4% |
| 90% | ~3.1% |
| 98% | ~0.6% |
This model protects high-LLTV markets from excessive liquidation profits while encouraging liquidations in safer markets.
Instructions
User Operations
| Instruction | Args | remaining_accounts | Description |
|---|---|---|---|
| supply | assets: u64 | [IrmConfig?] | Supply loan tokens to earn interest |
| withdraw | shares: u128 | [IrmConfig?] | Withdraw supplied assets |
| deposit_collateral | amount: u64 | - | Deposit collateral for borrowing |
| withdraw_collateral | amount: u64 | [coll_feed, loan_feed, IrmConfig?] | Withdraw collateral (if healthy) |
| borrow | assets: u64 | [coll_feed, loan_feed, IrmConfig?] | Borrow against collateral |
| repay | shares: u128 | [IrmConfig?] | Repay by specifying shares |
| repay_assets | assets: u64 | [IrmConfig?] | Repay by specifying asset amount |
| liquidate | repay_assets: u64 | [coll_feed, loan_feed, IrmConfig?] | Liquidate unhealthy position |
| accrue_interest | (none) | [IrmConfig?] | Manually trigger interest accrual on market |
Note: IrmConfig is only required in remaining_accounts when the market uses a custom IRM (irm_cache_type = 255). For builtin IRMs (types 0-2), the IrmConfig account is not needed.
Admin/Governance
| Instruction | Description |
|---|---|
| create_market | Create new isolated market |
| initialize_protocol_config | Initialize governance config |
| submit_enable_lltv | Queue new LLTV (timelocked) |
| accept_enable_lltv | Accept queued LLTV after timelock |
| submit_enable_irm | Queue new IRM type (timelocked) |
| accept_enable_irm | Accept queued IRM after timelock |
| update_market_fee | Update market protocol fee |
| update_oracle_risk_params | Update oracle staleness/confidence |
| claim_fees | Claim accumulated protocol fees |
| revoke_enable_lltv | Cancel pending LLTV timelock |
| revoke_enable_irm | Cancel pending IRM timelock |
| set_fee_recipient | Update fee recipient address |
| set_fee_authority | Update fee authority address |
| set_default_market_fee | Set protocol default fee for new markets |
| submit_transfer_authority | Submit authority transfer (timelocked) |
| accept_transfer_authority | Accept authority transfer after timelock |
| revoke_transfer_authority | Cancel pending authority transfer |
Interest Rate Models (IRM)
IRM Types
IRMs are identified by their irm_id (Pubkey). Builtin IRMs use canonical PDAs, while custom IRMs use IrmConfig account PDAs.
| Type | Cache Type | Description |
|---|---|---|
| None (builtin) | 0 | No interest accrual (0%) |
| Linear (builtin) | 1 | Fixed rate from rate_at_target field |
| AdaptiveCurve (builtin) | 2 | Dynamic rate that adjusts based on utilization vs 90% target |
| Custom (IrmConfig) | 255 | Parametric IRM loaded from IrmConfig account |
Custom IRM (Type 255)
When a market uses a custom IRM (irm_cache_type = 255), the IrmConfig account must be passed in remaining_accounts for instructions that accrue interest:
- supply, withdraw, repay, accrueInterest: remaining_accounts[0] = IrmConfig
- borrow, withdrawCollateral, liquidate: remaining_accounts[2] = IrmConfig (after oracle feeds)
For builtin IRMs (types 0-2), no IrmConfig account is needed in remaining_accounts.
AdaptiveCurve Details
Target utilization: 90% Rate bounds: 0.1% APR (min) to 200% APR (max) Initial rate_at_target: 4% APR Two-slope formula: • Below 90%: APR = 1% + (utilization × 4% / 90%) - At 0% util: 1% APR - At 90% util: 5% APR • Above 90%: APR = 5% + ((utilization - 90%) × 10) - At 100% util: 105% APR Rate adjustment: Exponential based on utilization deviation If util > 90%: rate increases over time If util < 90%: rate decreases over time
APR vs APY
The UI displays APY (Annual Percentage Yield) using continuous compounding:
APY = e^APR - 1 Example: 5% APR = 5.127% APY
Oracle System
Cho Markets uses a modular oracle adapter system supporting multiple price feed providers.
Supported Adapters
| Adapter | Program ID | Description |
|---|---|---|
| Mock | choMock... | Testing/devnet mock prices |
| Pyth | choPyth... | Pyth Network price feeds (in development) |
Oracle Risk Parameters
| Parameter | Description |
|---|---|
| max_staleness_slots | Max slots since last update (0 = disabled) |
| max_confidence_bps | Max confidence interval width (0 = disabled) |
| min_price | Minimum acceptable price (0 = disabled) |
| max_price | Maximum acceptable price (0 = disabled) |
Shares Math
Assets to Shares conversion uses virtual shares (1e6) and virtual assets (1) to prevent first-depositor manipulation:
// Constants VIRTUAL_SHARES = 1_000_000 // 1e6 VIRTUAL_ASSETS = 1 // To shares (round down for supply, round up for borrow) shares = (assets * (total_shares + VIRTUAL_SHARES)) / (total_assets + VIRTUAL_ASSETS) // To assets (round up for withdraw, round down for repay) assets = (shares * (total_assets + VIRTUAL_ASSETS)) / (total_shares + VIRTUAL_SHARES)
Health Factor
Position health is calculated as:
health_factor = (collateral_value * LLTV) / borrow_value // Position is liquidatable when: health_factor < (1.0 - health_factor_buffer) // Default buffer = 0 (exact 1.0 threshold)
Protocol Fees
Protocol fees are collected as a percentage of interest accrued. Fee shares are minted during interest accrual and can be claimed by the fee authority.
// Fee calculation during interest accrual: fee_amount = interest * fee_bps / 10000 // Fee shares minted: fee_shares = fee_amount * total_shares / (total_assets - fee_amount) // Claiming: fee_authority calls claim_fees to convert shares to assets
User Journeys
1. Lender: Earn Interest on USDC
- Connect wallet with USDC balance
- Go to User tab, select a USDC/SOL market
- Enter amount in "Supply USDC" field
- Click Supply - you receive supply shares
- As borrowers pay interest, your shares become worth more assets
- To exit: enter shares in Withdraw field, click Withdraw
2. Borrower: Borrow USDC Against SOL
- Connect wallet with SOL balance
- Go to User tab, select USDC/SOL market
- Deposit SOL as collateral (will be wrapped to wSOL)
- Borrow USDC up to LLTV ratio (e.g., 80% means 0.8 USDC per $1 SOL)
- Use borrowed USDC as needed
- To close: Repay borrowed USDC + interest, then withdraw collateral
3. Liquidator: Liquidate Unhealthy Positions
- Monitor positions for health_factor < 1.0
- When position is liquidatable, call liquidate instruction
- Repay borrower's debt (100%)
- Receive collateral + LLTV-dependent incentive bonus
- Profit = collateral received - debt repaid
4. Admin: Create New Market
- Initialize protocol config (one-time)
- Enable LLTV value via timelock (submit, wait, accept)
- Enable IRM type via timelock (submit, wait, accept)
- Go to Admin tab, select loan/collateral tokens
- Set LLTV, fee, IRM, and oracle parameters
- Click Create Market - market is live for users
Example: Complete Borrow Flow
Market: USDC/SOL, LLTV: 80%, SOL price: $100 1. User deposits 1 SOL as collateral → collateral_value = 1 SOL × $100 = $100 2. Max borrow = collateral_value × LLTV → max_borrow = $100 × 0.80 = $80 USDC 3. User borrows 50 USDC (conservative) → health_factor = ($100 × 0.80) / $50 = 1.6 ✓ Healthy 4. If SOL drops to $60: → health_factor = ($60 × 0.80) / $50 = 0.96 ✗ Liquidatable 5. Liquidator repays full 50 USDC debt (100% close factor) → At 80% LLTV, ~6.4% bonus → Collateral seized: $50 × 1.064 = $53.20 worth → Liquidator receives: 0.887 SOL ($53.20 / $60) → Borrower keeps: 0.113 SOL ($6.80), debt cleared
Bad Debt Handling
Bad debt occurs when liquidation leaves remaining debt with zero collateral. It is socialized atomically during liquidation (no separate instruction) to prevent front-running. Losses are distributed proportionally across all lenders by reducing total_supply_assets.
// Bad debt socialization (automatic during liquidation):
if (position.collateral == 0 && position.borrow_shares > 0):
bad_debt_amount = to_assets(position.borrow_shares)
market.bad_debt += bad_debt_amount
market.total_supply_assets -= bad_debt_amount // Lenders absorb loss
market.total_borrow_assets -= bad_debt_amount
position.borrow_shares = 0Events
- MarketCreated - New market deployed
- SupplyEvent - User supplies assets
- WithdrawEvent - User withdraws assets
- DepositCollateralEvent - User deposits collateral
- WithdrawCollateralEvent - User withdraws collateral
- BorrowEvent - User borrows assets
- RepayEvent - User repays debt
- LiquidateEvent - Position liquidated
- AccrueInterestEvent - Interest accrued
- BadDebtSocialized - Bad debt distributed to lenders
- FeesClaimed - Protocol fees claimed
- MarketFeeUpdated - Market fee changed
- OracleRiskParamsUpdated - Oracle params changed
Constants
| Constant | Value | Description |
|---|---|---|
| MAX_LLTV_BP | 9800 | Maximum LLTV (98%) |
| MAX_FEE_BP | 2500 | Maximum protocol fee (25%) |
| MAX_LIQUIDATION_INCENTIVE_FACTOR | 1.15 | Max LLTV-dependent bonus (15%) |
| TARGET_UTILIZATION | 90% | AdaptiveCurve target |
| MIN_RATE_AT_TARGET | 0.1% APR | Minimum rate bound |
| MAX_RATE_AT_TARGET | 200% APR | Maximum rate bound |
Devnet Tokens
| Token | Mint | Decimals | Price |
|---|---|---|---|
| SOL | So111...1112 | 9 | $100 |
| USDC | 4zMMC...ncDU | 6 | $1 |
| PYUSD | CXk2A...ynM | 6 | $1 |
| EURC | Hzwqb...Ktr | 6 | $1.08 |
Devnet Faucets
Get test tokens for devnet:
| Token | Faucet |
|---|---|
| SOL | faucet.solana.com |
| PYUSD | Google Cloud PYUSD Faucet |
| USDC | faucet.circle.com |
| EURC | faucet.circle.com |