Kind Realm (renderable)
Name v0
Namespace moul / x / amm
Exported functions 15
Module gno.land/r/moul/x/amm/v0
gno 0.9 amm_test.gno gno
⧉
1 package amm 2 3 import ( 4 "chain" 5 "testing" 6 7 "gno.land/p/nt/avl/v0" 8 AddLiquidity (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, keyA string, keyB string, maxA int64, maxB int64) int64
AllowanceLP (keyA string, keyB string, owner string, spender string) int64
AmountOut (amountIn int64, reserveIn int64, reserveOut int64) int64
ApproveLP (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, keyA string, keyB string, spender string, amount int64)
LPToken (keyA string, keyB string) string
PoolCount () int
Quote (keyIn string, keyOut string, amountIn int64) int64
RemoveLiquidity (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, keyA string, keyB string, shares int64) (int64, int64)
Render (path string) string
Reserves (keyA string, keyB string) (int64, int64)
SharesOf (keyA string, keyB string, owner string) int64
Swap (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, keyIn string, keyOut string, amountIn int64, minOut int64) int64
TotalShares (keyA string, keyB string) int64
TransferFromLP (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, keyA string, keyB string, from string, to string, amount int64)
TransferLP (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, keyA string, keyB string, to string, amount int64)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.
"gno.land/p/nt/grc20/v0"
9 "gno.land/p/nt/testutils/v0"
10 "gno.land/p/nt/uassert/v0"
11 "gno.land/r/nt/grc20reg/v0"
12 )
13
14 // Two throwaway tokens, minted and registered once for the whole test binary.
15 // They are created from this realm, so grc20reg keys them under this path.
16 var (
17 tokAAA, tokBBB *grc20.Token
18 ledAAA, ledBBB *grc20.PrivateLedger
19 keyAAA, keyBBB string
20 )
21
22 func init(cur realm) {
23 tokAAA, ledAAA = grc20.NewToken( "Test Alpha" , "AAA" , 6 , 0 , cur)
24 tokBBB, ledBBB = grc20.NewToken( "Test Beta" , "BBB" , 6 , 1 , cur)
25 keyAAA = grc20reg.Register(cross(cur), tokAAA, "" )
26 keyBBB = grc20reg.Register(cross(cur), tokBBB, "" )
27 }
28
29 func self() address { return chain.PackageAddress( "gno.land/r/moul/x/amm/v0" ) }
30
31 // fund mints both tokens to addr and approves this realm to spend them.
32 func fund(addr address, amount int64 ) {
33 ledAAA.Mint(addr, amount)
34 ledBBB.Mint(addr, amount)
35 ledAAA.Approve(addr, self(), amount)
36 ledBBB.Approve(addr, self(), amount)
37 }
38
39 // reset clears every pool so each test starts from an empty realm.
40 func reset() { pools = avl.Tree{} }
41
42 //
43 // Pure arithmetic.
44 //
45
46 func TestAmountOut(t *testing.T) {
47 cases := [] struct {
48 name string
49 in, resIn, resOut, want int64
50 }{
51 { "balanced pool, small trade" , 1_000 , 1_000_000 , 1_000_000 , 996 },
52 { "balanced pool, 1%" , 10_000 , 1_000_000 , 1_000_000 , 9_871 },
53 { "balanced pool, 10%" , 100_000 , 1_000_000 , 1_000_000 , 90_661 },
54 { "balanced pool, 50%" , 500_000 , 1_000_000 , 1_000_000 , 332_665 },
55 { "skewed pool" , 100_000 , 1_000_000 , 4_000_000 , 362_644 },
56 { "near drain" , 1_000 , 1 , 1_000_000 , 998_997 },
57 { "dust rounds to zero" , 1 , 1_000_000 , 1_000_000 , 0 },
58 { "reserves at the cap" , maxReserve / 2 , maxReserve / 2 , maxReserve, 4_604_758_097_518_382 },
59 }
60 for _, tc := range cases {
61 got := AmountOut(tc.in, tc.resIn, tc.resOut)
62 uassert.Equal(t, tc.want, got, tc.name)
63 }
64 }
65
66 func TestAmountOutNeverBreaksK(t *testing.T) {
67 resIn, resOut := int64 ( 3_000_000 ), int64 ( 7_500_000 )
68 for _, in := range [] int64 { 1 , 7 , 1_000 , 999_999 , 3_000_000 } {
69 out := AmountOut(in, resIn, resOut)
70 uassert.True(t, out < resOut, "output must stay inside the reserve" )
71 uassert.True(t, cmpProd(resIn+in, resOut-out, resIn, resOut) >= 0 , "k must not decrease" )
72 }
73 }
74
75 func TestAmountOutGuards(cur realm, t *testing.T) {
76 uassert.PanicsWithMessage(t, cur, "amm: amountIn must be > 0" , func () {
77 AmountOut( 0 , 1_000 , 1_000 )
78 })
79 uassert.PanicsWithMessage(t, cur, "amm: pool has an empty reserve" , func () {
80 AmountOut( 1 , 0 , 1_000 )
81 })
82 uassert.PanicsWithMessage(t, cur, "amm: reserve above cap" , func () {
83 AmountOut( 1 , maxReserve+ 1 , 1_000 )
84 })
85 uassert.PanicsWithMessage(t, cur, "amm: reserve cap exceeded" , func () {
86 AmountOut(maxReserve, maxReserve, 1_000 )
87 })
88 }
89
90 func TestMulDiv(t *testing.T) {
91 uassert.Equal(t, int64 ( 0 ), mulDiv( 0 , 12345 , 7 ))
92 uassert.Equal(t, int64 ( 3 ), mulDiv( 7 , 5 , 10 )) // floors
93 // 2^62 * 4 is 2^64: the intermediate does not fit in int64, the result does.
94 const big = int64 ( 1 ) << 62
95 uassert.Equal(t, big/ 2 , mulDiv(big, 4 , 8 ))
96 // maxReserve * feeDen is the widest product this realm ever forms.
97 uassert.Equal(t, int64 (maxReserve), mulDiv(maxReserve, feeDen, feeDen))
98 }
99
100 func TestCmpProd(t *testing.T) {
101 const big = int64 ( 1 ) << 40
102 uassert.Equal(t, 0 , cmpProd(big, big, big, big))
103 uassert.Equal(t, - 1 , cmpProd(big, big, big, big+ 1 ))
104 uassert.Equal(t, 1 , cmpProd(big, big+ 1 , big, big))
105 }
106
107 //
108 // Lifecycle.
109 //
110
111 func TestSeedSwapRemove(cur realm, t *testing.T) {
112 reset()
113 alice := testutils.TestAddress( "seed-alice" )
114 fund(alice, 10_000_000 )
115
116 testing.SetRealm(testing.NewUserRealm(alice))
117 heldA, heldB := tokAAA.BalanceOf(self()), tokBBB.BalanceOf(self())
118
119 // Seed: the first provider sets the price and gets shares == amountA.
120 minted := AddLiquidity(cross(cur), keyAAA, keyBBB, 1_000_000 , 4_000_000 )
121 uassert.Equal(t, int64 ( 1_000_000 ), minted)
122 uassert.Equal(t, int64 ( 1 ), int64 (PoolCount()))
123
124 rA, rB := Reserves(keyAAA, keyBBB)
125 uassert.Equal(t, int64 ( 1_000_000 ), rA)
126 uassert.Equal(t, int64 ( 4_000_000 ), rB)
127 uassert.Equal(t, heldA+ 1_000_000 , tokAAA.BalanceOf(self()), "reserve is really held" )
128
129 // Swap AAA -> BBB.
130 quoted := Quote(keyAAA, keyBBB, 100_000 )
131 uassert.Equal(t, int64 ( 362_644 ), quoted)
132 out := Swap(cross(cur), keyAAA, keyBBB, 100_000 , 362_000 )
133 uassert.Equal(t, quoted, out)
134
135 rA, rB = Reserves(keyAAA, keyBBB)
136 uassert.Equal(t, int64 ( 1_100_000 ), rA)
137 uassert.Equal(t, int64 ( 3_637_356 ), rB)
138 uassert.True(t, cmpProd(rA, rB, 1_000_000 , 4_000_000 ) > 0 , "fee grew k" )
139
140 // Burn half the position.
141 gotA, gotB := RemoveLiquidity(cross(cur), keyAAA, keyBBB, 500_000 )
142 uassert.Equal(t, int64 ( 550_000 ), gotA)
143 uassert.Equal(t, int64 ( 1_818_678 ), gotB)
144 uassert.Equal(t, int64 ( 500_000 ), TotalShares(keyAAA, keyBBB))
145
146 // Burn the rest: the last provider out takes the whole reserve.
147 gotA, gotB = RemoveLiquidity(cross(cur), keyAAA, keyBBB, 500_000 )
148 uassert.Equal(t, int64 ( 550_000 ), gotA)
149 uassert.Equal(t, int64 ( 1_818_678 ), gotB)
150 uassert.Equal(t, int64 ( 0 ), TotalShares(keyAAA, keyBBB))
151 rA, rB = Reserves(keyAAA, keyBBB)
152 uassert.Equal(t, int64 ( 0 ), rA)
153 uassert.Equal(t, int64 ( 0 ), rB)
154 uassert.Equal(t, heldA, tokAAA.BalanceOf(self()), "nothing unclaimable left behind" )
155 uassert.Equal(t, heldB, tokBBB.BalanceOf(self()), "nothing unclaimable left behind" )
156
157 // A drained pool is reseedable, at whatever price the next provider picks.
158 uassert.Equal(t, int64 ( 200_000 ), AddLiquidity(cross(cur), keyAAA, keyBBB, 200_000 , 100_000 ))
159 rA, rB = Reserves(keyAAA, keyBBB)
160 uassert.Equal(t, int64 ( 200_000 ), rA)
161 uassert.Equal(t, int64 ( 100_000 ), rB)
162 }
163
164 func TestArgumentOrderIsSymmetric(cur realm, t *testing.T) {
165 reset()
166 alice := testutils.TestAddress( "sym-alice" )
167 fund(alice, 10_000_000 )
168 testing.SetRealm(testing.NewUserRealm(alice))
169
170 AddLiquidity(cross(cur), keyBBB, keyAAA, 4_000_000 , 1_000_000 )
171 uassert.Equal(t, int64 ( 1 ), int64 (PoolCount()), "one pool whichever order is used" )
172
173 rA, rB := Reserves(keyAAA, keyBBB)
174 uassert.Equal(t, int64 ( 1_000_000 ), rA)
175 uassert.Equal(t, int64 ( 4_000_000 ), rB)
176
177 rB2, rA2 := Reserves(keyBBB, keyAAA)
178 uassert.Equal(t, rA, rA2, "reserves follow the caller's order" )
179 uassert.Equal(t, rB, rB2, "reserves follow the caller's order" )
180 }
181
182 func TestSecondProviderIsTrimmedToRatio(cur realm, t *testing.T) {
183 reset()
184 alice := testutils.TestAddress( "trim-alice" )
185 bob := testutils.TestAddress( "trim-bob" )
186 fund(alice, 10_000_000 )
187 fund(bob, 10_000_000 )
188
189 testing.SetRealm(testing.NewUserRealm(alice))
190 AddLiquidity(cross(cur), keyAAA, keyBBB, 1_000_000 , 4_000_000 )
191
192 // Bob offers a badly skewed pair: 100k AAA is only worth 400k BBB, so the
193 // extra BBB is left in his wallet and his shares price off the scarce side.
194 testing.SetRealm(testing.NewUserRealm(bob))
195 beforeB := tokBBB.BalanceOf(bob)
196 minted := AddLiquidity(cross(cur), keyAAA, keyBBB, 100_000 , 9_000_000 )
197 uassert.Equal(t, int64 ( 100_000 ), minted)
198 uassert.Equal(t, int64 ( 400_000 ), beforeB-tokBBB.BalanceOf(bob), "only the ratio amount was taken" )
199
200 rA, rB := Reserves(keyAAA, keyBBB)
201 uassert.Equal(t, int64 ( 1_100_000 ), rA)
202 uassert.Equal(t, int64 ( 4_400_000 ), rB)
203 uassert.Equal(t, int64 ( 1_100_000 ), TotalShares(keyAAA, keyBBB))
204
205 // Round-trip: Bob can never take out more than he put in.
206 gotA, gotB := RemoveLiquidity(cross(cur), keyAAA, keyBBB, minted)
207 uassert.True(t, gotA <= 100_000 , "no value created on the A side" )
208 uassert.True(t, gotB <= 400_000 , "no value created on the B side" )
209 }
210
211 func TestDonationIsInert(cur realm, t *testing.T) {
212 reset()
213 alice := testutils.TestAddress( "don-alice" )
214 mallory := testutils.TestAddress( "don-mallory" )
215 fund(alice, 10_000_000 )
216 fund(mallory, 10_000_000 )
217
218 testing.SetRealm(testing.NewUserRealm(alice))
219 AddLiquidity(cross(cur), keyAAA, keyBBB, 1_000_000 , 1_000_000 )
220 before := Quote(keyAAA, keyBBB, 10_000 )
221
222 // Mallory sends 5,000,000 AAA straight to the realm address, the classic
223 // setup for a share-inflation or price-manipulation attack.
224 heldA := tokAAA.BalanceOf(self())
225 ledAAA.Transfer(mallory, self(), 5_000_000 )
226 uassert.Equal(t, heldA+ 5_000_000 , tokAAA.BalanceOf(self()), "the balance really moved" )
227
228 rA, rB := Reserves(keyAAA, keyBBB)
229 uassert.Equal(t, int64 ( 1_000_000 ), rA, "reserves ignore balances" )
230 uassert.Equal(t, int64 ( 1_000_000 ), rB, "reserves ignore balances" )
231 uassert.Equal(t, before, Quote(keyAAA, keyBBB, 10_000 ), "price is unchanged" )
232
233 // And a later provider is priced off reserves, not off the donation.
234 testing.SetRealm(testing.NewUserRealm(mallory))
235 uassert.Equal(t, int64 ( 1_000_000 ), AddLiquidity(cross(cur), keyAAA, keyBBB, 1_000_000 , 1_000_000 ))
236 }
237
238 //
239 // Guards.
240 //
241
242 func TestGuards(cur realm, t *testing.T) {
243 reset()
244 alice := testutils.TestAddress( "guard-alice" )
245 fund(alice, 10_000_000 )
246 testing.SetRealm(testing.NewUserRealm(alice))
247
248 uassert.AbortsWithMessage(t, cur, "amm: a pool needs two different tokens" , func () {
249 AddLiquidity(cross(cur), keyAAA, keyAAA, 1 , 1 )
250 })
251 uassert.AbortsWithMessage(t, cur, "amm: both deposit amounts must be > 0" , func () {
252 AddLiquidity(cross(cur), keyAAA, keyBBB, 0 , 1 )
253 })
254 uassert.AbortsWithMessage(t, cur, "amm: no such pool: " +keyAAA+ "~" +keyBBB, func () {
255 Swap(cross(cur), keyAAA, keyBBB, 1 , 0 )
256 })
257
258 AddLiquidity(cross(cur), keyAAA, keyBBB, 1_000_000 , 1_000_000 )
259
260 uassert.AbortsWithMessage(t, cur, "amm: slippage, output below minOut" , func () {
261 Swap(cross(cur), keyAAA, keyBBB, 10_000 , 10_000 )
262 })
263 uassert.AbortsWithMessage(t, cur, "amm: output rounds to zero" , func () {
264 Swap(cross(cur), keyAAA, keyBBB, 1 , 0 )
265 })
266 uassert.AbortsWithMessage(t, cur, "amm: amountIn must be > 0" , func () {
267 Swap(cross(cur), keyAAA, keyBBB, 0 , 0 )
268 })
269 uassert.AbortsWithMessage(t, cur, "amm: insufficient shares" , func () {
270 RemoveLiquidity(cross(cur), keyAAA, keyBBB, 2_000_000 )
271 })
272 uassert.AbortsWithMessage(t, cur, "amm: reserve cap exceeded" , func () {
273 AddLiquidity(cross(cur), keyAAA, keyBBB, maxReserve, maxReserve)
274 })
275
276 // An allowance that does not cover the deposit aborts, and nothing moved.
277 bob := testutils.TestAddress( "guard-bob" )
278 ledAAA.Mint(bob, 1_000_000 )
279 ledBBB.Mint(bob, 1_000_000 )
280 ledAAA.Approve(bob, self(), 10 )
281 ledBBB.Approve(bob, self(), 1_000_000 )
282 testing.SetRealm(testing.NewUserRealm(bob))
283 uassert.AbortsContains(t, cur, "insufficient allowance" , func () {
284 AddLiquidity(cross(cur), keyAAA, keyBBB, 100_000 , 100_000 )
285 })
286 uassert.Equal(t, int64 ( 1_000_000 ), tokAAA.BalanceOf(bob), "nothing moved: the first pull failed" )
287 uassert.Equal(t, int64 ( 0 ), SharesOf(keyAAA, keyBBB, bob))
288 }
289