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
USDC amount to deposit (6 decimals)
Address to receive vault shares
- Calculate shares:
(assets * total_supply) / total_assets
- Transfer USDC from caller to vault
- Mint shares to receiver
- Update
total_assets_managed
- Auto-deploy to Vesu (if above dust threshold)
- 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
USDC amount to withdraw (6 decimals)
Address whose shares are burned
- Refresh total_assets from Vesu (includes accrued interest)
- Calculate shares to burn:
(assets * total_supply) / total_assets
- Check allowance (if caller ≠ owner)
- Ensure idle balance covers withdrawal (pull from Vesu if needed)
- Burn shares from owner
- Transfer USDC to receiver
- 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
Vault shares to burn (6 decimals)
- Refresh total_assets from Vesu
- Calculate USDC:
(shares * total_assets) / total_supply
- Check allowance (if caller ≠ owner)
- Ensure idle balance covers withdrawal
- Burn shares from owner
- 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)
USDC amount to deploy (must be ≥ min_deposit)
- Approve Vesu pool to spend USDC
- Call
vesu.modify_position() with positive collateral delta
- Update
total_collateral_deposited
- 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)
- Call
vesu.modify_position() with negative collateral delta
- Update
total_collateral_deposited
- Emit
StrategyWithdraw event
harvest
Refresh total_assets from Vesu (includes accrued interest).
fn harvest(ref self: ContractState)
- Query Vesu position:
(position, collateral_value, debt_value)
- Query idle USDC balance
- Update
total_assets_managed = collateral_value + idle
- 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:
- Borrower interest: Users borrowing USDC pay interest
- 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)
Comparison: Stablecoin Vault vs Sentinel
| Feature | Stablecoin Vault | Sentinel Vaults |
|---|
| Asset | USDC | WBTC |
| Strategy | Vesu lending (no debt) | Leverage (WBTC collateral + USDC debt) |
| Risk | Low (lending-only) | Medium-High (liquidation risk) |
| APY | 3-8% (supply APY) | 10-30% (leveraged yield) |
| Complexity | Simple | Complex (debt mgmt) |
| Decimals | 6 | 8 |
See Also