Skip to main content

Overview

The CDP Manager is a per-user position manager wrapping Nostra’s lending protocol. Users can:
  • Deposit WBTC as collateral
  • Borrow USDC against collateral
  • Manage health factor to avoid liquidation
  • Close positions atomically (repay debt + withdraw collateral)
NOT an ERC-4626 vault. Each user manages their own isolated position. No yield aggregation.

Architecture

User WBTC → CDP Manager → Nostra Collateral Token (iWBTC-c)

                         Nostra Core

User ← USDC ← CDP Manager ← Nostra Debt Token (dUSDC)

Core Functions

deposit_and_borrow

Deposit WBTC as collateral and/or borrow USDC.
fn deposit_and_borrow(
    ref self: ContractState,
    wbtc_amount: u256,
    usdc_borrow_amount: u256,
)
wbtc_amount
u256
required
WBTC to deposit as collateral (8 decimals)
usdc_borrow_amount
u256
required
USDC to borrow (6 decimals)
  1. Transfer WBTC from caller to CDP contract
  2. Approve Nostra collateral token (iWBTC-c) to pull WBTC
  3. Call iWBTC-c.deposit() — mints collateral token to CDP
  4. If usdc_borrow_amount > 0: call dUSDC.borrow() — mints debt and sends USDC to CDP
  5. Transfer borrowed USDC to caller
  6. Update user tracking (deposited, borrowed, user_count)
  7. Update global stats (total_wbtc_collateral, total_usdc_debt)
  8. Emit DepositAndBorrow event
You can deposit without borrowing (usdc_borrow_amount = 0) or borrow additional USDC against existing collateral.

repay_and_withdraw

Repay USDC debt and/or withdraw WBTC collateral.
fn repay_and_withdraw(
    ref self: ContractState,
    usdc_repay_amount: u256,
    wbtc_withdraw_amount: u256,
)
usdc_repay_amount
u256
required
USDC to repay (6 decimals)
wbtc_withdraw_amount
u256
required
WBTC to withdraw (8 decimals)
  1. If repaying: transfer USDC from caller to CDP
  2. Approve dUSDC to pull USDC for repayment
  3. Call dUSDC.repay() — burns debt token
  4. If withdrawing: call iWBTC-c.withdraw() — burns collateral token, returns WBTC to CDP
  5. Transfer WBTC to caller
  6. Update user and global tracking
  7. Emit RepayAndWithdraw event
Withdrawing collateral while maintaining debt increases liquidation risk. Monitor health factor via get_position().

close_position

Close position entirely: repay ALL debt and withdraw ALL collateral.
fn close_position(ref self: ContractState)
  1. Read exact dUSDC balance from Nostra debt token (atomically captures accrued interest)
  2. Transfer exact debt amount in USDC from caller to CDP
  3. Approve and repay debt via dUSDC.repay()
  4. Withdraw all collateral via iWBTC-c.withdraw()
  5. Transfer all WBTC to caller
  6. Clear user tracking (deposited = 0, borrowed = 0)
  7. Update global stats
close_position() atomically reads the exact debt balance at execution time, preventing dust/rounding errors from accrued interest.

View Functions

get_position

Get user’s position: collateral, debt, and health factor.
fn get_position(self: @ContractState, user: ContractAddress) -> (u256, u256, u256)
Returns:
  • wbtc_collateral (u256): Total WBTC deposited (8 decimals)
  • usdc_debt (u256): Total USDC borrowed (6 decimals)
  • health_factor_bps (u256): Health factor in basis points (10000 = 1.0x)
Health Factor Calculation:
wbtc_value_usd = wbtc_collateral * wbtc_price / 10^8
usdc_value_usd = usdc_debt / 10^6
health_factor_bps = (wbtc_value_usd * 10000) / usdc_value_usd
Health factor > 10000 (1.0x) is safe. Nostra liquidates positions when health factor drops below protocol threshold (typically ~1.0x).

get_max_borrow

Get max additional USDC borrowable at 70% LTV.
fn get_max_borrow(self: @ContractState, user: ContractAddress) -> u256
Returns: Max additional USDC that can be borrowed without exceeding 70% LTV.
max_borrow_usdc = wbtc_collateral * wbtc_price * 0.7 / 10^10 - existing_debt
70% LTV is a safe target, not Nostra’s liquidation threshold. Actual liquidation occurs closer to 100% LTV (protocol-specific).

Global Stats

fn total_collateral(self: @ContractState) -> u256  // Total WBTC across all users
fn total_debt(self: @ContractState) -> u256        // Total USDC debt across all users
fn user_count(self: @ContractState) -> u32         // Number of unique users

Storage

wbtc: ContractAddress
usdc: ContractAddress
nostra_collateral_token: ContractAddress  // iWBTC-c
nostra_debt_token: ContractAddress        // dUSDC
pragma_oracle: ContractAddress
owner: ContractAddress
pending_owner: ContractAddress

// Per-user tracking
user_wbtc_deposited: Map<ContractAddress, u256>
user_usdc_borrowed: Map<ContractAddress, u256>

// Global stats
total_wbtc_collateral: u256
total_usdc_debt: u256
user_count: u32
user_exists: Map<ContractAddress, bool>
is_paused: bool
User tracking is for display purposes only. Actual collateral and debt are tracked by Nostra’s iWBTC-c and dUSDC tokens.

Events

DepositAndBorrow

pub struct DepositAndBorrow {
    pub user: ContractAddress,  // [key]
    pub wbtc_deposited: u256,
    pub usdc_borrowed: u256,
}

RepayAndWithdraw

pub struct RepayAndWithdraw {
    pub user: ContractAddress,  // [key]
    pub usdc_repaid: u256,
    pub wbtc_withdrawn: u256,
}

OwnershipTransferred

pub struct OwnershipTransferred {
    pub previous_owner: ContractAddress,
    pub new_owner: ContractAddress,
}

Configuration

Constructor

fn constructor(
    ref self: ContractState,
    wbtc: ContractAddress,
    usdc: ContractAddress,
    nostra_collateral_token: ContractAddress,  // iWBTC-c
    nostra_debt_token: ContractAddress,        // dUSDC
    pragma_oracle: ContractAddress,
    owner: ContractAddress,
)

Admin Functions

fn upgrade(ref self: ContractState, new_class_hash: ClassHash)
fn set_nostra_tokens(
    ref self: ContractState,
    collateral_token: ContractAddress,
    debt_token: ContractAddress,
)
fn set_pragma_oracle(ref self: ContractState, oracle: ContractAddress)
fn pause(ref self: ContractState)
fn unpause(ref self: ContractState)
fn transfer_ownership(ref self: ContractState, new_owner: ContractAddress)
fn accept_ownership(ref self: ContractState)

Nostra Integration

Collateral Token (iWBTC-c)

trait INostraCollateralToken {
    fn deposit(ref self: ContractState, user: ContractAddress, amount: u256);
    fn withdraw(ref self: ContractState, from: ContractAddress, to: ContractAddress, amount: u256);
    fn balance_of(self: @ContractState, user: ContractAddress) -> u256;
}

Debt Token (dUSDC)

trait INostraDebtToken {
    fn borrow(ref self: ContractState, user: ContractAddress, amount: u256);
    fn repay(ref self: ContractState, user: ContractAddress, amount: u256);
    fn balance_of(self: @ContractState, user: ContractAddress) -> u256;
}
Nostra uses rebasing tokens: iWBTC-c and dUSDC balances automatically increase to reflect accrued interest/debt.

Oracle Integration

WBTC price fetched from Pragma Oracle:
fn _get_wbtc_price(self: @ContractState) -> u128 {
    let oracle_addr = self.pragma_oracle.read();
    let oracle = IPragmaOracleDispatcher { contract_address: oracle_addr };
    let response = oracle.get_data_median(DataType::SpotEntry(WBTC_USD_PAIR));
    response.price  // 8 decimals
}
Pair ID: WBTC/USD (felt252: 0x574254432f555344)

Health Factor Examples

Safe Position (2.5x health)

WBTC Deposited: 0.1 BTC (10,000,000 sats)
WBTC Price: $60,000 (6000000000000 in 8-decimal scaled)
Collateral Value: $6,000

USDC Borrowed: 2,400 USDC (2400000000 in 6 decimals)
Debt Value: $2,400

Health Factor = (6000 * 10000) / 2400 = 25000 bps = 2.5x ✅

Risky Position (1.1x health)

WBTC Deposited: 0.1 BTC
WBTC Price: $60,000
Collateral Value: $6,000

USDC Borrowed: 5,400 USDC
Debt Value: $5,400

Health Factor = (6000 * 10000) / 5400 = 11111 bps = 1.11x ⚠️

Liquidation Risk (0.95x health)

WBTC Deposited: 0.1 BTC
WBTC Price: $60,000
Collateral Value: $6,000

USDC Borrowed: 6,300 USDC
Debt Value: $6,300

Health Factor = (6000 * 10000) / 6300 = 9524 bps = 0.95x 🔴
Nostra liquidates positions when health factor drops below 1.0x (protocol may vary). Always maintain health factor > 1.5x as safety buffer.

Integration Example

use btcvault::cdp::{ICDPDispatcher, ICDPDispatcherTrait};

// Deposit 0.1 WBTC and borrow 3000 USDC
let cdp = ICDPDispatcher { contract_address: cdp_addr };
let wbtc = IERC20Dispatcher { contract_address: WBTC };
let usdc = IERC20Dispatcher { contract_address: USDC };

// Approve CDP to spend WBTC
wbtc.approve(cdp_addr, 10_000_000);  // 0.1 WBTC (8 decimals)

// Deposit and borrow
cdp.deposit_and_borrow(
    wbtc_amount: 10_000_000,      // 0.1 WBTC
    usdc_borrow_amount: 3_000_000_000,  // 3000 USDC (6 decimals)
);

// Check position health
let (collateral, debt, health_bps) = cdp.get_position(user_addr);
assert(health_bps > 15000, 'Health too low');  // Require > 1.5x

// Repay 1000 USDC
usdc.approve(cdp_addr, 1_000_000_000);
cdp.repay_and_withdraw(
    usdc_repay_amount: 1_000_000_000,  // 1000 USDC
    wbtc_withdraw_amount: 0,           // Don't withdraw yet
);

// Close position entirely
let (_, exact_debt, _) = cdp.get_position(user_addr);
usdc.approve(cdp_addr, exact_debt);
cdp.close_position();  // Repays all debt, withdraws all collateral

Security Considerations

Users are responsible for monitoring health factor. CDP Manager does NOT auto-rebalance. If health < 1.0x, Nostra liquidators can seize collateral.
USDC debt grows over time due to Nostra’s borrow APR. Always check get_position() for exact debt before repaying.
Health factor relies on Pragma Oracle for WBTC price. If oracle fails, get_position() returns health = 0.
CDP Manager is a pass-through contract. Funds are held by Nostra, not the CDP contract. Owner CANNOT access user funds.

Nostra Liquidation Mechanics

Nostra uses a Dutch auction liquidation model:
  1. When health factor < 1.0x, position becomes eligible for liquidation
  2. Liquidators can repay user debt in exchange for discounted collateral
  3. Discount starts at 0% and increases over time (up to ~10%)
  4. First liquidator to execute wins the discount
To avoid liquidation, maintain health factor > 1.5x and monitor positions during volatile markets.

See Also

Build docs developers (and LLMs) love