Skip to main content

cp_curve/
lib.rs

1#![no_std]
2
3/// Errors returnable by every function in this crate.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum CurveError {
6    /// An arithmetic operation overflowed even at u128.
7    Overflow,
8    /// Caller passed a zero amount where a positive amount is required.
9    ZeroInput,
10    /// Pool reserves are zero and the operation requires a non-empty pool.
11    EmptyPool,
12    /// fee_bps >= 10_000 (>=100%).
13    InvalidFee,
14    /// LP burn amount exceeds total LP supply.
15    InsufficientLpSupply,
16}
17
18/// Returned by `deposit_amounts`: how much of each token was actually used,
19/// and how many LP tokens to mint.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct DepositResult {
22    pub amount_x_used: u64,
23    pub amount_y_used: u64,
24    pub lp_minted:     u64,
25}
26
27/// Given pool reserves and an input amount, compute how much of the other
28/// token the swapper receives. **No fee.**
29///
30/// Returns `Err(ZeroInput)` if `amount_in == 0`.
31/// Returns `Err(EmptyPool)` if either reserve is zero.
32pub fn swap_output(
33    reserve_in:  u64,
34    reserve_out: u64,
35    amount_in:   u64,
36) -> Result<u64, CurveError> {
37    if amount_in == 0 {
38        return Err(CurveError::ZeroInput);
39    }
40
41    if reserve_in == 0 || reserve_out == 0 {
42        return Err(CurveError::EmptyPool);
43    }
44
45    let reserve_in = reserve_in as u128;
46    let reserve_out = reserve_out as u128;
47    let amount_in = amount_in as u128;
48
49    let numerator = amount_in
50        .checked_mul(reserve_out)
51        .ok_or(CurveError::Overflow)?;
52
53    let denominator = reserve_in
54        .checked_add(amount_in)
55        .ok_or(CurveError::Overflow)?;
56
57    let amount_out = numerator / denominator;
58
59    u64::try_from(amount_out).map_err(|_| CurveError::Overflow)
60    
61}
62
63/// Same as `swap_output` but takes a fee (in basis points) from the input
64/// before the swap. The fee stays in the pool.
65pub fn swap_output_with_fee(
66    reserve_in:  u64,
67    reserve_out: u64,
68    amount_in:   u64,
69    fee_bps:     u16,
70) -> Result<u64, CurveError> {
71    if amount_in == 0 {
72        return Err(CurveError::ZeroInput);
73    }
74
75    if reserve_in == 0 || reserve_out == 0 {
76        return Err(CurveError::EmptyPool);
77    }
78
79    if fee_bps >= 10_000 {
80        return Err(CurveError::InvalidFee);
81    }
82
83    let reserve_in = reserve_in as u128;
84    let reserve_out = reserve_out as u128;
85    let amount_in = amount_in as u128;
86    let fee_bps = fee_bps as u128;
87
88    let amount_in_with_fee = amount_in
89        .checked_mul(10_000 - fee_bps)
90        .ok_or(CurveError::Overflow)?;
91
92    let numerator = amount_in_with_fee
93        .checked_mul(reserve_out)
94        .ok_or(CurveError::Overflow)?;
95
96    let reserve_in_scaled = reserve_in
97        .checked_mul(10_000)
98        .ok_or(CurveError::Overflow)?;
99
100    let denominator = reserve_in_scaled
101        .checked_add(amount_in_with_fee)
102        .ok_or(CurveError::Overflow)?;
103
104    let amount_out = numerator / denominator;
105
106    u64::try_from(amount_out).map_err(|_| CurveError::Overflow)
107    
108}
109
110/// Compute how much of each input token to actually pull, and how many LP
111/// tokens to mint, given proposed deposit amounts.
112///
113/// Handles the first-deposit case (total_lp == 0) via integer sqrt.
114pub fn deposit_amounts(
115    reserve_x:    u64,
116    reserve_y:    u64,
117    total_lp:     u64,
118    amount_x_in:  u64,
119    amount_y_in:  u64,
120) -> Result<DepositResult, CurveError> {
121    if amount_x_in == 0 || amount_y_in == 0 {
122        return Err(CurveError::ZeroInput);
123    }
124
125    if total_lp == 0 {
126        let product = (amount_x_in as u128) * (amount_y_in as u128);
127        let lp =  integer_sqrt(product);
128        let lp_minted = u64::try_from(lp).map_err(|_| CurveError::Overflow)?;
129        return Ok(DepositResult { amount_x_used: amount_x_in, amount_y_used: amount_y_in, lp_minted });
130    }
131
132    if reserve_x == 0 || reserve_y == 0 {
133        return Err(CurveError::EmptyPool);
134    }
135
136    let reserve_x = reserve_x as u128;
137    let reserve_y = reserve_y as u128;
138    let total_lp = total_lp as u128;
139    let amount_x_in = amount_x_in as u128;
140    let amount_y_in = amount_y_in as u128;
141
142    let amount_y_optimal = amount_x_in
143        .checked_mul(reserve_y)
144        .ok_or(CurveError::Overflow)?
145        / reserve_x;
146
147    let (amount_x_used, amount_y_used) = if amount_y_optimal <= amount_y_in {
148        (amount_x_in, amount_y_optimal)
149    } else {
150        let amount_x_optimal = amount_y_in
151            .checked_mul(reserve_x)
152            .ok_or(CurveError::Overflow)? / reserve_y;
153            (amount_x_optimal, amount_y_in)
154    };
155
156    let lp_from_x = amount_x_used
157        .checked_mul(total_lp)
158        .ok_or(CurveError::Overflow)? / reserve_x;
159
160    let lp_from_y = amount_y_used
161        .checked_mul(total_lp)
162        .ok_or(CurveError::Overflow)? / reserve_y;
163
164    let lp_minted = lp_from_x.min(lp_from_y);
165
166    Ok(DepositResult {
167        amount_x_used: u64::try_from(amount_x_used).map_err(|_| CurveError::Overflow)?, 
168        amount_y_used: u64::try_from(amount_y_used).map_err(|_| CurveError::Overflow)?, 
169        lp_minted: u64::try_from(lp_minted).map_err(|_| CurveError::Overflow)? 
170    })
171}
172
173/// Compute how much of each underlying token to return to the LP for burning
174/// `lp_burn` LP tokens.
175pub fn withdraw_amounts(
176    reserve_x: u64,
177    reserve_y: u64,
178    total_lp:  u64,
179    lp_burn:   u64,
180) -> Result<(u64, u64), CurveError> {
181    if lp_burn == 0 {
182        return Err(CurveError::ZeroInput);
183    }
184
185    if total_lp == 0 || lp_burn > total_lp {
186        return Err(CurveError::InsufficientLpSupply);
187    }
188
189    let reserve_x = reserve_x as u128;
190    let reserve_y = reserve_y as u128;
191    let total_lp = total_lp as u128;
192    let lp_burn = lp_burn as u128;
193
194    let x_out = lp_burn
195        .checked_mul(reserve_x)
196        .ok_or(CurveError::Overflow)?
197        / total_lp;
198
199    let y_out = lp_burn
200        .checked_mul(reserve_y)
201        .ok_or(CurveError::Overflow)?
202        / total_lp;
203
204    let x_out = u64::try_from(x_out).map_err(|_| CurveError::Overflow)?;
205    let y_out = u64::try_from(y_out).map_err(|_| CurveError::Overflow)?;
206
207    Ok((x_out, y_out))
208}
209
210/// Integer square root, Newton's method on u128.
211///
212/// Returns the largest `n` such that `n * n <= value`.
213pub fn integer_sqrt(value: u128) -> u128 {
214    if value == 0 {
215        return 0;
216    }
217
218    if value < 4 {
219        return 1;
220    }
221
222    let mut z = value;
223    let mut x = value / 2 + 1;
224
225    while x < z {
226        z = x;
227        x = (value / x + x) / 2;
228    }
229    z
230}
231
232
233#[cfg(test)]
234mod tests {
235    use core::u128;
236
237    use super::*;
238    
239    #[test]
240    fn lifecycle_deposit_withdraw_no_swap_round_trips_exact() {
241          let dep = deposit_amounts(0, 0, 0, 1000, 1000).unwrap();
242          assert_eq!(
243              dep,
244              DepositResult { amount_x_used: 1000, amount_y_used: 1000, lp_minted: 1000 },
245          );
246    
247          let (x_out, y_out) =
248              withdraw_amounts(dep.amount_x_used, dep.amount_y_used, dep.lp_minted, dep.lp_minted)
249                  .unwrap();
250          assert_eq!((x_out, y_out), (1000, 1000));
251      }
252    
253      #[test]
254      fn lifecycle_two_lps_proportional_withdrawal() {
255          let alice = deposit_amounts(0, 0, 0, 1000, 1000).unwrap();
256          let (mut rx, mut ry, mut tlp) =
257              (alice.amount_x_used, alice.amount_y_used, alice.lp_minted);
258    
259          let bob = deposit_amounts(rx, ry, tlp, 500, 500).unwrap();
260          rx  += bob.amount_x_used;
261          ry  += bob.amount_y_used;
262          tlp += bob.lp_minted;
263          assert_eq!((rx, ry, tlp), (1500, 1500, 1500));
264
265          let (ax, ay) = withdraw_amounts(rx, ry, tlp, alice.lp_minted).unwrap();
266          assert_eq!((ax, ay), (1000, 1000));
267          rx  -= ax;
268          ry  -= ay;
269          tlp -= alice.lp_minted;
270
271          let (bx, by) = withdraw_amounts(rx, ry, tlp, bob.lp_minted).unwrap();
272          assert_eq!((bx, by), (500, 500));
273      }
274    
275      #[test]
276      fn lifecycle_swap_grows_lp_value() {
277          let dep = deposit_amounts(0, 0, 0, 1000, 1000).unwrap();
278          let (mut rx, mut ry) = (dep.amount_x_used, dep.amount_y_used);
279          let tlp = dep.lp_minted;
280    
281          let amount_out = swap_output_with_fee(rx, ry, 100, 30).unwrap();
282          rx += 100;
283          ry -= amount_out;
284          
285          let (x_out, y_out) = withdraw_amounts(rx, ry, tlp, tlp).unwrap();
286    
287          let value_out = (x_out as u128) * (y_out as u128);
288          let value_in  = 1000u128 * 1000u128;
289          assert!(value_out > value_in);
290      }
291    
292      #[test]
293      fn lifecycle_late_lp_does_not_steal_earlier_fees() {
294          let alice = deposit_amounts(0, 0, 0, 1000, 1000).unwrap();
295          let (mut rx, mut ry, mut tlp) =
296              (alice.amount_x_used, alice.amount_y_used, alice.lp_minted);
297    
298          let bob_out = swap_output_with_fee(rx, ry, 100, 30).unwrap();
299          rx += 100;
300          ry -= bob_out;
301    
302          let charlie = deposit_amounts(rx, ry, tlp, 500, 500).unwrap();
303          let charlie_in_x = charlie.amount_x_used;
304          let charlie_in_y = charlie.amount_y_used;
305          rx  += charlie_in_x;
306          ry  += charlie_in_y;
307          tlp += charlie.lp_minted;
308    
309          let (ax, ay) = withdraw_amounts(rx, ry, tlp, alice.lp_minted).unwrap();
310          assert!((ax as u128) * (ay as u128) > 1_000_000);
311          rx  -= ax;
312          ry  -= ay;
313          tlp -= alice.lp_minted;
314    
315          let (cx, cy) = withdraw_amounts(rx, ry, tlp, charlie.lp_minted).unwrap();
316          assert!(cx <= charlie_in_x);
317          assert!(charlie_in_x - cx <= 2);
318          assert!(cy <= charlie_in_y);
319          assert!(charlie_in_y - cy <= 2);
320      }
321    
322      #[test]
323      fn lifecycle_extreme_inputs_dont_panic() {
324          let big: u64 = 10_u64.pow(15);
325    
326          let dep = deposit_amounts(0, 0, 0, big, big).unwrap();
327          let (mut rx, mut ry, mut tlp) = (dep.amount_x_used, dep.amount_y_used, dep.lp_minted);
328    
329          let out = swap_output_with_fee(rx, ry, big / 10, 30).unwrap();
330          rx += big / 10;
331          ry -= out;
332    
333          let dep2 = deposit_amounts(rx, ry, tlp, big / 2, big / 2).unwrap();
334          rx  += dep2.amount_x_used;
335          ry  += dep2.amount_y_used;
336          tlp += dep2.lp_minted;
337    
338          let (x, y) = withdraw_amounts(rx, ry, tlp, tlp / 2).unwrap();
339          assert!(x > 0);
340          assert!(y > 0);
341      }
342
343    #[test]
344    fn swap_basic_no_fee() {
345        assert_eq!(swap_output(100, 100, 50), Ok(33));
346    }
347
348    #[test]
349    fn swap_zero_input_error() {
350        assert_eq!(swap_output(100, 100, 0), Err(CurveError::ZeroInput));
351    }
352
353    #[test]
354    fn swap_empty_pool_errors() {
355        assert_eq!(swap_output(0, 100, 50), Err(CurveError::EmptyPool));
356        assert_eq!(swap_output(100, 0, 50), Err(CurveError::EmptyPool));
357    }
358
359    #[test]
360    fn swap_big_numbers_need_u128() {
361        assert_eq!(swap_output(10_000_000_000, 10_000_000_000, 10_000_000_000), Ok(5_000_000_000));
362    }
363
364    #[test]
365    fn swap_at_u64_max_does_not_panic() {
366        let result = swap_output(u64::MAX, u64::MAX, u64::MAX);
367        assert!(result.is_ok());
368        assert!(result.unwrap() < u64::MAX);
369    }
370
371    #[test]
372    fn swap_preserves_invariant() {
373        let reserve_x: u64 = 1000;
374        let reserve_y: u64 = 1000;
375        let amount_in: u64 = 250;
376
377        let amount_out = swap_output(reserve_x, reserve_y, amount_in).unwrap();
378
379        let new_x = reserve_x + amount_in;
380        let new_y = reserve_y - amount_out;
381
382        let old_k = (reserve_x as u128) * (reserve_y as u128);
383        let new_k = (new_x as u128) * (new_y as u128);
384
385        assert!(new_k >= old_k);
386    }
387
388    #[test]
389    fn swap_tiny_input() {
390        assert_eq!(swap_output(1_000_000, 1_000_000, 1), Ok(0));
391    }
392
393    #[test]
394    fn swap_drains_almost_all() {
395        let amount_out = swap_output(1000, 1000, 1_000_000).unwrap();
396        assert_eq!(amount_out, 999);
397        assert!(amount_out < 1000);
398    }
399
400    #[test]
401    fn swap_with_fee_matches_no_fee_when_zero() {
402        let no_fee = swap_output(1000, 1000, 250).unwrap();
403        let zero_fee = swap_output_with_fee(1000, 1000, 250, 0).unwrap();
404
405        assert_eq!(no_fee, zero_fee);
406    }
407
408    #[test]
409    fn swap_with_fee_30_bps_basic() {
410        assert_eq!(swap_output_with_fee(1000, 1000, 100, 30), Ok(90));
411    }
412
413    #[test]
414    fn swap_with_fee_invalid_fee_errors() {
415        assert_eq!(swap_output_with_fee(1000, 1000, 100, 10_000), Err(CurveError::InvalidFee));
416        assert_eq!(swap_output_with_fee(1000, 1000, 100, 10_001), Err(CurveError::InvalidFee));
417    }
418
419    #[test]
420    fn swap_with_fee_preserves_invariant() {
421        let reserve_x: u64 = 1_000_000;
422        let reserve_y: u64 = 1_000_000;
423        let amount_in: u64 = 250_000;
424        let fee_bps: u16 = 30;
425
426        let amount_out = swap_output_with_fee(reserve_x, reserve_y, amount_in, fee_bps).unwrap();
427
428        let new_x = reserve_x + amount_in;
429        let new_y = reserve_y - amount_out;
430
431        let old_k = (reserve_x as u128) * (reserve_y as u128);
432        let new_k = (new_x as u128) * (new_y as u128);
433
434        assert!(new_k > old_k);
435    }
436
437    #[test]
438    fn sqrt_zero() {
439        assert_eq!(integer_sqrt(0), 0);
440    }
441
442    #[test]
443    fn sqrt_small() {
444        assert_eq!(integer_sqrt(1), 1);
445        assert_eq!(integer_sqrt(2), 1);
446        assert_eq!(integer_sqrt(3), 1);
447        assert_eq!(integer_sqrt(4), 2);
448    }
449
450    #[test]
451    fn sqrt_perfect_sqaures() {
452        assert_eq!(integer_sqrt(9), 3);
453        assert_eq!(integer_sqrt(16), 4);
454        assert_eq!(integer_sqrt(100), 10);
455        assert_eq!(integer_sqrt(1_000_000), 1000);
456    }
457
458    #[test]
459    fn sqrt_non_perfect_floors() {
460          assert_eq!(integer_sqrt(15), 3);
461          assert_eq!(integer_sqrt(99), 9);
462          assert_eq!(integer_sqrt(1_000_001), 1000);
463    }
464
465    #[test]
466    fn sqrt_big() {
467        assert_eq!(integer_sqrt(1_000_000_000_000_000_000), 1_000_000_000);
468    }
469
470    #[test]
471    fn sqrt_property() {
472        for value in [50u128, 99, 1000, 1_000_001, 12_345_678] {
473            let r = integer_sqrt(value);
474            assert!(r * r <= value);
475            assert!((r + 1) * (r + 1) > value);
476        }
477    }
478
479    #[test]
480    fn sqrt_u128_max_does_not_panic() {
481        let r = integer_sqrt(u128::MAX);
482        assert_eq!(r, u64::MAX as u128);
483    }
484
485    #[test]
486    fn deposit_first_simple() {
487        let r = deposit_amounts(0, 0, 0, 100, 100).unwrap();
488        assert_eq!(r, DepositResult {amount_x_used: 100, amount_y_used: 100, lp_minted: 100});
489    }
490
491    #[test]
492    fn deposit_first_asymmetric() {
493        let r = deposit_amounts(0, 0, 0, 200, 50).unwrap();
494        assert_eq!(r, DepositResult {amount_x_used: 200, amount_y_used: 50, lp_minted: 100});
495    }
496
497    #[test]
498    fn deposit_proportional() {
499        let r = deposit_amounts(1000, 1000, 1000, 100, 100).unwrap();
500        assert_eq!(r, DepositResult {amount_x_used: 100, amount_y_used: 100, lp_minted: 100});
501    }
502
503    #[test]
504    fn deposit_x_constrained() {
505        let r = deposit_amounts(1000, 2000, 1000, 100, 100).unwrap();
506        assert_eq!(r, DepositResult {amount_x_used: 50, amount_y_used: 100, lp_minted: 50});
507    }
508
509    #[test]
510    fn deposit_y_constrained() {
511         let r = deposit_amounts(1000, 2000, 1000, 100, 500).unwrap();
512         assert_eq!(r, DepositResult { amount_x_used: 100, amount_y_used: 200, lp_minted: 100 });
513    }
514     
515    #[test]
516    fn deposit_zero_input_errors() {
517        assert_eq!(deposit_amounts(1000, 1000, 1000, 0, 100), Err(CurveError::ZeroInput));
518        assert_eq!(deposit_amounts(1000, 1000, 1000, 100, 0), Err(CurveError::ZeroInput));
519    }
520
521    #[test]
522    fn deposit_inconsistent_pool_errors() {
523        assert_eq!(deposit_amounts(0, 100, 1000, 50, 50), Err(CurveError::EmptyPool));
524        assert_eq!(deposit_amounts(100, 0, 1000, 50, 50), Err(CurveError::EmptyPool));
525    }
526
527    #[test]
528    fn withdraw_proportional() {
529        assert_eq!(withdraw_amounts(1000, 1000, 1000, 500), Ok((500, 500)));
530    }
531
532    #[test]
533    fn withdraw_zero_lp_errors() {
534        assert_eq!(withdraw_amounts(1000, 1000, 1000, 0), Err(CurveError::ZeroInput));
535    }
536
537    #[test]
538    fn withdraw_full() {
539        assert_eq!(withdraw_amounts(1000, 1000, 1000, 1000), Ok((1000, 1000)));
540    }
541
542    #[test]
543    fn withdraw_asymmetric_reserves() {
544        assert_eq!(withdraw_amounts(1000, 2000, 1000, 250), Ok((250, 500)));
545    }
546
547    #[test]
548    fn withdraw_to_much_errors() {
549        assert_eq!(withdraw_amounts(1000, 1000, 1000, 1001), Err(CurveError::InsufficientLpSupply));
550    }
551
552    #[test]
553    fn withdraw_empty_supply() {
554        assert_eq!(withdraw_amounts(0, 0, 0, 50), Err(CurveError::InsufficientLpSupply));
555    }
556
557    #[test]
558    fn withdraw_rounds_down() {
559        assert_eq!(withdraw_amounts(100, 100, 3, 1), Ok((33, 33)));
560    }
561}