Factory
Source: Factory.vy on GitHub
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 borrowrateis 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 itstaker.
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:
- It calls
create_from_blueprintwith the constructor arguments to deploy theOracle,LT, andAMM. The price oracle uses the Curve pool and the current global aggregatoragg. The LEVAMM usesLEVERAGE = 2e18, a per-marketfee, and the market's price oracle. - It sets them up:
LT.set_amm(amm),LT.set_rate(rate), an unlimited crvUSD approval to LT, andLT.allocate_stablecoins(debt_ceiling). LT forwards therateto the LEVAMM. - If
virtual_pool_implandflashare not empty, it deploys the VirtualPool (create_from_blueprint(virtual_pool_impl, amm)). Ifstaker_implis 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:
| Markets | Pool code | POLICY() | LP allowlist |
|---|---|---|---|
| 0–6 | Older 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" | Present | Present |
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 aprice_scaletarget. 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. Itsget_feereturns 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 addresslt_impl(address) - Current LT implementation addressvirtual_pool_impl(address) - Current VirtualPool implementation addressprice_oracle_impl(address) - Current price oracle implementation addressstaker_impl(address) - Current gauge implementation address
System configuration
STABLECOIN(IERC20) - The stablecoin of the protocol, crvUSD (immutable)agg(address) - Current crvUSD price aggregator addressflash(address) - Current flash loan contract addressgauge_controller(address) - YieldBasisGaugeControlleraddressfee_receiver(address) - Address that receives collected fees. To read it, callfee_receiver().admin(address) - Primary admin addressemergency_admin(address) - Emergency admin addressmin_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 marketsmarket_count(uint256) - Current number of marketsallocators(HashMap[address, uint256]) - Stablecoin allocation limits per addressmint_factory(address) - crvUSD mint factory address
Data structs
markets(i) returns a Market struct with these fields:
asset_token(IERC20) - Asset token contract addresscryptopool(CurveCryptoPool) - Curve pool contract addressamm(address) - LEVAMM (AMM) contract addresslt(address) - LT contract addressprice_oracle(address) - Price oracle contract addressvirtual_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 addresslt(address) - New LT implementation addressvirtual_pool(address) - New VirtualPool implementation addressprice_oracle(address) - New price oracle implementation addressstaker(address) - New gauge implementation address
SetAllocator: set_allocator() and set_mint_factory() emit this event.
allocator(address) - Address that can allocate stablecoinsamount(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 addressemergency_admin(address) - New emergency admin addressold_admin(address) - Previous admin addressold_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) - NewGaugeControlleraddress
MarketParameters: add_market() and fill_staker_vpool() emit this event.
idx(indexed uint256) - Market indexasset_token(indexed address) - Asset token addresscryptopool(indexed address) - Curve pool addressamm(address) - LEVAMM (AMM) contract addresslt(address) - LT contract addressprice_oracle(address) - Price oracle addressvirtual_pool(address) - VirtualPool addressstaker(address) - Gauge addressagg(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:
| Input | Type | Description |
|---|---|---|
pool | CurveCryptoPool | Curve pool for the asset |
fee | uint256 | LEVAMM trading fee (max 10%) |
rate | uint256 | Interest rate for the market, as a 1e18-based fraction per second. The LEVAMM maximum is 100% per year. |
debt_ceiling | uint256 | Maximum 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:
| String | Cause |
|---|---|
Access | The caller is not admin. |
Wrong stablecoin | pool.coins(0) is not the Factory STABLECOIN. |
Too many markets | market_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:feeis more thanAMM.MAX_FEE.Rate too high:rateis 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_implandflashare not empty. - The
IMPL()of the existing VirtualPool differs fromvirtual_pool_impl. This path does not checkflash. It reverts ifvirtual_pool_implis 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:
| Input | Type | Description |
|---|---|---|
i | uint256 | Market 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:
| String | Cause |
|---|---|
Access | The caller is not admin. |
Nonexistent market | i 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:
| Input | Type | Description |
|---|---|---|
mint_factory | address | Address 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:
| String | Cause |
|---|---|
Access | The caller is not admin. |
Only set once | mint_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:
| Input | Type | Description |
|---|---|---|
allocator | address | Address to allocate stablecoins to |
amount | uint256 | New 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:
| String | Cause |
|---|---|
Access | The caller is not admin. |
Minter | allocator 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.AGGis immutable.- LT reads its
aggfrom the price oracle once, inset_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:
| Input | Type | Description |
|---|---|---|
agg | address | New 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:
| String | Cause |
|---|---|
Access | The caller is not admin. |
Bad aggregator | agg.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:
| Input | Type | Description |
|---|---|---|
flash | address | New 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:
| String | Cause |
|---|---|
Access | The 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:
| Input | Type | Description |
|---|---|---|
new_admin | address | New admin address |
new_emergency_admin | address | New 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:
| String | Cause |
|---|---|
Access | The 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:
| Input | Type | Description |
|---|---|---|
new_fee_receiver | address | New 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:
| String | Cause |
|---|---|
Access | The 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:
| Input | Type | Description |
|---|---|---|
gc | address | Gauge 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:
| String | Cause |
|---|---|
Access | The caller is not admin. |
Already set | gauge_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:
| Input | Type | Description |
|---|---|---|
new_min_admin_fee | uint256 | New 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:
| String | Cause |
|---|---|
Access | The caller is not admin. |
Admin fee too high | new_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:
| Input | Type | Description |
|---|---|---|
amm | address | New LEVAMM (AMM) implementation address, or the zero address to keep the current one |
lt | address | New LT implementation address, or the zero address to keep the current one |
virtual_pool | address | New VirtualPool implementation address, or the zero address to keep the current one |
price_oracle | address | New price oracle implementation address, or the zero address to keep the current one |
staker | address | New 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:
| String | Cause |
|---|---|
Access | The caller is not admin. |
Related
- 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.