use crate::{
apr_to_tick, buyer_assets_to_units, max_lif, seller_assets_to_units, u256_to_word,
validate_offer, word_from_u64, word_to_u256, Address, CollateralParams, Market, Offer,
OfferError, SimError, SizingError, ValidateCtx, Word, DEFAULT_TICK_SPACING, U256,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum BuildError {
#[error(transparent)]
Sim(#[from] SimError),
#[error(transparent)]
Sizing(#[from] SizingError),
#[error("computed units exceed u128")]
UnitsOverflow,
#[error("offer side never set")]
SideNotSet,
#[error("offer tick never set")]
TickNotSet,
#[error("offer expiry never set")]
ExpiryNotSet,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum MarketBuildError {
#[error("market must configure at least one collateral token")]
NoCollateralParams,
#[error("market configures too many collateral tokens: {0}")]
TooManyCollateralParams(usize),
#[error("collateral token must not be the zero address")]
ZeroCollateralToken,
#[error("duplicate collateral token: {0:?}")]
DuplicateCollateralToken(Address),
#[error("collateral {0:?} has an LLTV above WAD")]
InvalidLltv(Address),
#[error("collateral {0:?} has a liquidation cursor at or above WAD")]
InvalidLiquidationCursor(Address),
#[error("collateral {0:?} has an invalid maximum liquidation incentive factor")]
InvalidMaxLif(Address),
#[error("collateral {0:?} violates the LLTV/maximum-LIF product bound")]
MaxLifTooHigh(Address),
}
#[derive(Clone, Debug)]
#[must_use = "call `.build()` or `.build_checked()` to produce the Market"]
pub struct MarketBuilder {
chain_id: U256,
midnight: Address,
loan_token: Address,
collateral_params: Vec<CollateralParams>,
maturity: U256,
rcf_threshold: U256,
enter_gate: Address,
liquidator_gate: Address,
}
impl MarketBuilder {
pub fn new(chain_id: u64, midnight: Address, loan_token: Address) -> Self {
Self {
chain_id: U256::from(chain_id),
midnight,
loan_token,
collateral_params: Vec::new(),
maturity: U256::ZERO,
rcf_threshold: U256::ZERO,
enter_gate: [0u8; 20],
liquidator_gate: [0u8; 20],
}
}
pub fn collateral(
mut self,
token: Address,
lltv: U256,
liquidation_cursor: U256,
oracle: Address,
) -> Self {
self.collateral_params.push(CollateralParams {
token,
lltv: u256_to_word(lltv),
liquidation_cursor: u256_to_word(liquidation_cursor),
oracle,
});
self
}
pub fn maturity(mut self, unix_secs: u64) -> Self {
self.maturity = U256::from(unix_secs);
self
}
pub fn rcf_threshold(mut self, v: U256) -> Self {
self.rcf_threshold = v;
self
}
pub fn enter_gate(mut self, gate: Address) -> Self {
self.enter_gate = gate;
self
}
pub fn liquidator_gate(mut self, gate: Address) -> Self {
self.liquidator_gate = gate;
self
}
pub fn build(mut self) -> Market {
self.collateral_params
.sort_by_key(|collateral| collateral.token);
Market {
chain_id: u256_to_word(self.chain_id),
midnight: self.midnight,
loan_token: self.loan_token,
collateral_params: self.collateral_params,
maturity: u256_to_word(self.maturity),
rcf_threshold: u256_to_word(self.rcf_threshold),
enter_gate: self.enter_gate,
liquidator_gate: self.liquidator_gate,
}
}
pub fn build_checked(self) -> Result<Market, MarketBuildError> {
let market = self.build();
if market.collateral_params.is_empty() {
return Err(MarketBuildError::NoCollateralParams);
}
if market.collateral_params.len() > crate::MAX_COLLATERALS {
return Err(MarketBuildError::TooManyCollateralParams(
market.collateral_params.len(),
));
}
if market
.collateral_params
.iter()
.any(|collateral| collateral.token == [0u8; 20])
{
return Err(MarketBuildError::ZeroCollateralToken);
}
if let Some(pair) = market
.collateral_params
.windows(2)
.find(|pair| pair[0].token == pair[1].token)
{
return Err(MarketBuildError::DuplicateCollateralToken(pair[0].token));
}
let wad = U256::from(1_000_000_000_000_000_000u128);
let max_product = U256::from(999_000_000_000_000_000u128) * wad;
for collateral in &market.collateral_params {
let lltv = word_to_u256(&collateral.lltv);
let cursor = word_to_u256(&collateral.liquidation_cursor);
if lltv > wad {
return Err(MarketBuildError::InvalidLltv(collateral.token));
}
if cursor >= wad {
return Err(MarketBuildError::InvalidLiquidationCursor(collateral.token));
}
let Some(max_lif) = max_lif(lltv, cursor) else {
return Err(MarketBuildError::InvalidMaxLif(collateral.token));
};
if max_lif > U256::from(2u8) * wad {
return Err(MarketBuildError::InvalidMaxLif(collateral.token));
}
if lltv != wad && lltv * max_lif > max_product {
return Err(MarketBuildError::MaxLifTooHigh(collateral.token));
}
}
Ok(market)
}
}
#[derive(Clone, Debug)]
#[must_use = "call `.build()` or `.try_build(..)` to produce the Offer"]
pub struct OfferBuilder {
market: Market,
buy: bool,
side_set: bool,
maker: Address,
start: U256,
expiry: U256,
expiry_set: bool,
tick: U256,
tick_set: bool,
group: Word,
callback: Address,
callback_data: Vec<u8>,
receiver_if_maker_is_seller: Address,
receiver_set: bool,
ratifier: Address,
reduce_only: bool,
max_units: u128,
max_assets: u128,
continuous_fee_cap: U256,
error: Option<BuildError>,
}
impl OfferBuilder {
pub fn new(market: Market, maker: Address) -> Self {
Self {
market,
buy: true,
side_set: false,
maker,
start: U256::ZERO,
expiry: U256::ZERO,
expiry_set: false,
tick: U256::ZERO,
tick_set: false,
group: [0u8; 32],
callback: [0u8; 20],
callback_data: Vec::new(),
receiver_if_maker_is_seller: [0u8; 20],
receiver_set: false,
ratifier: [0u8; 20],
reduce_only: false,
max_units: 0,
max_assets: 0,
continuous_fee_cap: U256::ZERO,
error: None,
}
}
fn record_err(&mut self, e: BuildError) {
if self.error.is_none() {
self.error = Some(e);
}
}
pub fn side(mut self, buy: bool) -> Self {
self.buy = buy;
self.side_set = true;
if !self.receiver_set {
self.receiver_if_maker_is_seller = if buy { [0u8; 20] } else { self.maker };
}
self
}
pub fn buy(self) -> Self {
self.side(true)
}
pub fn sell(self) -> Self {
self.side(false)
}
pub fn lend(self) -> Self {
self.side(true)
}
pub fn borrow(self) -> Self {
self.side(false)
}
pub fn tick(mut self, tick: u64) -> Self {
self.tick = U256::from(tick);
self.tick_set = true;
self
}
pub fn start(mut self, unix_secs: u64) -> Self {
self.start = U256::from(unix_secs);
self
}
pub fn expiry(mut self, unix_secs: u64) -> Self {
self.expiry = U256::from(unix_secs);
self.expiry_set = true;
self
}
pub fn group(mut self, group: Word) -> Self {
self.group = group;
self
}
pub fn group_u64(mut self, group: u64) -> Self {
self.group = word_from_u64(group);
self
}
pub fn ratifier(mut self, ratifier: Address) -> Self {
self.ratifier = ratifier;
self
}
pub fn callback(mut self, callback: Address, data: Vec<u8>) -> Self {
self.callback = callback;
self.callback_data = data;
self
}
pub fn receiver_if_maker_is_seller(mut self, receiver: Address) -> Self {
self.receiver_if_maker_is_seller = receiver;
self.receiver_set = true;
self
}
pub fn reduce_only(mut self, reduce_only: bool) -> Self {
self.reduce_only = reduce_only;
self
}
pub fn max_units(mut self, max_units: u128) -> Self {
self.max_units = max_units;
self.max_assets = 0;
self
}
pub fn max_assets(mut self, max_assets: u128) -> Self {
self.max_assets = max_assets;
self.max_units = 0;
self
}
pub fn continuous_fee_cap(mut self, cap: U256) -> Self {
self.continuous_fee_cap = cap;
self
}
pub fn apr(mut self, apr_pct: f64, now: u64) -> Self {
let maturity = word_to_u256(&self.market.maturity);
let ttm = maturity.saturating_sub(U256::from(now));
let ttm_secs = u64::try_from(ttm).unwrap_or(u64::MAX);
match apr_to_tick(apr_pct, ttm_secs, DEFAULT_TICK_SPACING as u64) {
Ok(tick) => {
self.tick = U256::from(tick);
self.tick_set = true;
}
Err(e) => self.record_err(e.into()),
}
self
}
pub fn assets(mut self, target_assets: u128, now: u64, cbps: [u16; 7]) -> Self {
let offer = self.clone().build();
let target = U256::from(target_assets);
let res = if self.buy {
buyer_assets_to_units(&offer, target, now, cbps)
} else {
seller_assets_to_units(&offer, target, now, cbps)
};
match res {
Ok(units) => match u128::try_from(units) {
Ok(u) => {
self.max_units = u;
self.max_assets = 0;
}
Err(_) => self.record_err(BuildError::UnitsOverflow),
},
Err(e) => self.record_err(e.into()),
}
self
}
pub fn build_checked(self) -> Result<Offer, BuildError> {
if let Some(e) = self.error {
return Err(e);
}
if !self.side_set {
return Err(BuildError::SideNotSet);
}
if !self.tick_set {
return Err(BuildError::TickNotSet);
}
if !self.expiry_set {
return Err(BuildError::ExpiryNotSet);
}
Ok(self.build())
}
pub fn build(self) -> Offer {
Offer {
market: self.market,
buy: self.buy,
maker: self.maker,
start: u256_to_word(self.start),
expiry: u256_to_word(self.expiry),
tick: u256_to_word(self.tick),
group: self.group,
callback: self.callback,
callback_data: self.callback_data,
receiver_if_maker_is_seller: self.receiver_if_maker_is_seller,
ratifier: self.ratifier,
reduce_only: self.reduce_only,
max_units: self.max_units,
max_assets: self.max_assets,
continuous_fee_cap: u256_to_word(self.continuous_fee_cap),
}
}
pub fn try_build(self, ctx: &ValidateCtx) -> Result<Offer, Vec<OfferError>> {
let mut errs = Vec::new();
if !self.side_set {
errs.push(OfferError::SideNotSet);
}
if !self.tick_set {
errs.push(OfferError::TickNotSet);
}
if !self.expiry_set {
errs.push(OfferError::ExpiryNotSet);
}
let offer = self.build();
errs.extend(validate_offer(&offer, ctx));
if errs.is_empty() {
Ok(offer)
} else {
Err(errs)
}
}
}