Skip to main content

Factory

Overview​

The Factory is the onchain market registry and deployer of YieldBasis. For each 2-coin Curve pool (stablecoin, asset), it deploys the contracts of one market and connects them. It also keeps the global protocol configuration. Use this reference to find the contracts of a market, or to audit market creation.

Each market has three core contracts and two optional helpers:

  • LT, the market's vault contract. It is the share token of the 2× leveraged position, and it gets an unlimited crvUSD approval from the Factory.
  • LEVAMM (contract AMM), the market's leveraged AMM. The borrow rate is in the LEVAMM, not in LT.
  • Price oracle (CryptopoolLPOracle), which returns the LP price of the Curve pool.
  • VirtualPool (optional), which routes swaps through the LEVAMM.
  • Gauge (optional), the market's staking contract (LiquidityGauge). The Factory names it staker.

The Factory stores the addresses of each market in a Market struct. markets[i] returns the struct, and market_count gives the number of markets.

The Factory deploys each contract from a blueprint implementation with Vyper's built-in create_from_blueprint function. It holds the implementation addresses amm_impl, lt_impl, and price_oracle_impl, and optionally virtual_pool_impl and staker_impl. add_market(...) runs these steps:

  1. It calls create_from_blueprint with the constructor arguments to deploy the Oracle, LT, and AMM. The price oracle uses the Curve pool and the current global aggregator agg. The LEVAMM uses LEVERAGE = 2e18, a per-market fee, and the market's price oracle.
  2. It sets them up: LT.set_amm(amm), LT.set_rate(rate), an unlimited crvUSD approval to LT, and LT.allocate_stablecoins(debt_ceiling). LT forwards the rate to the LEVAMM.
  3. If virtual_pool_impl and flash are not empty, it deploys the VirtualPool (create_from_blueprint(virtual_pool_impl, amm)). If staker_impl is not empty, it deploys the gauge (create_from_blueprint(staker_impl, lt)) and registers it on LT.

Only the Factory admin can change the implementations, with set_implementations(...). The change applies to the markets that add_market() creates later. On an existing market, fill_staker_vpool(i) can deploy a VirtualPool with the current implementation. It can also deploy a gauge when the market has none.


Pool policy and LP allowlist​

The Factory does not create the Curve pool. It accepts an existing Twocrypto pool in add_market(). The Factory checks two things: pool.coins(0) is the stablecoin, and pool.decimals() is 18. It does not check the pool code version, the pool policy, or the LP allowlist. This section summarizes the three.

Pool code versions​

Markets use two pool code versions. This table shows which version has a policy hook and an LP allowlist:

MarketsPool codePOLICY()LP allowlist
0–6Older Twocrypto code (version() = "v2.1.0d")Not present. A call reverts.Not present
7–10 (WBTC, cbBTC, tBTC v3 and YB-WETH)Twocrypto version() = "v3.0.0"PresentPresent

To find the version of a market, call markets(i).cryptopool.version() and POLICY(). A revert on POLICY() means that the pool has no policy hook.

Pool policy​

A pool policy is a separate contract that a v3.0.0 pool calls. The pool asks the policy for a swap fee with POLICY.get_fee(xp) and for a price_scale target with POLICY.get_price_scale(). A result of 0 keeps the pool's own fee curve (mid_fee, out_fee, fee_gamma) or its price_oracle target. The pool clamps any other result. After each state change, the pool pushes its state to the policy with POLICY.update_pool_state(...). price_scale sets the LP price that CryptopoolLPOracle returns, so a policy changes the price that the LEVAMM, LT, and YBNetPressure use. For how the oracle reads price_scale, read CryptopoolLPOracle.

Only the Curve Twocrypto factory admin, not Factory.admin() of YieldBasis, can call pool.set_policy_contract(policy). On mainnet, this admin is the pool admin proxy in Contract addresses. After initialization, only a Curve DAO vote can attach, replace, or remove a pool policy.

What a policy does depends on its type. To see which policy a pool runs, call POLICY() on the pool or read the table in Contract addresses.

  • On a pool that runs the oracle-report policy, YBOraclizedPolicy, the policy sets the swap fee from signed Pyth price reports and sets a price_scale target. It also runs the LT deviation check: it checks the debt ratio of each LT deposit into the market of the pool. For why a pool runs it, read Why a pool runs the oracle-report policy in Pool policies.
  • On a pool that runs YBTwocryptoPolicy, the policy uses only the pool's own prices. Its get_fee returns 0, so the pool keeps its native fee.

For the fee and price_scale rules, the state push, the access rules, and the LT deviation check, read Pool policy.

LP allowlist​

A v3.0.0 pool (markets 7 to 10) can restrict add_liquidity to listed addresses. The pools of markets 0 to 6 have no allowlist. The flag is lp_allowlist(empty(address)). When the flag is True, add_liquidity reverts with !wl for a caller that is not on the list. The pool does not check donations (donation=True). The pool factory admin changes the list with change_allowlist(add, remove).

LT and VirtualPool call add_liquidity on the pool. The allowlist of each market pool must list the market lt and virtual_pool. If one is missing, deposits or asset-to-stablecoin swaps revert with !wl. To check a market, call lp_allowlist(empty(address)), lp_allowlist(lt), and lp_allowlist(virtual_pool) on its pool.


Public variables​

The Factory has these public variables for contract state and configuration.

Constants

  • MAX_MARKETS (uint256) - Maximum number of markets that the Factory can create (50,000)
  • LEVERAGE (uint256) - Fixed leverage ratio for all markets (2× = 2e18)

Implementation addresses

  • amm_impl (address) - Current LEVAMM (AMM) implementation address
  • lt_impl (address) - Current LT implementation address
  • virtual_pool_impl (address) - Current VirtualPool implementation address
  • price_oracle_impl (address) - Current price oracle implementation address
  • staker_impl (address) - Current gauge implementation address

System configuration

  • STABLECOIN (IERC20) - The stablecoin of the protocol, crvUSD (immutable)
  • agg (address) - Current crvUSD price aggregator address
  • flash (address) - Current flash loan contract address
  • gauge_controller (address) - YieldBasis GaugeController address
  • fee_receiver (address) - Address that receives collected fees. To read it, call fee_receiver().
  • admin (address) - Primary admin address
  • emergency_admin (address) - Emergency admin address
  • min_admin_fee (uint256) - Admin fee that LT uses when no LT shares are staked (1e18-based). Default 10% (10**17).

Market management

  • markets (Market[MAX_MARKETS]) - Array of all created markets
  • market_count (uint256) - Current number of markets
  • allocators (HashMap[address, uint256]) - Stablecoin allocation limits per address
  • mint_factory (address) - crvUSD mint factory address

Data structs​

markets(i) returns a Market struct with these fields:

  • asset_token (IERC20) - Asset token contract address
  • cryptopool (CurveCryptoPool) - Curve pool contract address
  • amm (address) - LEVAMM (AMM) contract address
  • lt (address) - LT contract address
  • price_oracle (address) - Price oracle contract address
  • virtual_pool (address) - VirtualPool contract address (optional: the zero address if the market has none)
  • staker (address) - Gauge contract address (optional: the zero address if the market has none)

Events​

The Factory emits these events.

SetImplementations: The constructor and set_implementations() emit this event.

  • amm (address) - New LEVAMM (AMM) implementation address
  • lt (address) - New LT implementation address
  • virtual_pool (address) - New VirtualPool implementation address
  • price_oracle (address) - New price oracle implementation address
  • staker (address) - New gauge implementation address

SetAllocator: set_allocator() and set_mint_factory() emit this event.

  • allocator (address) - Address that can allocate stablecoins
  • amount (uint256) - Allocation amount

SetFeeReceiver: set_fee_receiver() emits this event.

  • fee_receiver (address) - New fee receiver address

SetAgg: set_agg() emits this event.

  • agg (address) - New price aggregator address

SetFlash: set_flash() emits this event.

  • flash (address) - New flash loan contract address

SetAdmin: set_admin() emits this event.

  • admin (address) - New admin address
  • emergency_admin (address) - New emergency admin address
  • old_admin (address) - Previous admin address
  • old_emergency_admin (address) - Previous emergency admin address

SetMinAdminFee: set_min_admin_fee() emits this event.

  • admin_fee (uint256) - New minimum admin fee

SetGaugeController: set_gauge_controller() emits this event.

  • gc (address) - New GaugeController address

MarketParameters: add_market() and fill_staker_vpool() emit this event.

  • idx (indexed uint256) - Market index
  • asset_token (indexed address) - Asset token address
  • cryptopool (indexed address) - Curve pool address
  • amm (address) - LEVAMM (AMM) contract address
  • lt (address) - LT contract address
  • price_oracle (address) - Price oracle address
  • virtual_pool (address) - VirtualPool address
  • staker (address) - Gauge address
  • agg (address) - Price aggregator address

Functions​

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

add_market()​

Creates a market for the asset of a Curve pool. The function deploys the contracts of the market (price oracle, LT, LEVAMM, VirtualPool, and gauge) and connects them, as the Overview describes. Only the Factory admin can call it.

The function returns the new Market struct with all contract addresses. It emits MarketParameters with all contract addresses and the market index.

The function takes these inputs:

InputTypeDescription
poolCurveCryptoPoolCurve pool for the asset
feeuint256LEVAMM trading fee (max 10%)
rateuint256Interest rate for the market, as a 1e18-based fraction per second. The LEVAMM maximum is 100% per year.
debt_ceilinguint256Maximum stablecoin borrowing limit
Contract source: add_market()

The contract source gives this function body:

@external
@nonreentrant
def add_market(
pool: CurveCryptoPool,
fee: uint256,
rate: uint256,
debt_ceiling: uint256
) -> Market:
assert msg.sender == self.admin, "Access"
assert staticcall pool.coins(0) == STABLECOIN.address, "Wrong stablecoin"
assert staticcall pool.decimals() == 18

market: Market = empty(Market)
agg: address = self.agg

market.asset_token = IERC20(staticcall pool.coins(1))
market.cryptopool = pool
market.price_oracle = create_from_blueprint(self.price_oracle_impl, pool.address, agg)
market.lt = create_from_blueprint(
self.lt_impl,
market.asset_token.address,
STABLECOIN,
pool.address,
self
)
market.amm = create_from_blueprint(
self.amm_impl,
market.lt,
STABLECOIN.address,
pool.address,
LEVERAGE,
fee,
market.price_oracle
)
extcall LT(market.lt).set_amm(market.amm)
extcall LT(market.lt).set_rate(rate)
assert extcall STABLECOIN.approve(market.lt, max_value(uint256), default_return_value=True)
extcall LT(market.lt).allocate_stablecoins(debt_ceiling)

if self.virtual_pool_impl != empty(address) and self.flash != empty(address):
market.virtual_pool = create_from_blueprint(
self.virtual_pool_impl,
market.amm
)
if self.staker_impl != empty(address):
market.staker = create_from_blueprint(
self.staker_impl,
market.lt)
extcall LT(market.lt).set_staker(market.staker)

i: uint256 = self.market_count
assert i < MAX_MARKETS, "Too many markets"
self.market_count = i + 1
self.markets[i] = market

log MarketParameters(
idx=i,
asset_token=market.asset_token.address,
cryptopool=market.cryptopool.address,
amm=market.amm,
lt=market.lt,
price_oracle=market.price_oracle,
virtual_pool=market.virtual_pool,
staker=market.staker,
agg=agg
)

return market

add_market() reverts with these strings:

StringCause
AccessThe caller is not admin.
Wrong stablecoinpool.coins(0) is not the Factory STABLECOIN.
Too many marketsmarket_count is already MAX_MARKETS (50,000).

Reverts without a message: pool.decimals() is not 18, or the crvUSD approve to the new LT fails.

The setup calls into the new LEVAMM and LT can also revert with their own strings:

  • Fee too high: fee is more than AMM.MAX_FEE.
  • Rate too high: rate is more than the LEVAMM maximum.

fill_staker_vpool()​

Adds or updates the helpers of an existing market. Only the Factory admin can call it.

The function deploys a new VirtualPool in two cases:

  • The market has no VirtualPool, and virtual_pool_impl and flash are not empty.
  • The IMPL() of the existing VirtualPool differs from virtual_pool_impl. This path does not check flash. It reverts if virtual_pool_impl is empty.

The function deploys a gauge only when the market has none. It does not replace an existing gauge.

The function returns nothing. It emits MarketParameters with the updated contract addresses.

The function takes this input:

InputTypeDescription
iuint256Market index to update
Contract source: fill_staker_vpool()

The contract source gives this function body:

@external
def fill_staker_vpool(i: uint256):
assert msg.sender == self.admin, "Access"
assert i < self.market_count, "Nonexistent market"

market: Market = self.markets[i]
assert market.lt != empty(address)
assert market.amm != empty(address)

new_virtual_pool: bool = False
if market.virtual_pool == empty(address):
new_virtual_pool = self.virtual_pool_impl != empty(address) and self.flash != empty(address)
else:
new_virtual_pool = (staticcall VirtualPool(market.virtual_pool).IMPL()) != self.virtual_pool_impl

if new_virtual_pool:
market.virtual_pool = create_from_blueprint(
self.virtual_pool_impl,
market.amm
)

if market.staker == empty(address) and self.staker_impl != empty(address):
market.staker = create_from_blueprint(
self.staker_impl,
market.lt)
extcall LT(market.lt).set_staker(market.staker)

self.markets[i] = market

log MarketParameters(
idx=i,
asset_token=market.asset_token.address,
cryptopool=market.cryptopool.address,
amm=market.amm,
lt=market.lt,
price_oracle=market.price_oracle,
virtual_pool=market.virtual_pool,
staker=market.staker,
agg=(staticcall LPOracle(market.price_oracle).AGG())
)

fill_staker_vpool() reverts with these strings:

StringCause
AccessThe caller is not admin.
Nonexistent marketi is not less than market_count.

Reverts without a message: the market has an empty lt or amm.


set_mint_factory()​

Sets the crvUSD mint factory address. It gives the mint factory an unlimited approval to withdraw stablecoins from the Factory. Only the Factory admin can call it, and only once.

The function returns nothing. It emits SetAllocator for the mint factory with an unlimited amount.

The function takes this input:

InputTypeDescription
mint_factoryaddressAddress of the crvUSD mint factory
Contract source: set_mint_factory()

The contract source gives this function body:

@external
@nonreentrant
def set_mint_factory(mint_factory: address):
assert msg.sender == self.admin, "Access"
assert self.mint_factory == empty(address), "Only set once"
assert mint_factory != empty(address)
self.mint_factory = mint_factory
# crvUSD factory can take back as much as it wants. Very important function - this is why it can be called only once
assert extcall STABLECOIN.approve(mint_factory, max_value(uint256), default_return_value=True)

log SetAllocator(allocator=mint_factory, amount=max_value(uint256))

set_mint_factory() reverts with these strings:

StringCause
AccessThe caller is not admin.
Only set oncemint_factory is already set.

Reverts without a message: the mint_factory argument is the zero address, or the crvUSD approve fails.


set_allocator()​

Records how much crvUSD an allocator supplies to the Factory. An increase pulls the difference from the allocator with transferFrom. A decrease raises the allowance of the allocator, so that the allocator can take back the difference. Only the Factory admin can call it.

The function returns nothing. It emits SetAllocator with the allocator address and the new amount.

The function takes these inputs:

InputTypeDescription
allocatoraddressAddress to allocate stablecoins to
amountuint256New allocation amount
Contract source: set_allocator()

The contract source gives this function body:

@external
@nonreentrant
def set_allocator(allocator: address, amount: uint256):
assert msg.sender == self.admin, "Access"
assert allocator != self.mint_factory, "Minter"
assert allocator != empty(address)

old_allocation: uint256 = self.allocators[allocator]
if amount > old_allocation:
# Use transferFrom
assert extcall STABLECOIN.transferFrom(allocator, self, amount - old_allocation, default_return_value=True)
self.allocators[allocator] = amount

elif amount < old_allocation:
# Allow to take back the allocation via transferFrom, but not more than the allocation reduction
assert extcall STABLECOIN.approve(allocator, (staticcall STABLECOIN.allowance(self, allocator)) + old_allocation - amount, default_return_value=True)
self.allocators[allocator] = amount

log SetAllocator(allocator=allocator, amount=amount)

set_allocator() reverts with these strings:

StringCause
AccessThe caller is not admin.
Minterallocator is the mint_factory. While mint_factory is unset, this includes the zero address.

Reverts without a message: allocator is the zero address after mint_factory is set, or the crvUSD transferFrom or approve fails.


set_agg()​

Sets and validates the aggregator for markets that add_market() creates after this call. Only the Factory admin can call it.

Existing markets keep their own aggregator, for two reasons:

  • CryptopoolLPOracle.AGG is immutable.
  • LT reads its agg from the price oracle once, in set_amm().

The net-pressure contracts PID and MerklPIDDriver read Factory.agg() at each call. They use the new value at once.

The function returns nothing. It emits SetAgg with the new aggregator address.

The function takes this input:

InputTypeDescription
aggaddressNew price aggregator address
Contract source: set_agg()

The contract source gives this function body:

@external
def set_agg(agg: address):
assert msg.sender == self.admin, "Access"
assert agg != empty(address)
self.agg = agg
self._validate_agg()
log SetAgg(agg=agg)

set_agg() reverts with these strings:

StringCause
AccessThe caller is not admin.
Bad aggregatoragg.price() is not strictly between 9 * 10**17 and 11 * 10**17 (0.90 and 1.10).

Reverts without a message: agg is the zero address.


set_flash()​

Sets the flash loan contract address. VirtualPools use this flash loan provider for arbitrage. Only the Factory admin can call it.

The function returns nothing. It emits SetFlash with the new contract address.

The function takes this input:

InputTypeDescription
flashaddressNew flash loan contract address
Contract source: set_flash()

The contract source gives this function body:

@external
def set_flash(flash: address):
assert msg.sender == self.admin, "Access"
self.flash = flash
log SetFlash(flash=flash)

set_flash() reverts with these strings:

StringCause
AccessThe caller is not admin.

set_admin()​

Sets the new admin and emergency admin addresses. Use it to transfer administrative control of the Factory. Only the Factory admin can call it.

The function returns nothing. It emits SetAdmin with the new and old admin addresses.

The function takes these inputs:

InputTypeDescription
new_adminaddressNew admin address
new_emergency_adminaddressNew emergency admin address
Contract source: set_admin()

The contract source gives this function body:

@external
def set_admin(new_admin: address, new_emergency_admin: address):
assert msg.sender == self.admin, "Access"
assert new_admin != empty(address)
assert new_emergency_admin != empty(address)
log SetAdmin(admin=new_admin, emergency_admin=new_emergency_admin, old_admin=self.admin, old_emergency_admin=self.emergency_admin)
self.admin = new_admin
self.emergency_admin = new_emergency_admin

set_admin() reverts with these strings:

StringCause
AccessThe caller is not admin.

Reverts without a message: new_admin or new_emergency_admin is the zero address.


set_fee_receiver()​

Sets the fee receiver address. Collected fees go to this address. Only the Factory admin can call it.

The function returns nothing. It emits SetFeeReceiver with the new receiver address.

The function takes this input:

InputTypeDescription
new_fee_receiveraddressNew fee receiver address
Contract source: set_fee_receiver()

The contract source gives this function body:

@external
def set_fee_receiver(new_fee_receiver: address):
assert msg.sender == self.admin, "Access"
self.fee_receiver = new_fee_receiver
log SetFeeReceiver(fee_receiver=new_fee_receiver)

set_fee_receiver() reverts with these strings:

StringCause
AccessThe caller is not admin.

set_gauge_controller()​

Sets the address of the YieldBasis GaugeController for staking rewards. Only the Factory admin can call it, and only once.

The function returns nothing. It emits SetGaugeController with the new controller address.

The function takes this input:

InputTypeDescription
gcaddressGauge controller address
Contract source: set_gauge_controller()

The contract source gives this function body:

@external
def set_gauge_controller(gc: address):
assert msg.sender == self.admin, "Access"
assert self.gauge_controller == empty(address), "Already set"
self.gauge_controller = gc
log SetGaugeController(gc=gc)

set_gauge_controller() reverts with these strings:

StringCause
AccessThe caller is not admin.
Already setgauge_controller is already set.

set_min_admin_fee()​

Sets the admin fee that LT uses when no LT shares are staked. LT raises the admin fee toward 100% as the staked fraction grows. Only the Factory admin can call it.

The function returns nothing. It emits SetMinAdminFee with the new fee rate.

The function takes this input:

InputTypeDescription
new_min_admin_feeuint256New minimum admin fee (max 100%)
Contract source: set_min_admin_fee()

The contract source gives this function body:

@external
def set_min_admin_fee(new_min_admin_fee: uint256):
assert msg.sender == self.admin, "Access"
assert new_min_admin_fee <= 10**18, "Admin fee too high"
self.min_admin_fee = new_min_admin_fee
log SetMinAdminFee(admin_fee=new_min_admin_fee)

set_min_admin_fee() reverts with these strings:

StringCause
AccessThe caller is not admin.
Admin fee too highnew_min_admin_fee is more than 10**18 (100%).

set_implementations()​

Updates the contract implementations for later deployments. The admin sets new implementation addresses for the LEVAMM, LT, VirtualPool, price oracle, and gauge contracts. Each input is optional: for an input that is the zero address, the Factory keeps the current implementation. Only the Factory admin can call it.

The function returns nothing. It emits SetImplementations with the input addresses.

The function takes these inputs:

InputTypeDescription
ammaddressNew LEVAMM (AMM) implementation address, or the zero address to keep the current one
ltaddressNew LT implementation address, or the zero address to keep the current one
virtual_pooladdressNew VirtualPool implementation address, or the zero address to keep the current one
price_oracleaddressNew price oracle implementation address, or the zero address to keep the current one
stakeraddressNew gauge implementation address, or the zero address to keep the current one
Contract source: set_implementations()

The contract source gives this function body:

@external
def set_implementations(amm: address, lt: address, virtual_pool: address, price_oracle: address, staker: address):
assert msg.sender == self.admin, "Access"
if amm != empty(address):
self.amm_impl = amm
if lt != empty(address):
self.lt_impl = lt
if virtual_pool != empty(address):
self.virtual_pool_impl = virtual_pool
if price_oracle != empty(address):
self.price_oracle_impl = price_oracle
if staker != empty(address):
self.staker_impl = staker
log SetImplementations(amm=amm, lt=lt, virtual_pool=virtual_pool, price_oracle=price_oracle, staker=staker)

set_implementations() reverts with these strings:

StringCause
AccessThe caller is not admin.
  • LT: the vault contract that add_market() deploys and sets up for each market.
  • LEVAMM: the leveraged AMM of each market, which holds the borrow rate.
  • VirtualPool: the optional swap helper that the Factory deploys.
  • CryptopoolLPOracle: the price oracle that add_market() deploys for each market.
  • Pool policy: the policy hook of the Curve pool, which the Factory does not check.
  • Contract addresses: the deployed Factory and the contracts of each market.