Skip to main content

Overview

The Stablecoin Vault is an ERC-4626 vault for USDC that:
  • Deposits USDC into Vesu RE7_USDC_CORE pool as collateral
  • Earns supply APY (no debt, no leverage)
  • Auto-deploys idle USDC on deposit
  • Respects Vesu dust thresholds (dynamic min_deposit)
Same architecture as Sentinel vaults but for USDC stablecoins instead of WBTC.

Architecture

User USDC → Vault (yvUSDC-STAB) → Vesu RE7_USDC_CORE → Lending APY

           ERC-4626

        Share Minting

Core Functions (ERC-4626)

deposit

Deposit USDC and receive vault shares.
fn deposit(ref self: ContractState, assets: u256, receiver: ContractAddress) -> u256
assets
u256
required
USDC amount to deposit (6 decimals)
receiver
ContractAddress
required
Address to receive vault shares
  1. Calculate shares: (assets * total_supply) / total_assets
  2. Transfer USDC from caller to vault
  3. Mint shares to receiver
  4. Update total_assets_managed
  5. Auto-deploy to Vesu (if above dust threshold)
  6. Emit DepositEvent
Returns: shares minted
If deposit is below Vesu’s dust threshold, USDC remains idle until next deposit or manual deploy_to_vesu().

withdraw

Withdraw USDC by burning shares.
fn withdraw(
    ref self: ContractState,
    assets: u256,
    receiver: ContractAddress,
    owner: ContractAddress
) -> u256
assets
u256
required
USDC amount to withdraw (6 decimals)
receiver
ContractAddress
required
Address to receive USDC
owner
ContractAddress
required
Address whose shares are burned
  1. Refresh total_assets from Vesu (includes accrued interest)
  2. Calculate shares to burn: (assets * total_supply) / total_assets
  3. Check allowance (if caller ≠ owner)
  4. Ensure idle balance covers withdrawal (pull from Vesu if needed)
  5. Burn shares from owner
  6. Transfer USDC to receiver
  7. Update total_assets_managed
Returns: shares burned
If vault has insufficient idle USDC, it automatically withdraws from Vesu. Large withdrawals may fail if Vesu liquidity is low.

redeem

Burn shares and receive USDC.
fn redeem(
    ref self: ContractState,
    shares: u256,
    receiver: ContractAddress,
    owner: ContractAddress
) -> u256
shares
u256
required
Vault shares to burn (6 decimals)
  1. Refresh total_assets from Vesu
  2. Calculate USDC: (shares * total_assets) / total_supply
  3. Check allowance (if caller ≠ owner)
  4. Ensure idle balance covers withdrawal
  5. Burn shares from owner
  6. Transfer USDC to receiver
Returns: USDC withdrawn

Vesu Integration

deploy_to_vesu

Manually deploy idle USDC to Vesu.
fn deploy_to_vesu(ref self: ContractState, amount: u256)
amount
u256
required
USDC amount to deploy (must be ≥ min_deposit)
  1. Approve Vesu pool to spend USDC
  2. Call vesu.modify_position() with positive collateral delta
  3. Update total_collateral_deposited
  4. Emit StrategyDeposit event
Curator can call this to manually deploy idle USDC if auto-deploy was skipped due to dust threshold.

withdraw_from_vesu

Manually withdraw USDC from Vesu.
fn withdraw_from_vesu(ref self: ContractState, amount: u256)
  1. Call vesu.modify_position() with negative collateral delta
  2. Update total_collateral_deposited
  3. Emit StrategyWithdraw event

harvest

Refresh total_assets from Vesu (includes accrued interest).
fn harvest(ref self: ContractState)
  1. Query Vesu position: (position, collateral_value, debt_value)
  2. Query idle USDC balance
  3. Update total_assets_managed = collateral_value + idle
  4. Update total_collateral_deposited = collateral_value
Curator should call harvest() periodically to update share price with accrued lending interest.

Storage

ERC-4626 State

name: ByteArray              // "BTCVault Stablecoin"
symbol: ByteArray            // "yvUSDC-STAB"
total_supply: u256           // Total shares minted
balances: Map<ContractAddress, u256>
allowances: Map<(ContractAddress, ContractAddress), u256>
asset: ContractAddress       // USDC
total_assets_managed: u256   // Idle + Vesu collateral

Vesu Strategy

vesu_singleton: ContractAddress  // Vesu Singleton address
vesu_pool_id: felt252            // RE7_USDC_CORE pool ID
debt_asset: ContractAddress      // 0 (no debt)
total_collateral_deposited: u256 // USDC in Vesu

Management

owner: ContractAddress
pending_owner: ContractAddress
is_paused: bool

View Functions

ERC-4626 Standard

fn asset(self: @ContractState) -> ContractAddress
fn total_assets(self: @ContractState) -> u256
fn convert_to_shares(self: @ContractState, assets: u256) -> u256
fn convert_to_assets(self: @ContractState, shares: u256) -> u256
fn max_deposit(self: @ContractState, receiver: ContractAddress) -> u256
fn preview_deposit(self: @ContractState, assets: u256) -> u256

Vault-Specific

fn get_owner(self: @ContractState) -> ContractAddress
fn get_strategy_info(self: @ContractState) -> (u256, u256, u8, bool)
    // Returns: (collateral_deposited, debt, strategy_mode, is_paused)
fn get_vesu_pool_id(self: @ContractState) -> felt252
fn min_deposit(self: @ContractState) -> u256  // Vesu dust threshold

Events

DepositEvent

pub struct DepositEvent {
    pub sender: ContractAddress,   // [key]
    pub owner: ContractAddress,    // [key]
    pub assets: u256,
    pub shares: u256,
}

WithdrawEvent

pub struct WithdrawEvent {
    pub sender: ContractAddress,   // [key]
    pub receiver: ContractAddress, // [key]
    pub owner: ContractAddress,    // [key]
    pub assets: u256,
    pub shares: u256,
}

StrategyDeposit

pub struct StrategyDeposit {
    pub amount: u256,
    pub pool_id: felt252,
}

StrategyWithdraw

pub struct StrategyWithdraw {
    pub amount: u256,
    pub pool_id: felt252,
}

Configuration

Constructor

fn constructor(
    ref self: ContractState,
    asset: ContractAddress,          // USDC
    owner: ContractAddress,
    vesu_singleton: ContractAddress,
    vesu_pool_id: felt252,           // RE7_USDC_CORE
)

Curator Functions

fn upgrade(ref self: ContractState, new_class_hash: ClassHash)
fn set_debt_asset(ref self: ContractState, debt_asset: ContractAddress)
fn set_vesu_pool_id(ref self: ContractState, pool_id: felt252)
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)

Vesu Dust Threshold

Dynamic min_deposit

Vesu rejects deposits below a “dust” threshold to prevent spam:
fn min_deposit(self: @ContractState) -> u256 {
    let vesu = IVesuPoolDispatcher { contract_address: pool_addr };
    let config = vesu.asset_config(usdc_addr);
    let asset_price = vesu.price(usdc_addr);
    if asset_price.value > 0 && config.floor > 0 {
        (config.floor * config.scale) / asset_price.value + 1
    } else {
        0
    }
}
min_deposit is dynamically computed based on Vesu’s floor parameter and USDC price. Typically ~$1-5.

Auto-Deploy Logic

On deposit, vault checks if amount exceeds dust threshold:
fn _deploy_to_strategy(ref self: ContractState, amount: u256) {
    let min_amount = self.min_deposit();
    if amount < min_amount { return; }  // Skip if below threshold
    // ... deploy to Vesu
}

Share Price Calculation

Initial Deposit (Bootstrap)

if total_supply == 0 || total_assets == 0 {
    shares = assets  // 1:1 ratio
}

Subsequent Deposits

shares = (assets * total_supply) / total_assets

After Yield Accrual

Example:
  • Total supply: 1000 shares
  • Total assets: 1100 USDC (100 USDC earned from Vesu)
  • Share price: 1.1 USDC per share
User deposits 110 USDC:
shares = (110 * 1000) / 1100 = 100 shares

Integration Example

use btcvault::stablecoin_vault::{IERC4626Dispatcher, IERC4626DispatcherTrait};

// Deposit USDC
let vault = IERC4626Dispatcher { contract_address: vault_addr };
let usdc = IERC20Dispatcher { contract_address: USDC };

// Check min deposit
let min_dep = vault.min_deposit();
assert(1000_000_000 >= min_dep, 'Below min');  // 1000 USDC

// Approve and deposit
usdc.approve(vault_addr, 1000_000_000);
let shares = vault.deposit(1000_000_000, user_addr);

// Check share price
let total_assets = vault.total_assets();
let total_supply = vault.total_supply();
let price_per_share = (total_assets * 1_000_000) / total_supply;  // 6 decimals

// Withdraw 500 USDC
let shares_to_burn = vault.preview_withdraw(500_000_000);
let assets = vault.withdraw(500_000_000, user_addr, user_addr);

// Or redeem all shares
let user_shares = vault.balance_of(user_addr);
let usdc_out = vault.redeem(user_shares, user_addr, user_addr);

Security Considerations

Withdrawals pull from Vesu if idle balance is insufficient. If Vesu pool has low liquidity, large withdrawals may revert.
Deposits below min_deposit remain idle (not earning yield). Curator must manually deploy via deploy_to_vesu().
First depositor can donate USDC directly to vault to inflate share price. Mitigated by setting reasonable min_deposit.
Vault relies on Vesu’s modify_position() and position() view functions. If Vesu is paused/upgraded, vault withdrawals may fail.

Vesu Position Structure

pub struct ModifyPositionParams {
    pub collateral_asset: ContractAddress,  // USDC
    pub debt_asset: ContractAddress,        // 0 (no debt)
    pub user: ContractAddress,              // vault address
    pub collateral: Amount,
    pub debt: Amount,
}

pub struct Amount {
    pub denomination: AmountDenomination,  // Assets or Shares
    pub value: i257,                        // Signed: + = deposit, - = withdraw
}

Example: Deposit 1000 USDC

let params = ModifyPositionParams {
    collateral_asset: USDC,
    debt_asset: ZERO_ADDRESS,
    user: vault_addr,
    collateral: Amount {
        denomination: AmountDenomination::Assets,
        value: i257 { abs: 1000_000_000, is_negative: false },
    },
    debt: Amount {
        denomination: AmountDenomination::Assets,
        value: i257 { abs: 0, is_negative: false },
    },
};
vesu.modify_position(params);

Example: Withdraw 500 USDC

let params = ModifyPositionParams {
    collateral_asset: USDC,
    debt_asset: ZERO_ADDRESS,
    user: vault_addr,
    collateral: Amount {
        denomination: AmountDenomination::Assets,
        value: i257 { abs: 500_000_000, is_negative: true },  // Negative = withdraw
    },
    debt: Amount {
        denomination: AmountDenomination::Assets,
        value: i257 { abs: 0, is_negative: false },
    },
};
vesu.modify_position(params);

Yield Sources

Vesu RE7_USDC_CORE earns supply APY from:
  1. Borrower interest: Users borrowing USDC pay interest
  2. Protocol fees: Vesu protocol fee on interest
APY is variable and depends on pool utilization:
Utilization = Total Debt / Total Collateral
Supply APY = Borrow APR * Utilization * (1 - Protocol Fee)
Check Vesu Analytics for current supply APY.

Comparison: Stablecoin Vault vs Sentinel

FeatureStablecoin VaultSentinel Vaults
AssetUSDCWBTC
StrategyVesu lending (no debt)Leverage (WBTC collateral + USDC debt)
RiskLow (lending-only)Medium-High (liquidation risk)
APY3-8% (supply APY)10-30% (leveraged yield)
ComplexitySimpleComplex (debt mgmt)
Decimals68

See Also

Build docs developers (and LLMs) love