Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 72 additions & 16 deletions crates/orderbook/src/quoter.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use {
crate::app_data,
alloy::primitives::{U256, U512, Uint, ruint::UintTryFrom},
alloy::primitives::{U256, U512, ruint::UintTryFrom},
bigdecimal::{BigDecimal, FromPrimitive},
chrono::{TimeZone, Utc},
configs::{fee_factor::FeeFactor, orderbook::VolumeFeeConfig},
Expand Down Expand Up @@ -245,17 +245,13 @@ fn get_vol_fee_adjusted_quote_data(
// BPS)
let scaled_factor = U256::from(factor.to_high_precision());
let scale = U512::from(FeeFactor::HIGH_PRECISION_SCALE);
// Round the fee up so we never overpromise the user.
let (adjusted_sell_amount, adjusted_buy_amount) = match side {
OrderQuoteSide::Sell { .. } => {
// For SELL orders, fee is calculated on buy amount
let protocol_fee = U256::uint_try_from(
quote
.buy_amount
.widening_mul(scaled_factor)
.checked_div(scale)
.ok_or_else(|| anyhow::anyhow!("volume fee calculation division by zero"))?,
)
.map_err(|_| anyhow::anyhow!("volume fee calculation overflow"))?;
let numerator = quote.buy_amount.widening_mul(scaled_factor);
let protocol_fee = U256::uint_try_from(numerator.div_ceil(scale))
.map_err(|_| anyhow::anyhow!("volume fee calculation overflow"))?;

// Reduce buy amount by protocol fee
let adjusted_buy = quote.buy_amount.saturating_sub(protocol_fee);
Expand All @@ -266,13 +262,9 @@ fn get_vol_fee_adjusted_quote_data(
// For BUY orders, fee is calculated on sell amount + network fee.
// Network fee is already in sell token, so it is added to get the total volume.
let total_sell_volume = quote.sell_amount.saturating_add(quote.fee_amount);
let volume_scaled: Uint<512, 8> = total_sell_volume.widening_mul(scaled_factor);
let protocol_fee = U256::uint_try_from(
volume_scaled
.checked_div(scale)
.ok_or_else(|| anyhow::anyhow!("volume fee calculation division by zero"))?,
)
.map_err(|_| anyhow::anyhow!("volume fee calculation overflow"))?;
let numerator = total_sell_volume.widening_mul(scaled_factor);
let protocol_fee = U256::uint_try_from(numerator.div_ceil(scale))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches run the exact same fee computation on different inputs. Could fold into a one-liner helper:

fn ceil_volume_fee(volume: U256, scaled_factor: U256, scale: U512) -> anyhow::Result<U256> {
    U256::uint_try_from(volume.widening_mul(scaled_factor).div_ceil(scale))
        .map_err(|_| anyhow::anyhow!("volume fee calculation overflow"))
}

.map_err(|_| anyhow::anyhow!("volume fee calculation overflow"))?;

// Increase sell amount by protocol fee
let adjusted_sell = quote.sell_amount.saturating_add(protocol_fee);
Expand Down Expand Up @@ -585,6 +577,70 @@ mod tests {
assert_eq!(result.protocol_fee_bps, None);
}

#[test]
fn test_volume_fee_rounds_up_sell_order() {
// factor 0.0001, buy_amount 12345: fee = 12345/10000 = 1.2345 -> ceil 2.
let volume_fee = FeeFactor::try_from(0.0001).unwrap();
let volume_fee_config = VolumeFeeConfig {
factor: Some(volume_fee),
effective_from_timestamp: None,
};
let volume_fee_policy = VolumeFeePolicy::new(vec![], Some(volume_fee), false);

let quote = create_test_quote(U256::from(100u64), U256::from(12345u64));
let side = OrderQuoteSide::Sell {
sell_amount: model::quote::SellAmount::BeforeFee {
value: number::nonzero::NonZeroU256::try_from(U256::from(100u64)).unwrap(),
},
};

let result = get_vol_fee_adjusted_quote_data(
&quote,
&side,
Some(&volume_fee_config),
&volume_fee_policy,
TEST_BUY_TOKEN,
TEST_SELL_TOKEN,
)
.unwrap();

assert_eq!(result.sell_amount, U256::from(100u64));
assert_eq!(result.buy_amount, U256::from(12345u64 - 2));
}

#[test]
fn test_volume_fee_rounds_up_buy_order() {
Comment thread
anxolin marked this conversation as resolved.
// factor 0.0001, sell 10_000, network fee 5: ceil((10000 + 5)/10000) =
// ceil(1.0005) = 2
let volume_fee = FeeFactor::try_from(0.0001).unwrap();
let volume_fee_config = VolumeFeeConfig {
factor: Some(volume_fee),
effective_from_timestamp: None,
};
let volume_fee_policy = VolumeFeePolicy::new(vec![], Some(volume_fee), false);

let mut quote = create_test_quote(U256::from(10_000u64), U256::from(100u64));
quote.fee_amount = U256::from(5u64);

let side = OrderQuoteSide::Buy {
buy_amount_after_fee: number::nonzero::NonZeroU256::try_from(U256::from(100u64))
.unwrap(),
};

let result = get_vol_fee_adjusted_quote_data(
&quote,
&side,
Some(&volume_fee_config),
&volume_fee_policy,
TEST_BUY_TOKEN,
TEST_SELL_TOKEN,
)
.unwrap();

assert_eq!(result.sell_amount, U256::from(10_000u64 + 2));
assert_eq!(result.buy_amount, U256::from(100u64));
}

#[test]
fn test_volume_fee_sub_basis_point_precision() {
// Test sub-BPS precision: 0.00003 = 0.3 BPS
Expand Down
Loading