pair_test.gno
3.27 Kb · 102 lines
1package pair
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9// The pricing function, on its own, is the part worth pinning numerically: it
10// is pure, it is where the 128-bit arithmetic lives, and every instance in the
11// ecosystem runs this exact code.
12func TestAmountOut(t *testing.T) {
13 cases := []struct {
14 name string
15 in, resIn, resOut, want int64
16 }{
17 {"balanced pool, small trade", 1_000, 1_000_000, 1_000_000, 996},
18 {"balanced pool, 1%", 10_000, 1_000_000, 1_000_000, 9_871},
19 {"balanced pool, 10%", 100_000, 1_000_000, 1_000_000, 90_661},
20 {"balanced pool, 50%", 500_000, 1_000_000, 1_000_000, 332_665},
21 {"skewed pool", 100_000, 1_000_000, 4_000_000, 362_644},
22 {"near drain", 1_000, 1, 1_000_000, 998_997},
23 {"dust rounds to zero", 1, 1_000_000, 1_000_000, 0},
24 {"reserves at the cap", MaxReserve / 2, MaxReserve / 2, MaxReserve, 4_604_758_097_518_382},
25 }
26 for _, tc := range cases {
27 got := AmountOut(tc.in, tc.resIn, tc.resOut)
28 uassert.Equal(t, tc.want, got, tc.name)
29 }
30}
31
32// The fee is what stays behind: k must never decrease across a swap.
33func TestConstantProductNeverRegresses(t *testing.T) {
34 resIn, resOut := int64(1_000_000), int64(4_000_000)
35 for _, in := range []int64{1, 1_000, 100_000, 999_999} {
36 out := AmountOut(in, resIn, resOut)
37 uassert.True(t, cmpProd(resIn+in, resOut-out, resIn, resOut) >= 0,
38 "k must not regress")
39 }
40}
41
42func TestAmountOutGuards(t *testing.T) {
43 mustPanic(t, "pair: amountIn must be > 0", func() {
44 AmountOut(0, 1_000, 1_000)
45 })
46 mustPanic(t, "pair: pair has an empty reserve", func() {
47 AmountOut(1, 0, 1_000)
48 })
49 mustPanic(t, "pair: reserve above cap", func() {
50 AmountOut(1, MaxReserve+1, 1_000)
51 })
52 mustPanic(t, "pair: reserve cap exceeded", func() {
53 AmountOut(2, MaxReserve-1, 1_000)
54 })
55}
56
57// mulDiv is the only place a 128-bit intermediate is required, and the only
58// place bits.Div64 could panic. Both ends are asserted rather than argued.
59func TestMulDiv(t *testing.T) {
60 uassert.Equal(t, int64(2), mulDiv(4, 3, 6))
61 uassert.Equal(t, int64(0), mulDiv(1, 1, 3), "floors")
62 uassert.Equal(t, int64(4_611_686_018_427_387_903), mulDiv(9_223_372_036_854_775_807, 1, 2),
63 "operands near MaxInt64 stay exact through the 128-bit path")
64
65 mustPanic(t, "pair: division by a non-positive value", func() {
66 mulDiv(1, 1, 0)
67 })
68 mustPanic(t, "pair: quotient overflows int64", func() {
69 mulDiv(9_223_372_036_854_775_807, 9_223_372_036_854_775_807, 1)
70 })
71}
72
73func TestCmpProd(t *testing.T) {
74 uassert.Equal(t, 0, cmpProd(6, 7, 42, 1))
75 uassert.Equal(t, -1, cmpProd(6, 6, 42, 1))
76 uassert.Equal(t, 1, cmpProd(7, 7, 42, 1))
77 // Both products exceed int64 and differ only in the low word.
78 uassert.Equal(t, -1, cmpProd(4_000_000_000, 4_000_000_000, 4_000_000_000, 4_000_000_001))
79}
80
81// mustPanic is uassert.PanicsWithMessage without the realm argument: a pure
82// package cannot produce a `cur` to hand it, since it can never declare a
83// crossing function in the first place.
84func mustPanic(t *testing.T, want string, f func()) {
85 t.Helper()
86 defer func() {
87 r := recover()
88 if r == nil {
89 t.Errorf("expected panic %q, got none", want)
90 return
91 }
92 got, ok := r.(string)
93 if !ok {
94 t.Errorf("expected a string panic %q, got %v", want, r)
95 return
96 }
97 if got != want {
98 t.Errorf("expected panic %q, got %q", want, got)
99 }
100 }()
101 f()
102}