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 to deposit as collateral (8 decimals)
USDC to borrow (6 decimals)
Transfer WBTC from caller to CDP contract
Approve Nostra collateral token (iWBTC-c) to pull WBTC
Call iWBTC-c.deposit() — mints collateral token to CDP
If usdc_borrow_amount > 0: call dUSDC.borrow() — mints debt and sends USDC to CDP
Transfer borrowed USDC to caller
Update user tracking (deposited, borrowed, user_count)
Update global stats (total_wbtc_collateral, total_usdc_debt)
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 to repay (6 decimals)
WBTC to withdraw (8 decimals)
If repaying: transfer USDC from caller to CDP
Approve dUSDC to pull USDC for repayment
Call dUSDC.repay() — burns debt token
If withdrawing: call iWBTC-c.withdraw() — burns collateral token, returns WBTC to CDP
Transfer WBTC to caller
Update user and global tracking
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)
Read exact dUSDC balance from Nostra debt token (atomically captures accrued interest)
Transfer exact debt amount in USDC from caller to CDP
Approve and repay debt via dUSDC.repay()
Withdraw all collateral via iWBTC-c.withdraw()
Transfer all WBTC to caller
Clear user tracking (deposited = 0, borrowed = 0)
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:
When health factor < 1.0x, position becomes eligible for liquidation
Liquidators can repay user debt in exchange for discounted collateral
Discount starts at 0% and increases over time (up to ~10%)
First liquidator to execute wins the discount
To avoid liquidation, maintain health factor > 1.5x and monitor positions during volatile markets.
See Also