Skip to content
48 changes: 46 additions & 2 deletions math/src/omnipool/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,58 @@ pub fn calculate_buy_state_changes(

// Step 2: Invert buy-side slip to find D_gross from D_net
let d_gross = if let Some(slip) = slip {
invert_buy_side_slip(d_net, slip.asset_out_hub_reserve, slip.asset_out_delta)?
let d_gross_uncapped = invert_buy_side_slip(d_net, slip.asset_out_hub_reserve, slip.asset_out_delta)?;
let max_parts = slip.max_slip_fee.deconstruct() as u128;
let one_minus_max = 1_000_000u128.checked_sub(max_parts)?;
let (d_net_hp, d_gross_uncapped_hp) = to_u256!(d_net, d_gross_uncapped);
let rate_hp = d_net_hp
.checked_mul(U256::from(1_000_000u128))?
.checked_div(d_gross_uncapped_hp)?;
if rate_hp < U256::from(one_minus_max) {
// Cap fired: d_gross = floor(D_net * 1_000_000 / (1_000_000 - max_parts))
// -1 compensates for the +1 ceiling added by invert_sell_side_fees
let d_gross_capped_hp = d_net_hp
.checked_mul(U256::from(1_000_000u128))?
.checked_div(U256::from(one_minus_max))?;
let d_gross_capped = to_balance!(d_gross_capped_hp).ok()?;
d_gross_capped.checked_sub(Balance::one())?
} else {
d_gross_uncapped
}
} else {
d_net
};

// Step 3: Invert sell-side fees (protocol_fee + sell slip) to find delta_hub_reserve_in
let delta_hub_reserve_in = if let Some(slip) = slip {
invert_sell_side_fees(d_gross, protocol_fee, slip.asset_in_hub_reserve, slip.asset_in_delta)?
let u_raw = invert_sell_side_fees(d_gross, protocol_fee, slip.asset_in_hub_reserve, slip.asset_in_delta)?;
let max_parts = slip.max_slip_fee.deconstruct() as u128;
let cumulative_sell = slip.asset_in_delta.checked_add(SignedBalance::Negative(u_raw))?;
let denom_sell = cumulative_sell.add_to_unsigned(slip.asset_in_hub_reserve)?;
if denom_sell > 0 && !cumulative_sell.is_zero() {
let abs_cum = cumulative_sell.abs();
let (abs_cum_hp, denom_hp) = to_u256!(abs_cum, denom_sell);
let rate_hp = abs_cum_hp
.checked_mul(U256::from(1_000_000u128))?
.checked_div(denom_hp)?;
if rate_hp > U256::from(max_parts) {
// Cap fired: delta_hub_reserve_in = floor((d_gross - 1) * 1_000_000 / (k_parts - max_parts))
// -1 compensates for the +1 ceiling added by invert_buy_side_slip
let d_gross_adj = d_gross.checked_sub(Balance::one())?;
let pf_parts = protocol_fee.deconstruct() as u128;
let k_parts = 1_000_000u128.checked_sub(pf_parts)?;
let den_parts = k_parts.checked_sub(max_parts)?;
let (d_gross_adj_hp, den_parts_hp) = to_u256!(d_gross_adj, den_parts);
let u_cap_hp = d_gross_adj_hp
.checked_mul(U256::from(1_000_000u128))?
.checked_div(den_parts_hp)?;
to_balance!(u_cap_hp).ok()?
} else {
u_raw
}
} else {
u_raw
}
} else {
// No slip — original inversion
FixedU128::from_inner(d_net)
Expand Down
124 changes: 124 additions & 0 deletions math/src/omnipool/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3350,3 +3350,127 @@ fn cross_validate_buy_dot_with_lrna_with_prior_delta() {
*r_fresh.asset.delta_hub_reserve
);
}

#[test]
fn calculate_buy_should_respect_max_slip_fee_cap_on_buy_side() {
// large pool: ~0.02% hub flow -> cap does not fire
let asset_in_state = AssetReserveState {
reserve: 100_000_000 * UNIT,
hub_reserve: 100_000_000 * UNIT,
shares: 100_000_000 * UNIT,
protocol_shares: 0u128,
};
// small pool: ~2% hub flow -> cap fires
let asset_out_state = AssetReserveState {
reserve: 1_000_000 * UNIT,
hub_reserve: 1_000_000 * UNIT,
shares: 1_000_000 * UNIT,
protocol_shares: 0u128,
};

let amount = 20_000 * UNIT;
let asset_fee = Permill::zero();
let protocol_fee = Permill::zero();
let burn_fee = Permill::zero();
let max_slip_fee = Permill::from_percent(1);

let slip = TradeSlipFees {
asset_in_hub_reserve: asset_in_state.hub_reserve,
asset_in_delta: SignedBalance::zero(),
asset_out_hub_reserve: asset_out_state.hub_reserve,
asset_out_delta: SignedBalance::zero(),
max_slip_fee,
};

let state_changes = calculate_buy_state_changes(
&asset_in_state,
&asset_out_state,
amount,
asset_fee,
protocol_fee,
burn_fee,
Some(&slip),
)
.expect("calculate_buy_state_changes should succeed");

let delta_hub_reserve_in = match state_changes.asset_in.delta_hub_reserve {
BalanceUpdate::Decrease(v) => v,
_ => panic!("expected Decrease for asset_in hub reserve"),
};
let total_protocol_fee = state_changes.fee.protocol_fee;

let d_net_expected: Balance = asset_out_state.hub_reserve * amount / (asset_out_state.reserve - amount) + 1;

let d_net_actual = delta_hub_reserve_in
.checked_sub(total_protocol_fee)
.expect("delta_hub_reserve_in must be >= total_protocol_fee");

assert_eq!(
d_net_actual, d_net_expected,
"buy-side slip fee cap not applied during inversion: \
buyer pays {delta_hub_reserve_in} hub, fees = {total_protocol_fee}, \
net to pool = {d_net_actual} but pool needs {d_net_expected} hub for the trade",
);
}

#[test]
fn calculate_buy_should_respect_max_slip_fee_cap_on_sell_side() {
// small pool: ~2% hub flow → cap fires
let asset_in_state = AssetReserveState {
reserve: 1_000_000 * UNIT,
hub_reserve: 1_000_000 * UNIT,
shares: 1_000_000 * UNIT,
protocol_shares: 0u128,
};
// large pool: ~0.02% hub flow → cap does not fire
let asset_out_state = AssetReserveState {
reserve: 100_000_000 * UNIT,
hub_reserve: 100_000_000 * UNIT,
shares: 100_000_000 * UNIT,
protocol_shares: 0u128,
};

let amount = 20_000 * UNIT;
let asset_fee = Permill::zero();
let protocol_fee = Permill::zero();
let burn_fee = Permill::zero();
let max_slip_fee = Permill::from_percent(1);

let slip = TradeSlipFees {
asset_in_hub_reserve: asset_in_state.hub_reserve,
asset_in_delta: SignedBalance::zero(),
asset_out_hub_reserve: asset_out_state.hub_reserve,
asset_out_delta: SignedBalance::zero(),
max_slip_fee,
};

let state_changes = calculate_buy_state_changes(
&asset_in_state,
&asset_out_state,
amount,
asset_fee,
protocol_fee,
burn_fee,
Some(&slip),
)
.expect("calculate_buy_state_changes should succeed");

let delta_hub_reserve_in = match state_changes.asset_in.delta_hub_reserve {
BalanceUpdate::Decrease(v) => v,
_ => panic!("expected Decrease for asset_in hub reserve"),
};
let total_protocol_fee = state_changes.fee.protocol_fee;

let d_net_expected: Balance = asset_out_state.hub_reserve * amount / (asset_out_state.reserve - amount) + 1;

let d_net_actual = delta_hub_reserve_in
.checked_sub(total_protocol_fee)
.expect("delta_hub_reserve_in must be >= total_protocol_fee");

assert_eq!(
d_net_actual, d_net_expected,
"sell-side slip fee cap not applied during inversion: \
buyer pays {delta_hub_reserve_in} hub, fees = {total_protocol_fee}, \
net to pool = {d_net_actual} but pool needs {d_net_expected} hub for the trade",
);
}
Loading