Skip to main content

HybridFactoryOwner

Overview​

HybridFactoryOwner is the admin of the YieldBasis Factory. Governance calls a wrapper on this contract. The wrapper forwards the call to the Factory or to an LT, the market's vault contract. Use this reference to check what each wrapper forwards, and who can allocate stablecoins to an LT.

Factory.emergency_admin() can also kill a market directly, without this contract. For market discovery, read the Factory directly.

To find this contract, call admin() on the Factory. The NatSpec of the contract says "HybridVaultFactory", but the contract that it manages is the YieldBasis Factory.

The contract has three main features:

  • One access check. All setters check msg.sender == ADMIN. The exception is lt_allocate_stablecoins. It also accepts limit setters, and it accepts any caller for a disabled LT.
  • Limit setters. limit_setters lets a contract call lt_allocate_stablecoins without other admin rights. Limit-setter management names the contracts that need this role.
  • Permissionless deallocation of disabled LTs. When ADMIN calls lt_allocate_stablecoins(lt, 0), the contract sets disabled_lts[lt] and deallocates in the same call. After that, any caller can repeat the deallocation.

Immutable variables​

The contract sets these values in the constructor.

NameTypeNotes
ADMINaddressGovernance address that owns this proxy. It does not change after deployment. On the deployed contract, ADMIN is the YB DAO, which Contract addresses lists.
FACTORYFactoryThe YieldBasis Factory that this proxy controls.
STABLECOINIERC20crvUSD, read from factory.STABLECOIN() at deployment.

Storage​

The contract keeps two maps.

NameTypeDescription
disabled_ltsHashMap[LT, bool]Set to True when ADMIN calls lt_allocate_stablecoins(lt, 0). Set to False when ADMIN calls it with a limit that is not 0. Only ADMIN changes this flag. LTMigrator refuses to migrate into a disabled LT.
limit_settersHashMap[address, bool]Contracts that can call lt_allocate_stablecoins on an LT that is not disabled.

Events​

set_limit_setter emits SetLimitSetter, the only event of the contract. The event has these fields:

  • setter (address): address whose privilege changed
  • enabled (bool): true to grant, false to revoke

Functions​

The functions fall into five groups: LT controls, stablecoin allocation, Factory passthroughs, limit-setter management, and public views.

Privileged LT controls​

All functions in this table require msg.sender == ADMIN.

FunctionEffect
lt_set_rate(lt, rate)Forwards to LT.set_rate(rate). The rate is a 1e18-based fraction per second. The maximum is 10**18 // 31_536_000 (100% per year). For example, rate = 3_170_979_198 is 10% per year (10**17 // 31_536_000).
lt_set_amm_fee(lt, fee)Forwards to LT.set_amm_fee(fee). The fee is 1e18-based (1e18 = 100%). The maximum is AMM.MAX_FEE (10%).
lt_set_killed(lt, is_killed)Kills or unkills the market through LT.set_killed. In a killed market, deposit() and withdraw() revert. Holders exit with emergency_withdraw().
lt_distribute_borrower_fees(lt, discount)Forwards to LT.distribute_borrower_fees(discount). Anyone can call LT directly with the default discount. This wrapper lets ADMIN pass a larger discount.

Stablecoin allocation​

lt_allocate_stablecoins(lt: address, limit: uint256 = max_value(uint256)) sets the stablecoin allocation of an LT. It is @nonreentrant and returns nothing.

The function first calculates available_limit, the 95% floor. This value is 95% of the LP-priced collateral of the market's LEVAMM (contract AMM): 0.95 · lp_price · AMM.collateral_amount(). The behavior then depends on the caller and on disabled_lts[lt]:

CallerLT statelimitBehavior
ADMINanynot 0Sets disabled_lts[lt] = False. Calls LT.allocate_stablecoins(max(limit, available_limit)).
ADMINany0Sets disabled_lts[lt] = True. Then runs the deallocation branch (the row for a disabled LT) in the same call.
Limit setternot disabledanyCalls LT.allocate_stablecoins(max(limit, available_limit)). The flag does not change.
Limit setter or any other callerdisabledanyDeallocation branch. If available_limit < LT.stablecoin_allocated(), the call requires available_limit to be at least 75% of AMM.value_oracle().value (else "Not enough reserves"). Then it sets the allocation to available_limit. Otherwise, the call does nothing.
Any other callernot disabledanyReverts with "Access".

Because of the max(limit, available_limit) rule, a limit setter or ADMIN cannot set the allocation of a live LT below the 95% floor. With the default limit = max_value(uint256), LT keeps its stored stablecoin_allocation as the target. It fills its allocation up to that value.

The two 75% bounds in the system are different:

  • HybridFactoryOwner checks available_limit against 75% of the LEVAMM value (AMM.value_oracle().value).
  • LT.allocate_stablecoins limits a decrease to 75% of the LP-priced collateral value (lp_price · collateral_amount · 3/4).
Contract source: lt_allocate_stablecoins()

The contract source gives this function body:

@nonreentrant
@external
def lt_allocate_stablecoins(lt: LT, limit: uint256 = max_value(uint256)):
amm: AMM = staticcall lt.amm()
lp_price: uint256 = extcall (staticcall amm.PRICE_ORACLE_CONTRACT()).price_w()
# Set to no less than 95% of collateral value
available_limit: uint256 = lp_price * (staticcall amm.collateral_amount()) // 10**18 * 95 // 100

# Handle enable/disable
if msg.sender == ADMIN:
self.disabled_lts[lt] = (limit == 0)

if not self.disabled_lts[lt]:
assert msg.sender == ADMIN or self.limit_setters[msg.sender], "Access"
self.disabled_lts[lt] = False
extcall lt.allocate_stablecoins(max(limit, available_limit))

else:
# Deallocate as much as available, and allow anyone to do it
allocated: uint256 = staticcall lt.stablecoin_allocated()
safe_limit: uint256 = (staticcall amm.value_oracle()).value * 3 // 4
if available_limit < allocated: # Do not revert if we have less but do nothing
assert available_limit >= safe_limit, "Not enough reserves"
extcall lt.allocate_stablecoins(available_limit)

Factory passthroughs​

All functions in this table require msg.sender == ADMIN. Each function forwards one call.

FunctionForwards to
transfer_ownership_back()FACTORY.set_admin(ADMIN, FACTORY.emergency_admin()). Gives the Factory admin role back to ADMIN directly.
add_market(pool, fee, rate, debt_ceiling)FACTORY.add_market(...)
set_fee_receiver(addr)FACTORY.set_fee_receiver(addr)
set_implementations(amm, lt, virtual_pool, price_oracle, staker)FACTORY.set_implementations(...)
set_min_admin_fee(value)FACTORY.set_min_admin_fee(value)
fill_staker_vpool(i)FACTORY.fill_staker_vpool(i)
set_allocator(allocator, amount)FACTORY.set_allocator(...)
set_agg(agg)FACTORY.set_agg(agg)
set_flash(flash)FACTORY.set_flash(flash)

Limit-setter management​

set_limit_setter sets limit_setters[setter] and emits SetLimitSetter. Only ADMIN can call it. The function has this signature:

@external
def set_limit_setter(setter: address, enabled: bool)

To check an address, call limit_setters(address). Two contracts need the limit-setter role:

  • The HybridVaultFactory. HybridVaults do not call this contract directly. A HybridVault calls HybridVaultFactory.lt_allocate_stablecoins, and the factory forwards the call. So the caller at HybridFactoryOwner is the HybridVaultFactory, and that address must be a limit setter. If governance revokes the HybridVaultFactory, no HybridVault can change the allocation of a live market.
  • The LTMigrator. A new LTMigrator must also be a limit setter.

Public views​

The contract has these public views.

FunctionReturns
lt_in_factory(lt)True if STABLECOIN.allowance(FACTORY, lt) > 0. The Factory gives this allowance to each LT that it deploys. LTMigrator calls this view.
lt_needs_withdraw(lt)The number of LT shares to withdraw so that lp_price · collateral_amount falls to stablecoin_allocated. Returns 0 if no withdrawal is necessary. This view uses price() and does not apply the 95% factor.

Common revert strings​

The wrappers can revert with these strings.

StringCause
AccessA wrapper other than lt_allocate_stablecoins: the caller is not ADMIN. lt_allocate_stablecoins: the LT is not disabled, and the caller is not ADMIN or a limit setter.
Not enough reservesDeallocation of a disabled LT: available_limit (95% of the LP-priced collateral) is less than 75% of the LEVAMM value.
  • Factory: the contract owned by this proxy.
  • LT: the market vault contract that receives all the lt_* forwards.
  • HybridVaultFactory: the limit setter that forwards HybridVault calls.
  • LTMigrator: a limit setter that reads lt_in_factory and disabled_lts.
  • MigrationFactoryOwner: an earlier admin proxy. It has no limit_setters, no lt_distribute_borrower_fees, no 95% floor, and no @nonreentrant.