use crate::{word_from_u128 as w128, word_to_u128, word_to_u256, Address, Offer, U256};
const WAD: u128 = 1_000_000_000_000_000_000;
pub const MAX_TICK: u64 = 6744;
pub const MAX_COLLATERALS: usize = 128;
pub const DEFAULT_TICK_SPACING: u8 = 4;
pub const MAX_CONTINUOUS_FEE: u64 = 317_097_919;
pub const MAX_MATURITY_HORIZON_SECS: u64 = 100 * 365 * 24 * 60 * 60;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OfferError {
InvalidOfferCaps,
TickOutOfRange,
StartAfterExpiry,
UnusedReceiverMustBeZero,
SellerReceiverZero,
RatifierUnauthorized,
NoCollateralParams,
TooManyCollateralParams,
CollateralParamsNotSorted,
InvalidMaxLif,
MaxLifTooHigh,
InvalidChainId,
InvalidMidnight,
MaturityTooFar,
OfferNotStarted,
OfferExpired,
TickNotAccessible,
MarketLossFactorMaxedOut,
ContinuousFeeAboveOfferCap,
ConsumedUnits,
ConsumedAssets,
SelfTake,
CannotIncreaseDebtPostMaturity,
MakerCreditOrDebtIncreased,
SideNotSet,
TickNotSet,
ExpiryNotSet,
}
#[derive(Clone, Copy, Debug)]
pub struct MarketSnapshot {
pub tick_spacing: u8,
pub loss_factor_maxed: bool,
pub continuous_fee: u128,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ValidateCtx {
pub chain_id: Option<u64>,
pub midnight: Option<Address>,
pub now: Option<u64>,
pub market: Option<MarketSnapshot>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cap {
Units(u128),
Assets(u128),
}
pub fn active_cap(offer: &Offer) -> Option<Cap> {
match (offer.max_assets == 0, offer.max_units == 0) {
(false, true) => Some(Cap::Assets(offer.max_assets)),
(true, false) => Some(Cap::Units(offer.max_units)),
_ => None, }
}
pub fn consumption_headroom(offer: &Offer, consumed_so_far: u128) -> Option<u128> {
let cap = match active_cap(offer)? {
Cap::Units(v) | Cap::Assets(v) => v,
};
Some(cap.saturating_sub(consumed_so_far))
}
pub fn can_consume(offer: &Offer, consumed_so_far: u128, amount: u128) -> bool {
match consumption_headroom(offer, consumed_so_far) {
Some(h) => amount <= h,
None => false,
}
}
pub(crate) fn max_lif(lltv: U256, cursor: U256) -> Option<U256> {
let wad = U256::from(WAD);
let inner = cursor.checked_mul(wad.checked_sub(lltv)?)? / wad;
(wad * wad).checked_div(wad.checked_sub(inner)?)
}
pub fn validate_offer(offer: &Offer, ctx: &ValidateCtx) -> Vec<OfferError> {
let mut errs = Vec::new();
if active_cap(offer).is_none() {
errs.push(OfferError::InvalidOfferCaps);
}
if offer.tick > w128(MAX_TICK as u128) {
errs.push(OfferError::TickOutOfRange);
}
if offer.start > offer.expiry {
errs.push(OfferError::StartAfterExpiry);
}
if offer.buy && offer.receiver_if_maker_is_seller != [0u8; 20] {
errs.push(OfferError::UnusedReceiverMustBeZero);
}
if !offer.buy && offer.receiver_if_maker_is_seller == [0u8; 20] {
errs.push(OfferError::SellerReceiverZero);
}
if offer.ratifier == [0u8; 20] {
errs.push(OfferError::RatifierUnauthorized);
}
let cps = &offer.market.collateral_params;
if cps.is_empty() {
errs.push(OfferError::NoCollateralParams);
}
if cps.len() > MAX_COLLATERALS {
errs.push(OfferError::TooManyCollateralParams);
}
if cps.first().is_some_and(|cp| cp.token == [0u8; 20])
|| cps.windows(2).any(|w| w[1].token <= w[0].token)
{
errs.push(OfferError::CollateralParamsNotSorted);
}
let wad = U256::from(WAD);
let mut invalid_max_lif = false;
let mut max_lif_too_high = false;
for cp in cps {
let lltv = word_to_u256(&cp.lltv);
match max_lif(lltv, word_to_u256(&cp.liquidation_cursor)) {
None => invalid_max_lif = true,
Some(ml) => {
if ml > U256::from(2 * WAD) {
invalid_max_lif = true;
}
if lltv != wad && lltv * ml > U256::from(999_000_000_000_000_000u128 * WAD) {
max_lif_too_high = true;
}
}
}
}
if invalid_max_lif {
errs.push(OfferError::InvalidMaxLif);
}
if max_lif_too_high {
errs.push(OfferError::MaxLifTooHigh);
}
if let Some(chain_id) = ctx.chain_id {
if offer.market.chain_id != w128(chain_id as u128) {
errs.push(OfferError::InvalidChainId);
}
}
if let Some(midnight) = ctx.midnight {
if offer.market.midnight != midnight {
errs.push(OfferError::InvalidMidnight);
}
}
if let Some(now) = ctx.now {
let now_w = w128(now as u128);
if now_w < offer.start {
errs.push(OfferError::OfferNotStarted);
}
if now_w > offer.expiry {
errs.push(OfferError::OfferExpired);
}
let horizon = w128(now as u128 + MAX_MATURITY_HORIZON_SECS as u128);
if offer.market.maturity > horizon {
errs.push(OfferError::MaturityTooFar);
}
}
let tick_spacing = ctx
.market
.map(|market| market.tick_spacing)
.unwrap_or(DEFAULT_TICK_SPACING);
if tick_spacing == 0 || DEFAULT_TICK_SPACING % tick_spacing != 0 {
errs.push(OfferError::TickNotAccessible);
} else if let Some(tick) = word_to_u128(&offer.tick) {
if tick % u128::from(tick_spacing) != 0 {
errs.push(OfferError::TickNotAccessible);
}
}
if let Some(m) = ctx.market {
if m.loss_factor_maxed {
errs.push(OfferError::MarketLossFactorMaxedOut);
}
if w128(m.continuous_fee) > offer.continuous_fee_cap {
errs.push(OfferError::ContinuousFeeAboveOfferCap);
}
}
errs
}
pub fn is_valid(offer: &Offer, ctx: &ValidateCtx) -> bool {
validate_offer(offer, ctx).is_empty()
}