1#[cfg(feature = "wasm")]
5use equanetwork_macros::wasm_expose;
6
7use super::consts::{MAX_TOTAL_SKEW_PER_1M, PER_1M_DENOMINATOR};
8use super::error::{
9 CoreError, AMOUNT_EXCEEDS_MAX_U64, ARITHMETIC_OVERFLOW, DIVISION_BY_ZERO,
10 INSUFFICIENT_LIQUIDITY, INVALID_FEE, INVALID_PRICE, INVALID_SKEW, MAX_SWAP_EXCEEDED,
11};
12use super::skew::{average_skew_per_m, SkewExponent};
13use super::U128;
14
15#[cfg_attr(feature = "wasm", wasm_expose)]
17pub fn base_out_1_to_1(
18 amount_in: u64,
19 in_decimals: u8,
20 out_decimals: u8,
21) -> Result<u64, CoreError> {
22 if amount_in == 0 {
23 return Ok(0);
24 }
25 let in_scale = 10u128
26 .checked_pow(in_decimals as u32)
27 .ok_or(ARITHMETIC_OVERFLOW)?;
28 let out_scale = 10u128
29 .checked_pow(out_decimals as u32)
30 .ok_or(ARITHMETIC_OVERFLOW)?;
31 let numerator = (amount_in as u128)
32 .checked_mul(out_scale)
33 .ok_or(ARITHMETIC_OVERFLOW)?;
34 let out = numerator.checked_div(in_scale).ok_or(DIVISION_BY_ZERO)?;
35 out.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
36}
37
38#[cfg_attr(feature = "wasm", wasm_expose)]
42pub fn fair_out_with_prices(
43 amount_in: u64,
44 in_decimals: u8,
45 out_decimals: u8,
46 price_in_q64: U128,
47 price_out_q64: U128,
48) -> Result<u64, CoreError> {
49 let base = base_out_1_to_1(amount_in, in_decimals, out_decimals)?;
50 let price_in: u128 = price_in_q64.into();
51 let price_out: u128 = price_out_q64.into();
52 if price_in == 0 || price_out == 0 {
53 return Err(INVALID_PRICE);
54 }
55 let numerator = (base as u128)
56 .checked_mul(price_in)
57 .ok_or(ARITHMETIC_OVERFLOW)?;
58 let out = numerator.checked_div(price_out).ok_or(DIVISION_BY_ZERO)?;
59 out.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
60}
61
62#[cfg_attr(feature = "wasm", wasm_expose)]
64pub fn apply_swap_fee_per_1m(amount: u64, fee_per_1m: u32) -> Result<u64, CoreError> {
65 if fee_per_1m as u64 > PER_1M_DENOMINATOR {
66 return Err(INVALID_FEE);
67 }
68 let keep = PER_1M_DENOMINATOR.saturating_sub(fee_per_1m as u64);
69 let product = (amount as u128)
70 .checked_mul(keep as u128)
71 .ok_or(ARITHMETIC_OVERFLOW)?;
72 let out = product
73 .checked_div(PER_1M_DENOMINATOR as u128)
74 .ok_or(DIVISION_BY_ZERO)?;
75 out.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
76}
77
78#[allow(clippy::too_many_arguments)]
80#[cfg_attr(feature = "wasm", wasm_expose)]
81pub fn vault_skew_contribution_per_1m(
82 balance_pre: u64,
83 balance_post: u64,
84 limit_lower: u64,
85 limit_upper: u64,
86 positive_skew_per_1m: u32,
87 negative_skew_per_1m: u32,
88 lower_skew_offset_bps: u16,
89 upper_skew_offset_bps: u16,
90 skew_exponent: SkewExponent,
91) -> Result<i64, CoreError> {
92 average_skew_per_m(
93 balance_pre,
94 balance_post,
95 limit_lower,
96 limit_upper,
97 lower_skew_offset_bps,
98 upper_skew_offset_bps,
99 positive_skew_per_1m,
100 negative_skew_per_1m,
101 skew_exponent,
102 )
103}
104
105#[cfg_attr(feature = "wasm", wasm_expose)]
107pub fn apply_total_skew_per_1m(fair_out: u64, total_skew_per_1m: i64) -> Result<u64, CoreError> {
108 let capped = total_skew_per_1m.clamp(-MAX_TOTAL_SKEW_PER_1M, MAX_TOTAL_SKEW_PER_1M);
109 let factor = (PER_1M_DENOMINATOR as i128)
110 .checked_sub(capped as i128)
111 .ok_or(ARITHMETIC_OVERFLOW)?;
112 if factor <= 0 {
113 return Err(INVALID_SKEW);
114 }
115 let product = (fair_out as u128)
116 .checked_mul(factor as u128)
117 .ok_or(ARITHMETIC_OVERFLOW)?;
118 let out = product
119 .checked_div(PER_1M_DENOMINATOR as u128)
120 .ok_or(DIVISION_BY_ZERO)?;
121 out.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)
122}
123
124#[derive(Debug, Clone, Copy, Eq, PartialEq)]
125#[cfg_attr(feature = "wasm", wasm_expose)]
126pub struct SwapQuote {
127 pub amount_in: u64,
128 pub fair_out: u64,
129 pub amount_out: u64,
130 pub total_skew_per_1m: i64,
131 pub partial: bool,
132}
133
134#[derive(Debug, Clone, Copy, Eq, PartialEq)]
135#[cfg_attr(feature = "wasm", wasm_expose)]
136pub struct VaultSwapLimits {
137 pub balance: u64,
138 pub limit_lower: u64,
139 pub limit_upper: u64,
140 pub max_swap_amount: u64,
141 pub swap_fee_per_1m: u32,
142 pub positive_skew_per_1m: u32,
143 pub negative_skew_per_1m: u32,
144 pub lower_skew_offset_bps: u16,
145 pub upper_skew_offset_bps: u16,
146 pub skew_exponent: SkewExponent,
147 pub decimals: u8,
148}
149
150#[cfg_attr(feature = "wasm", wasm_expose)]
152pub fn quote_network_swap(
153 amount_in: u64,
154 input: &VaultSwapLimits,
155 output: &VaultSwapLimits,
156 price_in_q64: U128,
157 price_out_q64: U128,
158 allow_partial_fill: bool,
159) -> Result<SwapQuote, CoreError> {
160 if amount_in == 0 {
161 return Err(INSUFFICIENT_LIQUIDITY);
162 }
163
164 let price_in: u128 = price_in_q64.into();
165 let price_out: u128 = price_out_q64.into();
166
167 let mut fill_in = amount_in;
168 if fill_in > input.max_swap_amount {
169 if !allow_partial_fill {
170 return Err(MAX_SWAP_EXCEEDED);
171 }
172 fill_in = input.max_swap_amount;
173 }
174
175 let input_headroom = input.limit_upper.saturating_sub(input.balance);
176 if fill_in > input_headroom {
177 if !allow_partial_fill {
178 return Err(INSUFFICIENT_LIQUIDITY);
179 }
180 fill_in = input_headroom;
181 }
182
183 for _ in 0..8 {
185 if fill_in == 0 {
186 return Err(INSUFFICIENT_LIQUIDITY);
187 }
188 let in_post = input.balance.saturating_add(fill_in);
189 let skew_in = vault_skew_contribution_per_1m(
190 input.balance,
191 in_post,
192 input.limit_lower,
193 input.limit_upper,
194 input.positive_skew_per_1m,
195 input.negative_skew_per_1m,
196 input.lower_skew_offset_bps,
197 input.upper_skew_offset_bps,
198 input.skew_exponent,
199 )?;
200
201 let priced_in = apply_swap_fee_per_1m(fill_in, input.swap_fee_per_1m)?;
203 if priced_in == 0 {
204 return Err(INSUFFICIENT_LIQUIDITY);
205 }
206 let fair = fair_out_with_prices(
207 priced_in,
208 input.decimals,
209 output.decimals,
210 U128::from(price_in),
211 U128::from(price_out),
212 )?;
213 let out_post_for_skew = output.balance.saturating_sub(fair.min(output.balance));
215 let skew_out = vault_skew_contribution_per_1m(
216 output.balance,
217 out_post_for_skew,
218 output.limit_lower,
219 output.limit_upper,
220 output.positive_skew_per_1m,
221 output.negative_skew_per_1m,
222 output.lower_skew_offset_bps,
223 output.upper_skew_offset_bps,
224 output.skew_exponent,
225 )?;
226 let total_skew = skew_in.saturating_add(skew_out);
227 let skewed_out = apply_total_skew_per_1m(fair, total_skew)?;
228 let exec_out = apply_swap_fee_per_1m(skewed_out, output.swap_fee_per_1m)?;
230
231 let spendable = output.balance.saturating_sub(output.limit_lower);
232 let mut max_out = core::cmp::min(spendable, output.max_swap_amount);
233 max_out = core::cmp::min(max_out, output.balance);
234
235 if exec_out > max_out {
236 if !allow_partial_fill {
237 return Err(if skewed_out > output.max_swap_amount {
238 MAX_SWAP_EXCEEDED
239 } else {
240 INSUFFICIENT_LIQUIDITY
241 });
242 }
243 if fair == 0 || exec_out == 0 {
244 return Err(INSUFFICIENT_LIQUIDITY);
245 }
246 let reduced = (fill_in as u128)
247 .checked_mul(max_out as u128)
248 .ok_or(ARITHMETIC_OVERFLOW)?
249 .checked_div(exec_out as u128)
250 .ok_or(DIVISION_BY_ZERO)?;
251 let reduced: u64 = reduced.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
252 if reduced >= fill_in || reduced == 0 {
253 return Err(INSUFFICIENT_LIQUIDITY);
254 }
255 fill_in = reduced;
256 continue;
257 }
258
259 let out_post = output.balance.saturating_sub(exec_out);
261 let skew_out = vault_skew_contribution_per_1m(
262 output.balance,
263 out_post,
264 output.limit_lower,
265 output.limit_upper,
266 output.positive_skew_per_1m,
267 output.negative_skew_per_1m,
268 output.lower_skew_offset_bps,
269 output.upper_skew_offset_bps,
270 output.skew_exponent,
271 )?;
272 let total_skew = skew_in.saturating_add(skew_out);
273 let skewed_out = apply_total_skew_per_1m(fair, total_skew)?;
274 let exec_out = apply_swap_fee_per_1m(skewed_out, output.swap_fee_per_1m)?;
275 if exec_out > max_out {
276 if !allow_partial_fill {
277 return Err(INSUFFICIENT_LIQUIDITY);
278 }
279 let reduced = (fill_in as u128)
280 .checked_mul(max_out as u128)
281 .ok_or(ARITHMETIC_OVERFLOW)?
282 .checked_div(exec_out as u128)
283 .ok_or(DIVISION_BY_ZERO)?;
284 let reduced: u64 = reduced.try_into().map_err(|_| AMOUNT_EXCEEDS_MAX_U64)?;
285 if reduced >= fill_in || reduced == 0 {
286 return Err(INSUFFICIENT_LIQUIDITY);
287 }
288 fill_in = reduced;
289 continue;
290 }
291
292 return Ok(SwapQuote {
293 amount_in: fill_in,
294 fair_out: fair,
295 amount_out: exec_out,
296 total_skew_per_1m: total_skew,
297 partial: fill_in < amount_in,
298 });
299 }
300 Err(INSUFFICIENT_LIQUIDITY)
301}
302
303#[cfg(test)]
304mod tests {
305 use super::super::Q64_ONE;
306 use super::*;
307
308 #[test]
309 fn one_to_one_same_decimals() {
310 assert_eq!(base_out_1_to_1(1_000_000, 6, 6).unwrap(), 1_000_000);
311 }
312
313 #[test]
314 fn one_to_one_decimal_conversion() {
315 assert_eq!(base_out_1_to_1(1_000_000, 6, 8).unwrap(), 100_000_000);
316 assert_eq!(base_out_1_to_1(100_000_000, 8, 6).unwrap(), 1_000_000);
317 }
318
319 #[test]
320 fn cheaper_output_price_yields_more_out() {
321 let price_out = Q64_ONE * 998 / 1000;
323 let out = fair_out_with_prices(1_000_000, 6, 6, U128::from(Q64_ONE), U128::from(price_out))
324 .unwrap();
325 assert!(out > 1_000_000);
326 }
327
328 #[test]
329 fn swap_fee_retains_portion() {
330 let net = apply_swap_fee_per_1m(1_000_000, 1_000).unwrap();
331 assert_eq!(net, 999_000);
332 }
333
334 #[test]
335 fn skew_rebalance_negative() {
336 let skew = vault_skew_contribution_per_1m(
337 900,
338 600,
339 0,
340 1000,
341 1000,
342 1000,
343 0,
344 0,
345 SkewExponent::Linear,
346 )
347 .unwrap();
348 assert!(skew < 0);
349 }
350}