Skip to main content

HybridVault

Overview​

A HybridVault holds the YieldBasis market positions of one user and the crvUSD backing for them. It keeps the backing in a crvUSD vault: an ERC-4626 savings vault for crvUSD, such as scrvUSD. Each user gets a personal HybridVault, a minimal proxy that HybridVaultFactory deploys. One HybridVault holds the staked and unstaked positions of the user in up to 16 markets.

Each position is in the market's LT, the market's vault contract. This page calls the yb-LP shares of LT "LT shares". It calls the shares that the market's gauge (LiquidityGauge) mints for staked LT shares "gauge shares".

The vault enforces a backing requirement. The crvUSD in the crvUSD vault must cover a fraction (stablecoin_fraction) of the crvUSD value of all active market positions. So access to a market needs crvUSD deposits, which support the crvUSD peg. For the current fraction, read Hybrid Vaults.

The vault works in these ways:

  • On a deposit, the vault checks the crvUSD backing and the pool limit. It then raises the crvUSD allocation of the market's LT for the deposit. After the deposit, it requests the previous allocation plus twice the value of the new shares (previous_allocation + 2 · additional_crvusd). HybridFactoryOwner keeps the allocation at a minimum of 95% of the LP-priced collateral value.
  • The vault tracks the allocation that it added to each market (stablecoin_allocation[pool_id]). On a withdrawal, it reduces the allocation by twice the drop in the required crvUSD, at most by the tracked amount.
  • The vault deposits crvUSD into the crvUSD vault and reads previewRedeem() to find the available backing. You can deposit and withdraw crvUSD, or transfer crvUSD vault shares directly.
  • The LEVAMM (contract AMM), the market's leveraged AMM, keeps its debt-to-collateral ratio within safe bounds (AMM._deposit() → get_x0(safe_limits=True)). A deposit outside these bounds reverts in AMM._deposit() with "Unsafe min" or "Unsafe max". The view safe_to_deposit() estimates the same bounds before you send a transaction.
  • The contract uses # pragma nonreentrancy on, so all external functions are non-reentrant.

Functions for most integrations​

Most integrations need only these calls. Only the vault owner can call deposit(), withdraw(), stake(), unstake(), and emergency_withdraw().

TaskCallNote
Find the vault of a userHybridVaultFactory.user_to_vault(user)A zero address means that the user has no vault.
Create the vaultHybridVaultFactory.create_vault(crvusd_vault)One vault per address. The crvUSD vault must be on the allowlist.
Check the crvUSD to addcrvusd_for_deposit(pool_id, assets, debt)Returns 0 if the backing is enough.
Depositdeposit()Size debt for the pool policy (LT deviation check).
Withdrawwithdraw()Can also unstake and return excess crvUSD.
Stake or unstakestake(), unstake()Uses the gauge of the market.
Claim gauge rewardsclaim_reward()Anyone can call it. Rewards go to the owner.
Exit after an oracle failure or a killemergency_withdraw()Unstake first. While value_oracle() reverts on a market that is not killed, unstake() also reverts, so staked shares can exit only after a kill or after the oracle works again.

To find the vault of a user with web3.py, use this fragment. It assumes w3 and user from the complete script in the Developer quickstart. The HybridVaultFactory address comes from Hybrid Vaults in Contract addresses. The minimal ABI has only user_to_vault. For the full ABI, compile contracts/HybridVaultFactory.vy in yield-basis/yb-core, or export the ABI from the deployed contract on Etherscan.

HVF_ABI = [{
"name": "user_to_vault", "type": "function", "stateMutability": "view",
"inputs": [{"type": "address"}], "outputs": [{"type": "address"}],
}]
hvf = w3.eth.contract(
address="0xBdC32268851C324c6185809271dfe6d8dab8dC5b", abi=HVF_ABI
) # HybridVaultFactory
user_vault = hvf.functions.user_to_vault(user).call() # zero: no vault

System architecture​

The following figure shows how a HybridVault connects to the two factories, the crvUSD vault, and the contracts of each market:

HybridVault architecture: the user creates the vault through HybridVaultFactory, which deploys it, and calls the vault to deposit, withdraw, stake, and unstake. The vault reads market contracts from Factory.markets(pool_id). For each of up to 16 markets, it deposits into and withdraws from the LT, stakes in the gauge, and reads LEVAMM value_oracle(). It keeps crvUSD backing in a savings vault such as scrvUSD. For an allocation change, the vault calls lt_allocate_stablecoins on HybridVaultFactory, which forwards the call to HybridFactoryOwner, which calls LT.allocate_stablecoins.HybridVault architecture: the user creates the vault through HybridVaultFactory, which deploys it, and calls the vault to deposit, withdraw, stake, and unstake. The vault reads market contracts from Factory.markets(pool_id). For each of up to 16 markets, it deposits into and withdraws from the LT, stakes in the gauge, and reads LEVAMM value_oracle(). It keeps crvUSD backing in a savings vault such as scrvUSD. For an allocation change, the vault calls lt_allocate_stablecoins on HybridVaultFactory, which forwards the call to HybridFactoryOwner, which calls LT.allocate_stablecoins.

A user calls create_vault(crvusd_vault) on HybridVaultFactory. The factory checks that the caller has no vault and that the crvUSD vault is on the allowlist. It deploys a minimal proxy of vault_impl and calls initialize(user, crvusd_vault) on it. The caller becomes the vault owner.

The owner then calls the vault for each operation. The vault reads the contracts of a market from Factory.markets(pool_id). It deposits into the LT of the market, stakes in its gauge (market.staker), and values the position with AMM.value_oracle(). It keeps the crvUSD backing in the crvUSD vault.

A deposit or a withdrawal changes the stablecoin allocation of the LT. The vault calls lt_allocate_stablecoins on HybridVaultFactory. The factory forwards the call to the admin of the Factory, HybridFactoryOwner, which calls LT.allocate_stablecoins. The vault also reports its required crvUSD with update_vault_required, so the factory can enforce the crvUSD vault limit.


Public variables​

The vault has these public variables.

Constants

  • MAX_VAULTS (uint256) - Maximum simultaneous market positions per vault (16)

Immutable variables

  • AMM_MIN_SAFE_DEBT (uint256) - Lower bound of the debt-to-collateral-value ratio, 1e18-based: 1 / (4 · L²) = 0.0625 at leverage L = 2
  • AMM_MAX_SAFE_DEBT (uint256) - Upper bound of the debt-to-collateral-value ratio, 1e18-based: (2L − 1)² / (4L²) − 1 / (8L²) = 0.53125 at L = 2
  • FACTORY (Factory) - YieldBasis Factory contract
  • CRVUSD (IERC20) - crvUSD token
  • VAULT_FACTORY (VaultFactory) - HybridVaultFactory contract

State variables

  • owner (address) - Vault owner (set once at initialization)
  • crvusd_vault (IERC4626) - Current crvUSD vault, such as scrvUSD
  • used_vaults (DynArray[uint256, 16]) - Market indexes (pool_id) with active positions
  • stablecoin_allocation (HashMap[uint256, uint256]) - Tracked allocation that the vault added, per market
  • personal_limit (HashMap[uint256, uint256]) - Personal pool-limit boost, per market

Data structs​

The vault reads these structs from other contracts:

  • Market (read from Factory): asset_token, cryptopool, amm, lt, price_oracle, virtual_pool, staker.
  • OraclizedValue: p_o (oracle price), value (USD value).
  • LiquidityValues: admin, total, ideal_staked, staked.

Events​

The vault emits these events:

  • SetPersonalLimit: pool_id: uint256, limit: uint256
  • SetCrvusdVault: vault: IERC4626 (an address in the ABI)

Approvals​

The vault pulls tokens from the caller with transferFrom. Before you call these functions, approve the HybridVault:

  • Before deposit(), approve the vault for assets of the market's asset token. With deposit_stablecoins=True, also approve it for the crvUSD that the backing lacks. To read that amount, call crvusd_for_deposit(pool_id, assets, debt).
  • Before deposit_crvusd(), approve the vault for assets of crvUSD.
  • Before deposit_scrvusd(), approve the vault for shares of the crvUSD vault.
  • Before emergency_withdraw() with crvusd_from_wallet=True, approve the vault for the crvUSD that the debt repayment needs.

LT deviation check​

On a pool that runs the oracle-report policy (YBOraclizedPolicy), the policy checks each LT.deposit() into the market of that pool. A HybridVault deposit calls LT.deposit(), so the check applies to it. To see which policy a pool runs, call POLICY() on the pool or read the table in Contract addresses.

The check compares the debt of the deposit with added_lp · AMM.get_debt() / AMM.collateral_amount(). Here added_lp is the LP that the deposit mints. If the difference is more than ratio_tolerance times that value, the deposit reverts with "LT deviation". The policy owner sets ratio_tolerance, to a maximum of 10 bp. To read it, call config() on the policy. For the full rule and its exemptions, read Pool callback and LT deviation check.

The "Debt made too high" limit of this vault (110%) is much wider than ratio_tolerance. A debt that passes the vault check can fail the policy check.

Size debt to the LEVAMM book

The price rule sets debt from assets · price_oracle. A live policy is one that is not killed. Use the price rule unless the pool runs a live YBOraclizedPolicy and the LEVAMM holds collateral; then compute debt from the LEVAMM book. Do not use assets · price_oracle as debt on a pool that runs this policy. If the debt differs from the book ratio by more than ratio_tolerance, the deposit reverts. For both rules and the steps that compute debt, read Compute debt on a pool that runs the oracle-report policy. For a flowchart of the rule, read Choose the debt rule on the same page.


Functions​

Each function section lists its own revert strings. For the revert strings of all YieldBasis contracts, read Error codes: HybridVault.vy.

initialize()​

Initializes a cloned vault instance for a user. The function sets the vault owner and approves crvUSD to the crvUSD vault. It runs one time: it reverts with "Already initialized" if owner is not empty. The implementation contract has owner = 0x01, so nobody can initialize it.

The function returns True if the initialization succeeds (bool).

The function takes these inputs:

InputTypeDescription
useraddressAddress that becomes the owner of this vault
crvusd_vaultIERC4626crvUSD vault to use, such as scrvUSD
Contract source: initialize()

The contract source gives this function body:

@external
def initialize(user: address, crvusd_vault: IERC4626) -> bool:
assert self.owner == empty(address), "Already initialized"
self.owner = user
self.crvusd_vault = crvusd_vault
extcall CRVUSD.approve(crvusd_vault.address, max_value(uint256))
return True

initialize() reverts with Already initialized if owner is already set.


deposit()​

Deposits assets into a YieldBasis market through this vault. Only the owner can call it. Before you call it, give the approvals in Approvals.

The vault passes your debt to LT.deposit() without a change. On a pool that runs the oracle-report policy, the policy adds the LT deviation check (a debt ratio check), described in LT deviation check.

The function does these steps:

  1. Checks the crvUSD backing. If deposit_stablecoins=True, it pulls the missing crvUSD from the sender.
  2. Checks the pool limit: pool_value + additional_crvusd ≤ pool_limits(pool_id). pool_value is the LEVAMM value of the whole market (AMM.value_oracle().value). additional_crvusd is the value of the shares of this deposit. The limit therefore caps the LEVAMM value of the market after the deposit, not the size of one deposit. It applies only to deposits through a HybridVault.
  3. Checks debt ≤ 1.1 · additional_crvusd. The debt cannot be more than 110% of the value of the new shares. A larger debt reverts with "Debt made too high".
  4. Raises the market allocation.
  5. Calls LT.deposit().
  6. Requests previous_allocation + 2 · additional_crvusd.
  7. Stakes the shares if stake=True.

The function returns the LT shares received, or the gauge shares if stake=True (uint256).

The function takes these inputs:

InputTypeDescription
pool_iduint256Market index, as in Factory.markets(pool_id)
assetsuint256Amount of asset token to deposit
debtuint256Amount of crvUSD debt to take on
min_sharesuint256Minimum LT shares to receive (slippage protection)
stakeboolIf True, stake the LT shares in the gauge (default: False)
deposit_stablecoinsboolIf True, pull additional crvUSD from the sender if the backing is too low (default: False)
Contract source: deposit()

The contract source gives this function body:

@external
def deposit(pool_id: uint256, assets: uint256, debt: uint256, min_shares: uint256, stake: bool = False, deposit_stablecoins: bool = False) -> uint256:
assert self.owner == msg.sender, "Access"

market: Market = staticcall FACTORY.markets(pool_id)
assert market.lt.address != empty(address), "Bad pool_id"
if not self.pool_approved[pool_id]:
assert extcall market.asset_token.approve(market.lt.address, max_value(uint256), default_return_value=True)
extcall market.lt.approve(market.staker.address, max_value(uint256))
self.pool_approved[pool_id] = True
self.token_in_use[market.lt.address] = True
self.token_in_use[market.staker.address] = True

# Trigger checkpoint_staker_rebase() by staking 0 tokens
extcall market.staker.deposit(0, self)

pool_value: uint256 = 0
additional_crvusd: uint256 = 0
pool_value, additional_crvusd = self._required_crvusd_for(market, assets, debt)
crvusd_available: uint256 = self._crvusd_available()
# next line will revert if max_value(uint256)
crvusd_required: uint256 = self._downscale(self._required_crvusd() + additional_crvusd)
if crvusd_available < crvusd_required:
if deposit_stablecoins:
self._deposit_crvusd(crvusd_required - crvusd_available)
else:
raise "Not enough crvUSD"
assert pool_value + additional_crvusd <= self._pool_limits(pool_id), "Beyond pool limit"

# Temporarily make the cap bigger than necessary
assert debt <= 11 * additional_crvusd // 10, "Debt made too high"
previous_allocation: uint256 = staticcall market.lt.stablecoin_allocation()
self._allocate_stablecoins(market.lt, max((pool_value + additional_crvusd) * 22 // 10, previous_allocation))

assert extcall market.asset_token.transferFrom(msg.sender, self, assets, default_return_value=True)
lt_shares: uint256 = extcall market.lt.deposit(assets, debt, min_shares)

assert lt_shares > 0, "No liquidity given"

self._add_to_used(pool_id) # We don't get here if lt_shares = 0

# Reduce cap to what it should be
self._allocate_stablecoins(market.lt, previous_allocation + 2 * additional_crvusd)
self.stablecoin_allocation[pool_id] += 2 * additional_crvusd

extcall VAULT_FACTORY.update_vault_required(self.crvusd_vault.address, crvusd_required, True)

if not stake:
return lt_shares

else:
return extcall market.staker.deposit(lt_shares, self)

deposit() reverts with these strings:

StringCause
AccessThe caller is not the vault owner.
Bad pool_idThe Factory has no market with this pool_id.
Not enough crvUSDThe backing is too low and deposit_stablecoins=False.
Beyond pool limitThe LEVAMM value of the market after the deposit is more than the pool limit.
Debt made too highdebt is more than 110% of additional_crvusd.
No liquidity givenLT.deposit() returned 0 shares.
Vault wind-down, Beyond vault limitFrom HybridVaultFactory.update_vault_required.
Debt too high, Unsafe min, Unsafe max, SlippageFrom LT.deposit() and the LEVAMM.
LT deviationOnly on a pool that runs the oracle-report policy YBOraclizedPolicy. For the check, read LT deviation check.
slippage, donation above cap!From the Curve pool: the borrower-fee donation at the end of LT.deposit() fails. For the donation, read LT: distribute_borrower_fees.

withdraw()​

Withdraws assets from a YieldBasis market. Only owner can call it. To withdraw all shares, pass max_value(uint256) for shares. If withdraw_stablecoins=True, the vault returns excess crvUSD backing to the receiver.

The vault then reduces the allocation by twice the drop in the required crvUSD, at most by the tracked amount. It also updates the tracked required crvUSD in the factory.

If the oracle of this market fails, withdraw() reverts (in LT.withdraw(), or with "Oracle is broken"). If this market is killed, LT.withdraw() reverts. In these cases, use emergency_withdraw().

If the oracle of another market in the vault fails, a call with withdraw_stablecoins=True reverts with "Cannot withdraw stables". Call again with withdraw_stablecoins=False. If that call also reverts, use emergency_withdraw().

The function returns the amount of assets withdrawn (uint256), in raw units at the asset's decimals.

The function takes these inputs:

InputTypeDescription
pool_iduint256Market index, as in Factory.markets(pool_id)
sharesuint256LT shares, or gauge shares if unstake=True. max_value(uint256) withdraws all.
min_assetsuint256Minimum assets to receive (slippage protection)
unstakeboolIf True, unstake from the gauge first (default: False)
receiveraddressAddress to receive the withdrawn assets (default: msg.sender)
withdraw_stablecoinsboolIf True, return excess crvUSD to the receiver (default: False)
Contract source: withdraw()

The contract source gives this function body:

@external
def withdraw(pool_id: uint256, shares: uint256, min_assets: uint256, unstake: bool = False, receiver: address = msg.sender, withdraw_stablecoins: bool = False) -> uint256:
assert self.owner == msg.sender, "Access"

market: Market = staticcall FACTORY.markets(pool_id)
assert market.lt.address != empty(address), "Bad pool_id"

required_before: uint256 = self._required_crvusd()
pool_crvusd_before: uint256 = self._pool_crvusd(market)

lt_shares: uint256 = shares
if unstake:
staker_shares: uint256 = shares
if staker_shares == max_value(uint256):
staker_shares = staticcall market.staker.balanceOf(self)
lt_shares = extcall market.staker.redeem(staker_shares, self, self)
elif lt_shares == max_value(uint256):
lt_shares = staticcall market.lt.balanceOf(self)
lt_supply: uint256 = staticcall market.lt.totalSupply()

assets: uint256 = extcall market.lt.withdraw(lt_shares, min_assets, receiver)

removed: bool = False
if staticcall market.lt.balanceOf(self) == 0 and staticcall market.staker.balanceOf(self) == 0:
self._remove_from_used(pool_id)
removed = True

required_after: uint256 = self._required_crvusd()

if not removed:
previous_allocation: uint256 = staticcall market.lt.stablecoin_allocation()
reduction: uint256 = 0

if required_before == max_value(uint256) or required_after == max_value(uint256):
pool_crvusd_after: uint256 = self._pool_crvusd(market)
assert pool_crvusd_before != max_value(uint256) and pool_crvusd_after != max_value(uint256), "Oracle is broken"
assert not withdraw_stablecoins, "Cannot withdraw stables"
if pool_crvusd_before > pool_crvusd_after:
reduction = min(2 * (pool_crvusd_before - pool_crvusd_after), self.stablecoin_allocation[pool_id])
else:
if required_before > required_after:
if withdraw_stablecoins:
if required_after > 0:
self._withdraw_crvusd(self._downscale(required_before - required_after), receiver, True)
else:
self._redeem_crvusd(staticcall self.crvusd_vault.balanceOf(self), receiver)
reduction = min(2 * (required_before - required_after), self.stablecoin_allocation[pool_id])

if reduction > 0:
if reduction > previous_allocation:
reduction = previous_allocation
self._allocate_stablecoins(market.lt, previous_allocation - reduction)
self.stablecoin_allocation[pool_id] -= reduction

else:
if withdraw_stablecoins:
assert required_after != max_value(uint256), "Cannot withdraw stables"
if required_after > 0:
available: uint256 = self._crvusd_available()
needed: uint256 = self._downscale(required_after)
if available > needed:
self._withdraw_crvusd(available - needed, receiver, True)
else:
self._redeem_crvusd(staticcall self.crvusd_vault.balanceOf(self), receiver)

if required_after != max_value(uint256):
extcall VAULT_FACTORY.update_vault_required(self.crvusd_vault.address, self._downscale(required_after), False)

return assets

withdraw() reverts with these strings:

StringCause
AccessThe caller is not the vault owner.
Bad pool_idThe Factory has no market with this pool_id.
Oracle is brokenThe oracle of this market fails, as the description explains.
Cannot withdraw stableswithdraw_stablecoins=True, and the oracle of another market in the vault fails.
Not enough crvUSD leftThe returned crvUSD leaves the backing too low.

LT.withdraw() reverts pass through.


emergency_withdraw()​

Withdraws from a YieldBasis market through LT.emergency_withdraw(). Use it when withdraw() reverts because of an oracle failure or a killed market. On markets 0 to 6, LT.emergency_withdraw() also reverts when the oracle of that market fails and the market is not killed. Only owner can call it. The assets go to the vault owner. The function reverts with "Zero shares" if there are no shares to withdraw.

Unstake before an emergency withdrawal

Unstake the LT shares with unstake() before you call emergency_withdraw(). This function cannot use staked shares.

While the market is not killed, unstake() reverts if AMM.value_oracle() reverts. The gauge calls LT.checkpoint_staker_rebase(), which reads that oracle. Staked shares therefore stay in the gauge until the oracle works again or Factory.admin() or Factory.emergency_admin() kills the market with LT.set_killed(True). After a kill, unstake() works, and emergency_withdraw() can then withdraw the shares. For the calls that work in each market state, read Market states and allowed calls in LT.

The crvusd_from_wallet input sets the source of crvUSD for the debt repayment:

  • False: the vault redeems all its crvUSD vault shares. After the withdrawal, it deposits the remaining crvUSD back into the crvUSD vault.
  • True: the caller sends crvUSD directly and receives the excess back.

After the withdrawal, the vault reduces the allocation of the market. The reduction depends on the oracles:

  • All oracles work: twice the drop in the required crvUSD.
  • The oracle of another market fails, and both reads of this market succeed: twice the drop in the crvUSD value of this market.
  • The oracle of this market fails: the crvUSD that the withdrawal used, divided by stablecoin_fraction.

Each reduction is at most the allocation that the vault tracks for the market.

The function returns nothing.

The function takes these inputs:

InputTypeDescription
pool_iduint256Market index, as in Factory.markets(pool_id)
sharesuint256LT shares to withdraw. max_value(uint256) withdraws all.
crvusd_from_walletboolIf True, pull crvUSD from the caller. If False, redeem from the crvUSD vault (default: False).
Contract source: emergency_withdraw() (condensed)

The contract source gives this condensed function body:

@external
def emergency_withdraw(pool_id: uint256, shares: uint256, crvusd_from_wallet: bool = False):
assert self.owner == msg.sender, "Access"
# Omitted: resolve lt_shares and take crvUSD from the wallet or the savings vault.
assets: uint256 = (extcall market.lt.emergency_withdraw(lt_shares, self, self))[0]
# Omitted: return crvUSD, send assets, reduce the allocation, update the factory.

The linked source file has all oracle-fallback branches.

emergency_withdraw() reverts with these strings:

StringCause
AccessThe caller is not the vault owner.
Bad pool_idThe Factory has no market with this pool_id.
Zero sharesThere are no LT shares to withdraw.

LT.emergency_withdraw() reverts pass through.


stake()​

Stakes LT shares in the gauge of the market to earn YB emissions. Only owner can call it.

The function returns the gauge shares received (uint256).

The function takes these inputs:

InputTypeDescription
pool_iduint256Market index, as in Factory.markets(pool_id)
pool_sharesuint256Amount of LT shares to stake
Contract source: stake()

The contract source gives this function body:

@external
def stake(pool_id: uint256, pool_shares: uint256) -> uint256:
assert self.owner == msg.sender, "Access"
market: Market = staticcall FACTORY.markets(pool_id)
assert market.lt.address != empty(address), "Bad pool_id"
return extcall market.staker.deposit(pool_shares, self)

stake() reverts with these strings:

StringCause
AccessThe caller is not the vault owner.
Bad pool_idThe Factory has no market with this pool_id.

unstake()​

Unstakes gauge shares from the gauge of the market. The vault receives LT shares back. Only owner can call it.

The function returns the LT shares received (uint256).

The function takes these inputs:

InputTypeDescription
pool_iduint256Market index, as in Factory.markets(pool_id)
gauge_sharesuint256Amount of gauge shares to unstake
Contract source: unstake()

The contract source gives this function body:

@external
def unstake(pool_id: uint256, gauge_shares: uint256) -> uint256:
assert self.owner == msg.sender, "Access"
market: Market = staticcall FACTORY.markets(pool_id)
assert market.lt.address != empty(address), "Bad pool_id"
return extcall market.staker.redeem(gauge_shares, self, self)

unstake() reverts with these strings:

StringCause
AccessThe caller is not the vault owner.
Bad pool_idThe Factory has no market with this pool_id.

claim_reward()​

Claims rewards from all staked positions and sends them to the vault owner. The function calls claim() on the gauge of each market in used_vaults. Anyone can call it. The function always sends rewards to the vault owner, so a keeper or bot can call it safely.

The function returns the total amount claimed and transferred to the owner (uint256), in units of the reward token.

The function takes this input:

InputTypeDescription
tokenIERC20The reward token to claim
Contract source: claim_reward()

The contract source gives this function body:

@external
def claim_reward(token: IERC20) -> uint256:
total: uint256 = 0
for pool_id: uint256 in self.used_vaults:
market: Market = staticcall FACTORY.markets(pool_id)
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
market.staker.address,
abi_encode(token.address, self, method_id=method_id("claim(address,address)")),
max_outsize=32,
revert_on_failure=False)
if success:
total += convert(res, uint256)
if total > 0:
assert extcall token.transfer(self.owner, total, default_return_value=True)
return total

preview_claim_reward()​

View that returns the total claimable rewards of all staked positions.

The function returns the total claimable amount of the reward token (uint256).

The function takes this input:

InputTypeDescription
tokenIERC20The reward token to query
Contract source: preview_claim_reward()

The contract source gives this function body:

@external
@view
def preview_claim_reward(token: IERC20) -> uint256:
total: uint256 = 0
for pool_id: uint256 in self.used_vaults:
market: Market = staticcall FACTORY.markets(pool_id)
success: bool = False
res: Bytes[32] = empty(Bytes[32])
success, res = raw_call(
market.staker.address,
abi_encode(token.address, self, method_id=method_id("preview_claim(address,address)")),
max_outsize=32,
is_static_call=True,
revert_on_failure=False)
if success:
total += convert(res, uint256)
return total

deposit_crvusd()​

Deposits crvUSD into the crvUSD vault. The function pulls crvUSD from the caller with transferFrom, so approve the HybridVault for assets of crvUSD first. Anyone can call it to add backing.

The function returns the crvUSD vault shares received (uint256).

The function takes this input:

InputTypeDescription
assetsuint256Amount of crvUSD to deposit
Contract source: deposit_crvusd()

The contract source gives this function body:

@external
def deposit_crvusd(assets: uint256) -> uint256:
return self._deposit_crvusd(assets)

redeem_crvusd()​

Redeems crvUSD vault shares for crvUSD. It reverts with "Not enough crvUSD left" if the remaining backing is too low for the active positions. Only owner can call it.

The function returns the amount of crvUSD withdrawn (uint256).

The function takes this input:

InputTypeDescription
sharesuint256Amount of crvUSD vault shares to redeem
Contract source: redeem_crvusd()

The contract source gives this function body:

@external
def redeem_crvusd(shares: uint256) -> uint256:
assert self.owner == msg.sender, "Access"
return self._redeem_crvusd(shares, msg.sender)

redeem_crvusd() reverts with these strings:

StringCause
AccessThe caller is not the vault owner.
Not enough crvUSD leftThe remaining backing is too low.

deposit_scrvusd() / withdraw_scrvusd()​

Deposits or withdraws crvUSD vault shares, such as scrvUSD, directly with transferFrom or transfer. Before deposit_scrvusd(), approve the HybridVault for shares of the crvUSD vault. withdraw_scrvusd() reverts with "Not enough crvUSD left" if the remaining backing is too low. Anyone can call deposit_scrvusd(). Only owner can call withdraw_scrvusd().

Contract source: deposit_scrvusd()

The contract source gives these two function bodies:

@external
def deposit_scrvusd(shares: uint256):
extcall self.crvusd_vault.transferFrom(msg.sender, self, shares)

@external
def withdraw_scrvusd(shares: uint256):
assert self.owner == msg.sender, "Access"
extcall self.crvusd_vault.transfer(msg.sender, shares)
assert self._crvusd_available() >= self._downscale(self._required_crvusd()), "Not enough crvUSD left"

withdraw_scrvusd() reverts with these strings:

StringCause
AccessThe caller is not the vault owner.
Not enough crvUSD leftThe remaining backing is too low.

set_crvusd_vault()​

Changes the crvUSD vault of this HybridVault. Only owner can call it, and only when the HybridVault has no active positions. The new crvUSD vault must be on the allowlist that governance sets. The function returns the old crvUSD vault shares to the owner. By default it returns them as crvUSD. With redeem=False, it returns them as shares.

The function returns nothing. It emits SetCrvusdVault.

The function takes these inputs:

InputTypeDescription
new_vaultIERC4626New crvUSD vault to use (must be on the allowlist)
redeemboolIf True, redeem the old crvUSD vault shares for crvUSD; if False, transfer the shares directly (default: True)
Contract source: set_crvusd_vault()

The contract source gives this function body:

@external
def set_crvusd_vault(new_vault: IERC4626, redeem: bool = True):
assert msg.sender == self.owner, "Only owner"
assert staticcall VAULT_FACTORY.allowed_crvusd_vaults(new_vault.address), "Vault not allowed"
assert len(self.used_vaults) == 0, "Has active positions"
old_vault: IERC4626 = self.crvusd_vault
if old_vault != empty(IERC4626):
extcall VAULT_FACTORY.update_vault_required(old_vault.address, 0, False)
shares: uint256 = staticcall old_vault.balanceOf(self)
if shares > 0:
if redeem:
extcall old_vault.redeem(shares, msg.sender, self)
else:
extcall old_vault.transfer(msg.sender, shares)
extcall CRVUSD.approve(old_vault.address, 0)
self.crvusd_vault = new_vault
extcall CRVUSD.approve(new_vault.address, max_value(uint256))
log SetCrvusdVault(vault=new_vault)

set_crvusd_vault() reverts with these strings:

StringCause
Only ownerThe caller is not owner.
Vault not allowednew_vault is not on the allowlist of HybridVaultFactory.
Has active positionsused_vaults is not empty.

set_personal_limit()​

Sets a personal pool limit for this vault. The vault adds it to the global pool limit. Only VAULT_FACTORY.ADMIN() can call it.

The function returns nothing. It emits SetPersonalLimit.

The function takes these inputs:

InputTypeDescription
pool_iduint256Market index, as in Factory.markets(pool_id)
limituint256Personal limit to set
Contract source: set_personal_limit()

The contract source gives this function body:

@external
def set_personal_limit(pool_id: uint256, limit: uint256):
assert msg.sender == staticcall VAULT_FACTORY.ADMIN(), "Only admin"
self.personal_limit[pool_id] = limit
log SetPersonalLimit(pool_id=pool_id, limit=limit)

set_personal_limit() reverts with Only admin if the caller is not VAULT_FACTORY.ADMIN().


recover_tokens()​

Recovers tokens that a user sent to the vault by mistake. It cannot recover two kinds of token:

  • The LT shares or gauge shares of a market that this vault used. The vault marks these tokens at the first deposit and does not clear the mark.
  • The shares of the current crvUSD vault.

Only owner can call it. The function returns nothing.

The function takes this input:

InputTypeDescription
tokenIERC20The token to recover
Contract source: recover_tokens()

The contract source gives this function body:

@external
def recover_tokens(token: IERC20):
assert self.owner == msg.sender, "Access"
assert not self.token_in_use[token.address] and token.address != self.crvusd_vault.address, "Token not allowed"
assert extcall token.transfer(msg.sender, staticcall token.balanceOf(self), default_return_value=True)

recover_tokens() reverts with these strings:

StringCause
AccessThe caller is not the vault owner.
Token not allowedtoken is the LT or the gauge of a market that this vault used, or the current crvUSD vault.

safe_to_deposit()​

View that checks whether a deposit into a market keeps the LEVAMM within its safe bounds.

The function returns True if the debt-to-collateral ratio of the LEVAMM stays within the safe bounds after the deposit, and False if it does not (bool).

The function takes these inputs:

InputTypeDescription
pool_iduint256Market index, as in Factory.markets(pool_id)
assetsuint256Amount of assets to deposit (0 to check the current state)
debtuint256Amount of debt to take on (0 to check the current state)
Contract source: safe_to_deposit()

The contract source gives this function body:

@external
@view
def safe_to_deposit(pool_id: uint256, assets: uint256, debt: uint256) -> bool:
return self._check_safe_limits(staticcall FACTORY.markets(pool_id), assets, debt)

View helpers​

Each crvUSD amount in these views has 18 decimals and uses the USD value that AMM.value_oracle() reports. The vault has these views:

SignatureReturns
pool_limits(pool_id: uint256) -> uint256Effective pool limit: personal_limit[pool_id] plus HybridVaultFactory.pool_limits(pool_id).
required_crvusd() -> uint256Total crvUSD backing that all positions need, after stablecoin_fraction.
raw_required_crvusd_for(pool_id: uint256, assets: uint256, debt: uint256, downscale: bool = True) -> uint256crvUSD required for a possible deposit: after stablecoin_fraction when downscale=True, or the raw amount when False.
crvusd_for_deposit(pool_id: uint256, assets: uint256, debt: uint256) -> uint256Additional crvUSD that the user must provide for a deposit. 0 if the backing is enough.
withdrawable_crvusd_for(pool_id: uint256, shares: uint256, is_staked: bool) -> uint256crvUSD that a withdrawal of shares releases.
assets_for_crvusd(pool_id: uint256, crvusd_amount: uint256) -> uint256The asset amount, in the decimals of the asset, that matches a crvUSD input.

For example, positions with a crvUSD value of 10,000 (10_000 * 10**18) and stablecoin_fraction() = 5 * 10**17 (50%) give required_crvusd() = 5_000 * 10**18, which is 5,000 crvUSD.

raw_required_crvusd_for divides by liquidity.total and totalSupply, so it reverts on an empty market. The internal helper _required_crvusd_for returns the pair (pool_value, additional_crvusd). This view returns only additional_crvusd.

  • HybridVaultFactory: the factory that deploys this vault and sets its global limits.
  • Factory: the market registry that the vault reads.
  • LT and LEVAMM: the market contracts behind each position.
  • Hybrid Vaults: how the backing requirement works for a user.
  • Use Hybrid Vaults: the app steps to create and use a vault.