use crate::{word_to_u128, word_to_u256, Offer, OfferError, MAX_TICK, U256};
const WAD: u128 = 1_000_000_000_000_000_000; const CBP: u128 = 1_000_000_000_000; const SEC_PER_DAY: u64 = 86_400;
const LN_ONE_PLUS_DELTA: i128 = 4_987_541_511_039_073; const PRICE_ROUNDING_STEP: u128 = 100_000_000_000; const LN2: i128 = 693_147_180_559_945_309; const WEXP_OFFSET: i128 = 322_611_214_989_459_870;
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum SimError {
#[error("tick {0} exceeds MAX_TICK ({MAX_TICK})")]
TickOutOfRange(u128),
#[error("tick does not fit in u64")]
TickNotU64,
#[error("settlement fee exceeds offer price")]
SettlementFeeExceedsPrice,
#[error("price exceeds 1e18 (WAD)")]
PriceGreaterThanOne,
#[error("tick spacing {0} must be positive and divide MAX_TICK ({MAX_TICK})")]
InvalidTickSpacing(u64),
#[error("time-to-maturity is zero")]
ZeroTimeToMaturity,
#[error("price is zero; term rate is undefined")]
ZeroPrice,
#[error("uint256 multiplication overflow")]
ArithmeticOverflow,
}
pub const SECONDS_PER_YEAR: u64 = 31_536_000;
#[inline]
fn wad() -> U256 {
U256::from(WAD)
}
#[inline]
fn e36() -> U256 {
U256::from(WAD) * U256::from(WAD)
}
#[inline]
fn div_half_down(x: U256, d: U256) -> U256 {
(x + (d - U256::from(1u64)) / U256::from(2u64)) / d
}
#[inline]
fn mul_div_down(x: U256, y: U256, d: U256) -> Result<U256, SimError> {
Ok(x.checked_mul(y).ok_or(SimError::ArithmeticOverflow)? / d)
}
#[inline]
fn mul_div_up(x: U256, y: U256, d: U256) -> Result<U256, SimError> {
let product = x.checked_mul(y).ok_or(SimError::ArithmeticOverflow)?;
if product.is_zero() {
return Ok(U256::ZERO);
}
Ok((product - U256::from(1u8)) / d + U256::from(1u8))
}
#[inline]
fn zero_floor_sub(x: U256, y: U256) -> U256 {
if x > y {
x - y
} else {
U256::ZERO
}
}
fn wexp(x: i128) -> U256 {
if x < 0 {
return e36() / wexp(-x);
}
let q = (x + WEXP_OFFSET) / LN2; let r = x - q * LN2;
let second = r * r / (2 * WAD as i128);
let third = second * r / (3 * WAD as i128);
let exp_r = WAD as i128 + r + second + third; U256::from(exp_r as u128) << (q as usize)
}
pub fn tick_to_price(tick: u64) -> Result<U256, SimError> {
if tick > MAX_TICK {
return Err(SimError::TickOutOfRange(tick as u128));
}
let arg = LN_ONE_PLUS_DELTA * ((MAX_TICK as i128 / 2) - tick as i128);
let inner = wad() + wexp(arg);
let step = U256::from(PRICE_ROUNDING_STEP);
let p = div_half_down(e36(), inner);
Ok(div_half_down(p, step) * step)
}
pub fn tick_to_rate(tick: u64) -> Result<U256, SimError> {
let price = tick_to_price(tick)?;
if price.is_zero() {
return Err(SimError::ZeroPrice);
}
Ok(mul_div_up(wad(), wad(), price)? - wad())
}
pub fn tick_to_apr_wad(tick: u64, ttm_secs: u64) -> Result<U256, SimError> {
if ttm_secs == 0 {
return Err(SimError::ZeroTimeToMaturity);
}
mul_div_up(
tick_to_rate(tick)?,
U256::from(SECONDS_PER_YEAR),
U256::from(ttm_secs),
)
}
pub fn price_to_tick(price: U256, spacing: u64) -> Result<u64, SimError> {
if price > wad() {
return Err(SimError::PriceGreaterThanOne);
}
if spacing == 0 || MAX_TICK % spacing != 0 {
return Err(SimError::InvalidTickSpacing(spacing));
}
let mut low: u64 = 0;
let mut high: u64 = MAX_TICK;
while low != high {
let mid = (low + high) / 2;
if tick_to_price(mid)? < price {
low = mid + 1;
} else {
high = mid;
}
}
Ok((low + spacing - 1) / spacing * spacing)
}
pub fn tick_to_apr(tick: u64, ttm_secs: u64) -> Result<f64, SimError> {
let apr_wad = tick_to_apr_wad(tick, ttm_secs)?;
Ok(u128::try_from(apr_wad).unwrap_or(u128::MAX) as f64 / 1e16)
}
pub fn apr_to_tick(apr_pct: f64, ttm_secs: u64, spacing: u64) -> Result<u64, SimError> {
let term_rate = (apr_pct / 100.0) * (ttm_secs as f64 / SECONDS_PER_YEAR as f64);
let mut price_frac = 1.0 / (1.0 + term_rate);
if price_frac > 1.0 {
price_frac = 1.0;
}
let price_wad = U256::from((price_frac * 1e18) as u128);
price_to_tick(price_wad, spacing)
}
pub fn settlement_fee(cbps: [u16; 7], time_to_maturity: U256) -> U256 {
let day = |d: u64| U256::from(d * SEC_PER_DAY);
let cbp = |i: usize| U256::from(cbps[i] as u128) * U256::from(CBP);
if time_to_maturity >= day(360) {
return cbp(6);
}
let ttm = time_to_maturity; let (start, end, fee_lower, fee_upper) = if ttm < day(1) {
(day(0), day(1), cbp(0), cbp(1))
} else if ttm < day(7) {
(day(1), day(7), cbp(1), cbp(2))
} else if ttm < day(30) {
(day(7), day(30), cbp(2), cbp(3))
} else if ttm < day(90) {
(day(30), day(90), cbp(3), cbp(4))
} else if ttm < day(180) {
(day(90), day(180), cbp(4), cbp(5))
} else {
(day(180), day(360), cbp(5), cbp(6))
};
(fee_lower * (end - ttm) + fee_upper * (ttm - start)) / (end - start)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TakeAmounts {
pub offer_price: U256,
pub buyer_price: U256,
pub seller_price: U256,
pub buyer_assets: U256,
pub seller_assets: U256,
pub settlement_fee_assets: U256,
}
fn tick_u64(offer: &Offer) -> Result<u64, SimError> {
let t = word_to_u128(&offer.tick).ok_or(SimError::TickNotU64)?;
u64::try_from(t).map_err(|_| SimError::TickNotU64)
}
pub fn take_amounts(
offer: &Offer,
units: U256,
now: u64,
cbps: [u16; 7],
) -> Result<TakeAmounts, SimError> {
let offer_price = tick_to_price(tick_u64(offer)?)?;
let maturity = word_to_u256(&offer.market.maturity);
let ttm = zero_floor_sub(maturity, U256::from(now));
let fee = settlement_fee(cbps, ttm);
let (seller_price, buyer_price) = if offer.buy {
let sp = offer_price
.checked_sub(fee)
.ok_or(SimError::SettlementFeeExceedsPrice)?;
(sp, offer_price) } else {
(offer_price, offer_price + fee)
};
let (buyer_assets, seller_assets) = if offer.buy {
(
mul_div_down(units, buyer_price, wad())?,
mul_div_down(units, seller_price, wad())?,
)
} else {
(
mul_div_up(units, buyer_price, wad())?,
mul_div_up(units, seller_price, wad())?,
)
};
Ok(TakeAmounts {
offer_price,
buyer_price,
seller_price,
buyer_assets,
seller_assets,
settlement_fee_assets: buyer_assets - seller_assets,
})
}
#[derive(Clone, Copy, Debug, Default)]
pub struct Position {
pub credit: u128,
pub debt: u128,
pub pending_fee: u128,
}
#[derive(Clone, Copy, Debug)]
pub struct SimMarket {
pub tick_spacing: u8,
pub continuous_fee: u128,
pub settlement_fee_cbp: [u16; 7],
pub loss_factor_maxed: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct SimCtx {
pub now: u64,
pub market: SimMarket,
pub consumed: u128,
pub maker_position: Position,
pub taker_position: Position,
pub taker_is_maker: bool,
}
#[derive(Clone, Debug)]
pub struct TakeOutcome {
pub amounts: TakeAmounts,
pub buyer_credit_increase: U256,
pub seller_credit_decrease: U256,
pub seller_debt_increase: U256,
pub buyer_pending_fee_increase: U256,
pub seller_pending_fee_decrease: U256,
pub new_consumed: U256,
pub reverts: Vec<OfferError>,
}
pub fn simulate_take(offer: &Offer, units: U256, ctx: &SimCtx) -> Result<TakeOutcome, SimError> {
let amounts = take_amounts(offer, units, ctx.now, ctx.market.settlement_fee_cbp)?;
let mut reverts = Vec::new();
let assets_capped = offer.max_assets != 0;
let units_capped = offer.max_units != 0;
if assets_capped == units_capped {
reverts.push(OfferError::InvalidOfferCaps);
}
if ctx.market.loss_factor_maxed {
reverts.push(OfferError::MarketLossFactorMaxedOut);
}
if U256::from(ctx.market.continuous_fee) > word_to_u256(&offer.continuous_fee_cap) {
reverts.push(OfferError::ContinuousFeeAboveOfferCap);
}
if offer.buy && offer.receiver_if_maker_is_seller != [0u8; 20] {
reverts.push(OfferError::UnusedReceiverMustBeZero);
}
if let Ok(tick) = tick_u64(offer) {
let spacing = if ctx.market.tick_spacing == 0 {
crate::DEFAULT_TICK_SPACING
} else {
ctx.market.tick_spacing
};
if tick % spacing as u64 != 0 {
reverts.push(OfferError::TickNotAccessible);
}
}
let now = U256::from(ctx.now);
if now < word_to_u256(&offer.start) {
reverts.push(OfferError::OfferNotStarted);
}
if now > word_to_u256(&offer.expiry) {
reverts.push(OfferError::OfferExpired);
}
if ctx.taker_is_maker {
reverts.push(OfferError::SelfTake);
}
let consumed = U256::from(ctx.consumed);
let new_consumed = if assets_capped {
let add = if offer.buy {
amounts.buyer_assets
} else {
amounts.seller_assets
};
let nc = consumed
.checked_add(add)
.ok_or(SimError::ArithmeticOverflow)?;
if nc > U256::from(offer.max_assets) {
reverts.push(OfferError::ConsumedAssets);
}
nc
} else if units_capped {
let nc = consumed
.checked_add(units)
.ok_or(SimError::ArithmeticOverflow)?;
if nc > U256::from(offer.max_units) {
reverts.push(OfferError::ConsumedUnits);
}
nc
} else {
consumed };
let (buyer_pos, seller_pos) = if offer.buy {
(ctx.maker_position, ctx.taker_position)
} else {
(ctx.taker_position, ctx.maker_position)
};
let buyer_credit_increase = zero_floor_sub(units, U256::from(buyer_pos.debt));
let seller_credit_decrease = core::cmp::min(units, U256::from(seller_pos.credit));
let seller_debt_increase = units - seller_credit_decrease;
let ttm = zero_floor_sub(word_to_u256(&offer.market.maturity), now);
let continuous_fee_over_term = U256::from(ctx.market.continuous_fee)
.checked_mul(ttm)
.ok_or(SimError::ArithmeticOverflow)?;
let buyer_pending_fee_increase =
mul_div_down(buyer_credit_increase, continuous_fee_over_term, wad())?;
let seller_pending_fee_decrease = if seller_pos.credit > 0 {
mul_div_up(
U256::from(seller_pos.pending_fee),
seller_credit_decrease,
U256::from(seller_pos.credit),
)?
} else {
U256::ZERO
};
if now > word_to_u256(&offer.market.maturity) && seller_debt_increase != U256::ZERO {
reverts.push(OfferError::CannotIncreaseDebtPostMaturity);
}
if offer.reduce_only {
let maker_increased = if offer.buy {
buyer_credit_increase != U256::ZERO
} else {
seller_debt_increase != U256::ZERO
};
if maker_increased {
reverts.push(OfferError::MakerCreditOrDebtIncreased);
}
}
Ok(TakeOutcome {
amounts,
buyer_credit_increase,
seller_credit_decrease,
seller_debt_increase,
buyer_pending_fee_increase,
seller_pending_fee_decrease,
new_consumed,
reverts,
})
}