HybridVault
Source: HybridVault.vy on GitHub
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).HybridFactoryOwnerkeeps 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 inAMM._deposit()with"Unsafe min"or"Unsafe max". The viewsafe_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().
| Task | Call | Note |
|---|---|---|
| Find the vault of a user | HybridVaultFactory.user_to_vault(user) | A zero address means that the user has no vault. |
| Create the vault | HybridVaultFactory.create_vault(crvusd_vault) | One vault per address. The crvUSD vault must be on the allowlist. |
| Check the crvUSD to add | crvusd_for_deposit(pool_id, assets, debt) | Returns 0 if the backing is enough. |
| Deposit | deposit() | Size debt for the pool policy (LT deviation check). |
| Withdraw | withdraw() | Can also unstake and return excess crvUSD. |
| Stake or unstake | stake(), unstake() | Uses the gauge of the market. |
| Claim gauge rewards | claim_reward() | Anyone can call it. Rewards go to the owner. |
| Exit after an oracle failure or a kill | emergency_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:
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 = 2AMM_MAX_SAFE_DEBT(uint256) - Upper bound of the debt-to-collateral-value ratio, 1e18-based:(2L − 1)² / (4L²) − 1 / (8L²)= 0.53125 at L = 2FACTORY(Factory) - YieldBasis Factory contractCRVUSD(IERC20) - crvUSD tokenVAULT_FACTORY(VaultFactory) - HybridVaultFactory contract
State variables
owner(address) - Vault owner (set once at initialization)crvusd_vault(IERC4626) - Current crvUSD vault, such as scrvUSDused_vaults(DynArray[uint256, 16]) - Market indexes (pool_id) with active positionsstablecoin_allocation(HashMap[uint256, uint256]) - Tracked allocation that the vault added, per marketpersonal_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 forassetsof the market's asset token. Withdeposit_stablecoins=True, also approve it for the crvUSD that the backing lacks. To read that amount, callcrvusd_for_deposit(pool_id, assets, debt). - Before
deposit_crvusd(), approve the vault forassetsof crvUSD. - Before
deposit_scrvusd(), approve the vault forsharesof the crvUSD vault. - Before
emergency_withdraw()withcrvusd_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.
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:
| Input | Type | Description |
|---|---|---|
user | address | Address that becomes the owner of this vault |
crvusd_vault | IERC4626 | crvUSD 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:
- Checks the crvUSD backing. If
deposit_stablecoins=True, it pulls the missing crvUSD from the sender. - Checks the pool limit:
pool_value + additional_crvusd ≤ pool_limits(pool_id).pool_valueis the LEVAMM value of the whole market (AMM.value_oracle().value).additional_crvusdis 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. - 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". - Raises the market allocation.
- Calls
LT.deposit(). - Requests
previous_allocation + 2 · additional_crvusd. - 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:
| Input | Type | Description |
|---|---|---|
pool_id | uint256 | Market index, as in Factory.markets(pool_id) |
assets | uint256 | Amount of asset token to deposit |
debt | uint256 | Amount of crvUSD debt to take on |
min_shares | uint256 | Minimum LT shares to receive (slippage protection) |
stake | bool | If True, stake the LT shares in the gauge (default: False) |
deposit_stablecoins | bool | If 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:
| String | Cause |
|---|---|
Access | The caller is not the vault owner. |
Bad pool_id | The Factory has no market with this pool_id. |
Not enough crvUSD | The backing is too low and deposit_stablecoins=False. |
Beyond pool limit | The LEVAMM value of the market after the deposit is more than the pool limit. |
Debt made too high | debt is more than 110% of additional_crvusd. |
No liquidity given | LT.deposit() returned 0 shares. |
Vault wind-down, Beyond vault limit | From HybridVaultFactory.update_vault_required. |
Debt too high, Unsafe min, Unsafe max, Slippage | From LT.deposit() and the LEVAMM. |
LT deviation | Only 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:
| Input | Type | Description |
|---|---|---|
pool_id | uint256 | Market index, as in Factory.markets(pool_id) |
shares | uint256 | LT shares, or gauge shares if unstake=True. max_value(uint256) withdraws all. |
min_assets | uint256 | Minimum assets to receive (slippage protection) |
unstake | bool | If True, unstake from the gauge first (default: False) |
receiver | address | Address to receive the withdrawn assets (default: msg.sender) |
withdraw_stablecoins | bool | If 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:
| String | Cause |
|---|---|
Access | The caller is not the vault owner. |
Bad pool_id | The Factory has no market with this pool_id. |
Oracle is broken | The oracle of this market fails, as the description explains. |
Cannot withdraw stables | withdraw_stablecoins=True, and the oracle of another market in the vault fails. |
Not enough crvUSD left | The 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 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:
| Input | Type | Description |
|---|---|---|
pool_id | uint256 | Market index, as in Factory.markets(pool_id) |
shares | uint256 | LT shares to withdraw. max_value(uint256) withdraws all. |
crvusd_from_wallet | bool | If 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:
| String | Cause |
|---|---|
Access | The caller is not the vault owner. |
Bad pool_id | The Factory has no market with this pool_id. |
Zero shares | There 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:
| Input | Type | Description |
|---|---|---|
pool_id | uint256 | Market index, as in Factory.markets(pool_id) |
pool_shares | uint256 | Amount 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:
| String | Cause |
|---|---|
Access | The caller is not the vault owner. |
Bad pool_id | The 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:
| Input | Type | Description |
|---|---|---|
pool_id | uint256 | Market index, as in Factory.markets(pool_id) |
gauge_shares | uint256 | Amount 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:
| String | Cause |
|---|---|
Access | The caller is not the vault owner. |
Bad pool_id | The 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:
| Input | Type | Description |
|---|---|---|
token | IERC20 | The 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:
| Input | Type | Description |
|---|---|---|
token | IERC20 | The 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:
| Input | Type | Description |
|---|---|---|
assets | uint256 | Amount 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:
| Input | Type | Description |
|---|---|---|
shares | uint256 | Amount 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:
| String | Cause |
|---|---|
Access | The caller is not the vault owner. |
Not enough crvUSD left | The 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:
| String | Cause |
|---|---|
Access | The caller is not the vault owner. |
Not enough crvUSD left | The 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:
| Input | Type | Description |
|---|---|---|
new_vault | IERC4626 | New crvUSD vault to use (must be on the allowlist) |
redeem | bool | If 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:
| String | Cause |
|---|---|
Only owner | The caller is not owner. |
Vault not allowed | new_vault is not on the allowlist of HybridVaultFactory. |
Has active positions | used_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:
| Input | Type | Description |
|---|---|---|
pool_id | uint256 | Market index, as in Factory.markets(pool_id) |
limit | uint256 | Personal 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:
| Input | Type | Description |
|---|---|---|
token | IERC20 | The 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:
| String | Cause |
|---|---|
Access | The caller is not the vault owner. |
Token not allowed | token 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:
| Input | Type | Description |
|---|---|---|
pool_id | uint256 | Market index, as in Factory.markets(pool_id) |
assets | uint256 | Amount of assets to deposit (0 to check the current state) |
debt | uint256 | Amount 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:
| Signature | Returns |
|---|---|
pool_limits(pool_id: uint256) -> uint256 | Effective pool limit: personal_limit[pool_id] plus HybridVaultFactory.pool_limits(pool_id). |
required_crvusd() -> uint256 | Total crvUSD backing that all positions need, after stablecoin_fraction. |
raw_required_crvusd_for(pool_id: uint256, assets: uint256, debt: uint256, downscale: bool = True) -> uint256 | crvUSD 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) -> uint256 | Additional 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) -> uint256 | crvUSD that a withdrawal of shares releases. |
assets_for_crvusd(pool_id: uint256, crvusd_amount: uint256) -> uint256 | The 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.
Related
- 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.