rippled
Loading...
Searching...
No Matches
AMM_test.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2023 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19#include <test/jtx.h>
20#include <test/jtx/AMM.h>
21#include <test/jtx/AMMTest.h>
22#include <test/jtx/amount.h>
23#include <test/jtx/sendmax.h>
24#include <xrpld/app/misc/AMMHelpers.h>
25#include <xrpld/app/misc/AMMUtils.h>
26#include <xrpld/app/paths/AMMContext.h>
27#include <xrpld/app/paths/AMMOffer.h>
28#include <xrpld/app/tx/detail/AMMBid.h>
29#include <xrpld/rpc/RPCHandler.h>
30#include <xrpld/rpc/detail/RPCHelpers.h>
31#include <xrpl/basics/Number.h>
32#include <xrpl/protocol/AMMCore.h>
33#include <xrpl/protocol/Feature.h>
34#include <xrpl/protocol/STParsedJSON.h>
35#include <xrpl/resource/Fees.h>
36
37#include <chrono>
38#include <utility>
39#include <vector>
40
41#include <boost/regex.hpp>
42
43namespace ripple {
44namespace test {
45
50struct AMM_test : public jtx::AMMTest
51{
52private:
53 void
55 {
56 testcase("Instance Create");
57
58 using namespace jtx;
59
60 // XRP to IOU
61 testAMM([&](AMM& ammAlice, Env&) {
62 BEAST_EXPECT(ammAlice.expectBalances(
63 XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
64 });
65
66 // IOU to IOU
67 testAMM(
68 [&](AMM& ammAlice, Env&) {
69 BEAST_EXPECT(ammAlice.expectBalances(
70 USD(20'000), BTC(0.5), IOUAmount{100, 0}));
71 },
72 {{USD(20'000), BTC(0.5)}});
73
74 // IOU to IOU + transfer fee
75 {
76 Env env{*this};
77 fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
78 env(rate(gw, 1.25));
79 env.close();
80 // no transfer fee on create
81 AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
82 BEAST_EXPECT(ammAlice.expectBalances(
83 USD(20'000), BTC(0.5), IOUAmount{100, 0}));
84 BEAST_EXPECT(expectLine(env, alice, USD(0)));
85 BEAST_EXPECT(expectLine(env, alice, BTC(0)));
86 }
87
88 // Require authorization is set, account is authorized
89 {
90 Env env{*this};
91 env.fund(XRP(30'000), gw, alice);
92 env.close();
93 env(fset(gw, asfRequireAuth));
94 env(trust(alice, gw["USD"](30'000), 0));
95 env(trust(gw, alice["USD"](0), tfSetfAuth));
96 env.close();
97 env(pay(gw, alice, USD(10'000)));
98 env.close();
99 AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
100 }
101
102 // Cleared global freeze
103 {
104 Env env{*this};
105 env.fund(XRP(30'000), gw, alice);
106 env.close();
107 env.trust(USD(30'000), alice);
108 env.close();
109 env(pay(gw, alice, USD(10'000)));
110 env.close();
111 env(fset(gw, asfGlobalFreeze));
112 env.close();
113 AMM ammAliceFail(
114 env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
116 env.close();
117 AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
118 }
119
120 // Trading fee
121 testAMM(
122 [&](AMM& amm, Env&) {
123 BEAST_EXPECT(amm.expectTradingFee(1'000));
124 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
125 },
126 std::nullopt,
127 1'000);
128
129 // Make sure asset comparison works.
130 BEAST_EXPECT(
131 STIssue(sfAsset, STAmount(XRP(2'000)).issue()) ==
132 STIssue(sfAsset, STAmount(XRP(2'000)).issue()));
133 BEAST_EXPECT(
134 STIssue(sfAsset, STAmount(XRP(2'000)).issue()) !=
135 STIssue(sfAsset, STAmount(USD(2'000)).issue()));
136 }
137
138 void
140 {
141 testcase("Invalid Instance");
142
143 using namespace jtx;
144
145 // Can't have both XRP tokens
146 {
147 Env env{*this};
148 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
149 AMM ammAlice(
150 env, alice, XRP(10'000), XRP(10'000), ter(temBAD_AMM_TOKENS));
151 BEAST_EXPECT(!ammAlice.ammExists());
152 }
153
154 // Can't have both tokens the same IOU
155 {
156 Env env{*this};
157 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
158 AMM ammAlice(
159 env, alice, USD(10'000), USD(10'000), ter(temBAD_AMM_TOKENS));
160 BEAST_EXPECT(!ammAlice.ammExists());
161 }
162
163 // Can't have zero or negative amounts
164 {
165 Env env{*this};
166 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
167 AMM ammAlice(env, alice, XRP(0), USD(10'000), ter(temBAD_AMOUNT));
168 BEAST_EXPECT(!ammAlice.ammExists());
169 AMM ammAlice1(env, alice, XRP(10'000), USD(0), ter(temBAD_AMOUNT));
170 BEAST_EXPECT(!ammAlice1.ammExists());
171 AMM ammAlice2(
172 env, alice, XRP(10'000), USD(-10'000), ter(temBAD_AMOUNT));
173 BEAST_EXPECT(!ammAlice2.ammExists());
174 AMM ammAlice3(
175 env, alice, XRP(-10'000), USD(10'000), ter(temBAD_AMOUNT));
176 BEAST_EXPECT(!ammAlice3.ammExists());
177 }
178
179 // Bad currency
180 {
181 Env env{*this};
182 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
183 AMM ammAlice(
184 env, alice, XRP(10'000), BAD(10'000), ter(temBAD_CURRENCY));
185 BEAST_EXPECT(!ammAlice.ammExists());
186 }
187
188 // Insufficient IOU balance
189 {
190 Env env{*this};
191 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
192 AMM ammAlice(
193 env, alice, XRP(10'000), USD(40'000), ter(tecUNFUNDED_AMM));
194 BEAST_EXPECT(!ammAlice.ammExists());
195 }
196
197 // Insufficient XRP balance
198 {
199 Env env{*this};
200 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
201 AMM ammAlice(
202 env, alice, XRP(40'000), USD(10'000), ter(tecUNFUNDED_AMM));
203 BEAST_EXPECT(!ammAlice.ammExists());
204 }
205
206 // Invalid trading fee
207 {
208 Env env{*this};
209 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
210 AMM ammAlice(
211 env,
212 alice,
213 XRP(10'000),
214 USD(10'000),
215 false,
216 65'001,
217 10,
218 std::nullopt,
219 std::nullopt,
220 std::nullopt,
221 ter(temBAD_FEE));
222 BEAST_EXPECT(!ammAlice.ammExists());
223 }
224
225 // AMM already exists
226 testAMM([&](AMM& ammAlice, Env& env) {
227 AMM ammCarol(
228 env, carol, XRP(10'000), USD(10'000), ter(tecDUPLICATE));
229 });
230
231 // Invalid flags
232 {
233 Env env{*this};
234 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
235 AMM ammAlice(
236 env,
237 alice,
238 XRP(10'000),
239 USD(10'000),
240 false,
241 0,
242 10,
244 std::nullopt,
245 std::nullopt,
247 BEAST_EXPECT(!ammAlice.ammExists());
248 }
249
250 // Invalid Account
251 {
252 Env env{*this};
253 Account bad("bad");
254 env.memoize(bad);
255 AMM ammAlice(
256 env,
257 bad,
258 XRP(10'000),
259 USD(10'000),
260 false,
261 0,
262 10,
263 std::nullopt,
264 seq(1),
265 std::nullopt,
267 BEAST_EXPECT(!ammAlice.ammExists());
268 }
269
270 // Require authorization is set
271 {
272 Env env{*this};
273 env.fund(XRP(30'000), gw, alice);
274 env.close();
275 env(fset(gw, asfRequireAuth));
276 env.close();
277 env(trust(gw, alice["USD"](30'000)));
278 env.close();
279 AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecNO_AUTH));
280 BEAST_EXPECT(!ammAlice.ammExists());
281 }
282
283 // Globally frozen
284 {
285 Env env{*this};
286 env.fund(XRP(30'000), gw, alice);
287 env.close();
288 env(fset(gw, asfGlobalFreeze));
289 env.close();
290 env(trust(gw, alice["USD"](30'000)));
291 env.close();
292 AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
293 BEAST_EXPECT(!ammAlice.ammExists());
294 }
295
296 // Individually frozen
297 {
298 Env env{*this};
299 env.fund(XRP(30'000), gw, alice);
300 env.close();
301 env(trust(gw, alice["USD"](30'000)));
302 env.close();
303 env(trust(gw, alice["USD"](0), tfSetFreeze));
304 env.close();
305 AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
306 BEAST_EXPECT(!ammAlice.ammExists());
307 }
308
309 // Insufficient reserve, XRP/IOU
310 {
311 Env env(*this);
312 auto const starting_xrp =
313 XRP(1'000) + reserve(env, 3) + env.current()->fees().base * 4;
314 env.fund(starting_xrp, gw);
315 env.fund(starting_xrp, alice);
316 env.trust(USD(2'000), alice);
317 env.close();
318 env(pay(gw, alice, USD(2'000)));
319 env.close();
320 env(offer(alice, XRP(101), USD(100)));
321 env(offer(alice, XRP(102), USD(100)));
322 AMM ammAlice(
323 env, alice, XRP(1'000), USD(1'000), ter(tecUNFUNDED_AMM));
324 }
325
326 // Insufficient reserve, IOU/IOU
327 {
328 Env env(*this);
329 auto const starting_xrp =
330 reserve(env, 4) + env.current()->fees().base * 5;
331 env.fund(starting_xrp, gw);
332 env.fund(starting_xrp, alice);
333 env.trust(USD(2'000), alice);
334 env.trust(EUR(2'000), alice);
335 env.close();
336 env(pay(gw, alice, USD(2'000)));
337 env(pay(gw, alice, EUR(2'000)));
338 env.close();
339 env(offer(alice, EUR(101), USD(100)));
340 env(offer(alice, EUR(102), USD(100)));
341 AMM ammAlice(
342 env, alice, EUR(1'000), USD(1'000), ter(tecINSUF_RESERVE_LINE));
343 }
344
345 // Insufficient fee
346 {
347 Env env(*this);
348 fund(env, gw, {alice}, XRP(2'000), {USD(2'000), EUR(2'000)});
349 AMM ammAlice(
350 env,
351 alice,
352 EUR(1'000),
353 USD(1'000),
354 false,
355 0,
356 ammCrtFee(env).drops() - 1,
357 std::nullopt,
358 std::nullopt,
359 std::nullopt,
361 }
362
363 // AMM with LPTokens
364
365 // AMM with one LPToken from another AMM.
366 testAMM([&](AMM& ammAlice, Env& env) {
367 fund(env, gw, {alice}, {EUR(10'000)}, Fund::IOUOnly);
368 AMM ammAMMToken(
369 env,
370 alice,
371 EUR(10'000),
372 STAmount{ammAlice.lptIssue(), 1'000'000},
374 AMM ammAMMToken1(
375 env,
376 alice,
377 STAmount{ammAlice.lptIssue(), 1'000'000},
378 EUR(10'000),
380 });
381
382 // AMM with two LPTokens from other AMMs.
383 testAMM([&](AMM& ammAlice, Env& env) {
384 fund(env, gw, {alice}, {EUR(10'000)}, Fund::IOUOnly);
385 AMM ammAlice1(env, alice, XRP(10'000), EUR(10'000));
386 auto const token1 = ammAlice.lptIssue();
387 auto const token2 = ammAlice1.lptIssue();
388 AMM ammAMMTokens(
389 env,
390 alice,
391 STAmount{token1, 1'000'000},
392 STAmount{token2, 1'000'000},
394 });
395
396 // Issuer has DefaultRipple disabled
397 {
398 Env env(*this);
399 env.fund(XRP(30'000), gw);
401 AMM ammGw(env, gw, XRP(10'000), USD(10'000), ter(terNO_RIPPLE));
402 env.fund(XRP(30'000), alice);
403 env.trust(USD(30'000), alice);
404 env(pay(gw, alice, USD(30'000)));
405 AMM ammAlice(
406 env, alice, XRP(10'000), USD(10'000), ter(terNO_RIPPLE));
407 Account const gw1("gw1");
408 env.fund(XRP(30'000), gw1);
409 env(fclear(gw1, asfDefaultRipple));
410 env.trust(USD(30'000), gw1);
411 env(pay(gw, gw1, USD(30'000)));
412 auto const USD1 = gw1["USD"];
413 AMM ammGwGw1(env, gw, USD(10'000), USD1(10'000), ter(terNO_RIPPLE));
414 env.trust(USD1(30'000), alice);
415 env(pay(gw1, alice, USD1(30'000)));
416 AMM ammAlice1(
417 env, alice, USD(10'000), USD1(10'000), ter(terNO_RIPPLE));
418 }
419 }
420
421 void
423 {
424 testcase("Invalid Deposit");
425
426 using namespace jtx;
427
428 testAMM([&](AMM& ammAlice, Env& env) {
429 // Invalid flags
430 ammAlice.deposit(
431 alice,
432 1'000'000,
433 std::nullopt,
436
437 // Invalid options
445 invalidOptions = {
446 // flags, tokens, asset1In, asset2in, EPrice, tfee
447 {tfLPToken,
448 1'000,
449 std::nullopt,
450 USD(100),
451 std::nullopt,
452 std::nullopt},
453 {tfLPToken,
454 1'000,
455 XRP(100),
456 std::nullopt,
457 std::nullopt,
458 std::nullopt},
459 {tfLPToken,
460 1'000,
461 std::nullopt,
462 std::nullopt,
463 STAmount{USD, 1, -1},
464 std::nullopt},
465 {tfLPToken,
466 std::nullopt,
467 USD(100),
468 std::nullopt,
469 STAmount{USD, 1, -1},
470 std::nullopt},
471 {tfLPToken,
472 1'000,
473 XRP(100),
474 std::nullopt,
475 STAmount{USD, 1, -1},
476 std::nullopt},
477 {tfLPToken,
478 1'000,
479 std::nullopt,
480 std::nullopt,
481 std::nullopt,
482 1'000},
484 1'000,
485 std::nullopt,
486 std::nullopt,
487 std::nullopt,
488 std::nullopt},
490 std::nullopt,
491 std::nullopt,
492 USD(100),
493 std::nullopt,
494 std::nullopt},
496 std::nullopt,
497 std::nullopt,
498 std::nullopt,
499 STAmount{USD, 1, -1},
500 std::nullopt},
502 std::nullopt,
503 USD(100),
504 std::nullopt,
505 std::nullopt,
506 1'000},
507 {tfTwoAsset,
508 1'000,
509 std::nullopt,
510 std::nullopt,
511 std::nullopt,
512 std::nullopt},
513 {tfTwoAsset,
514 std::nullopt,
515 XRP(100),
516 USD(100),
517 STAmount{USD, 1, -1},
518 std::nullopt},
519 {tfTwoAsset,
520 std::nullopt,
521 XRP(100),
522 std::nullopt,
523 std::nullopt,
524 std::nullopt},
525 {tfTwoAsset,
526 std::nullopt,
527 XRP(100),
528 USD(100),
529 std::nullopt,
530 1'000},
531 {tfTwoAsset,
532 std::nullopt,
533 std::nullopt,
534 USD(100),
535 STAmount{USD, 1, -1},
536 std::nullopt},
538 1'000,
539 std::nullopt,
540 std::nullopt,
541 std::nullopt,
542 std::nullopt},
544 std::nullopt,
545 XRP(100),
546 USD(100),
547 std::nullopt,
548 std::nullopt},
550 std::nullopt,
551 XRP(100),
552 std::nullopt,
553 STAmount{USD, 1, -1},
554 std::nullopt},
556 1'000,
557 XRP(100),
558 std::nullopt,
559 std::nullopt,
560 1'000},
562 1'000,
563 std::nullopt,
564 std::nullopt,
565 std::nullopt,
566 std::nullopt},
568 1'000,
569 USD(100),
570 std::nullopt,
571 std::nullopt,
572 std::nullopt},
574 std::nullopt,
575 USD(100),
576 XRP(100),
577 std::nullopt,
578 std::nullopt},
580 std::nullopt,
581 XRP(100),
582 std::nullopt,
583 STAmount{USD, 1, -1},
584 1'000},
586 std::nullopt,
587 std::nullopt,
588 std::nullopt,
589 std::nullopt,
590 1'000},
592 1'000,
593 std::nullopt,
594 std::nullopt,
595 std::nullopt,
596 std::nullopt},
598 std::nullopt,
599 XRP(100),
600 USD(100),
601 STAmount{USD, 1, -1},
602 std::nullopt},
604 std::nullopt,
605 XRP(100),
606 USD(100),
607 STAmount{USD, 1, -1},
608 std::nullopt}};
609 for (auto const& it : invalidOptions)
610 {
611 ammAlice.deposit(
612 alice,
613 std::get<1>(it),
614 std::get<2>(it),
615 std::get<3>(it),
616 std::get<4>(it),
617 std::get<0>(it),
618 std::nullopt,
619 std::nullopt,
620 std::get<5>(it),
622 }
623
624 {
625 // bad preflight1
627 jv[jss::Account] = alice.human();
628 jv[jss::TransactionType] = jss::AMMDeposit;
629 jv[jss::Asset] =
631 jv[jss::Asset2] =
633 jv[jss::Fee] = "-1";
634 env(jv, ter(temBAD_FEE));
635 }
636
637 // Invalid tokens
638 ammAlice.deposit(
639 alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));
640 ammAlice.deposit(
641 alice,
642 IOUAmount{-1},
643 std::nullopt,
644 std::nullopt,
646
647 {
649 jv[jss::Account] = alice.human();
650 jv[jss::TransactionType] = jss::AMMDeposit;
651 jv[jss::Asset] =
653 jv[jss::Asset2] =
655 jv[jss::LPTokenOut] =
656 USD(100).value().getJson(JsonOptions::none);
657 jv[jss::Flags] = tfLPToken;
658 env(jv, ter(temBAD_AMM_TOKENS));
659 }
660
661 // Invalid trading fee
662 ammAlice.deposit(
663 carol,
664 std::nullopt,
665 XRP(200),
666 USD(200),
667 std::nullopt,
669 std::nullopt,
670 std::nullopt,
671 10'000,
672 ter(temBAD_FEE));
673
674 // Invalid tokens - bogus currency
675 {
676 auto const iss1 = Issue{Currency(0xabc), gw.id()};
677 auto const iss2 = Issue{Currency(0xdef), gw.id()};
678 ammAlice.deposit(
679 alice,
680 1'000,
681 std::nullopt,
682 std::nullopt,
683 std::nullopt,
684 std::nullopt,
685 {{iss1, iss2}},
686 std::nullopt,
687 std::nullopt,
688 ter(terNO_AMM));
689 }
690
691 // Depositing mismatched token, invalid Asset1In.issue
692 ammAlice.deposit(
693 alice,
694 GBP(100),
695 std::nullopt,
696 std::nullopt,
697 std::nullopt,
699
700 // Depositing mismatched token, invalid Asset2In.issue
701 ammAlice.deposit(
702 alice,
703 USD(100),
704 GBP(100),
705 std::nullopt,
706 std::nullopt,
708
709 // Depositing mismatched token, Asset1In.issue == Asset2In.issue
710 ammAlice.deposit(
711 alice,
712 USD(100),
713 USD(100),
714 std::nullopt,
715 std::nullopt,
717
718 // Invalid amount value
719 ammAlice.deposit(
720 alice,
721 USD(0),
722 std::nullopt,
723 std::nullopt,
724 std::nullopt,
726 ammAlice.deposit(
727 alice,
728 USD(-1'000),
729 std::nullopt,
730 std::nullopt,
731 std::nullopt,
733 ammAlice.deposit(
734 alice,
735 USD(10),
736 std::nullopt,
737 USD(-1),
738 std::nullopt,
740
741 // Bad currency
742 ammAlice.deposit(
743 alice,
744 BAD(100),
745 std::nullopt,
746 std::nullopt,
747 std::nullopt,
749
750 // Invalid Account
751 Account bad("bad");
752 env.memoize(bad);
753 ammAlice.deposit(
754 bad,
755 1'000'000,
756 std::nullopt,
757 std::nullopt,
758 std::nullopt,
759 std::nullopt,
760 std::nullopt,
761 seq(1),
762 std::nullopt,
764
765 // Invalid AMM
766 ammAlice.deposit(
767 alice,
768 1'000,
769 std::nullopt,
770 std::nullopt,
771 std::nullopt,
772 std::nullopt,
773 {{USD, GBP}},
774 std::nullopt,
775 std::nullopt,
776 ter(terNO_AMM));
777
778 // Single deposit: 100000 tokens worth of USD
779 // Amount to deposit exceeds Max
780 ammAlice.deposit(
781 carol,
782 100'000,
783 USD(200),
784 std::nullopt,
785 std::nullopt,
786 std::nullopt,
787 std::nullopt,
788 std::nullopt,
789 std::nullopt,
791
792 // Single deposit: 100000 tokens worth of XRP
793 // Amount to deposit exceeds Max
794 ammAlice.deposit(
795 carol,
796 100'000,
797 XRP(200),
798 std::nullopt,
799 std::nullopt,
800 std::nullopt,
801 std::nullopt,
802 std::nullopt,
803 std::nullopt,
805
806 // Deposit amount is invalid
807 // Calculated amount to deposit is 98,000,000
808 ammAlice.deposit(
809 alice,
810 USD(0),
811 std::nullopt,
812 STAmount{USD, 1, -1},
813 std::nullopt,
815 // Calculated amount is 0
816 ammAlice.deposit(
817 alice,
818 USD(0),
819 std::nullopt,
820 STAmount{USD, 2'000, -6},
821 std::nullopt,
823
824 // Tiny deposit
825 ammAlice.deposit(
826 carol,
827 IOUAmount{1, -4},
828 std::nullopt,
829 std::nullopt,
831 ammAlice.deposit(
832 carol,
833 STAmount{USD, 1, -12},
834 std::nullopt,
835 std::nullopt,
836 std::nullopt,
838
839 // Deposit non-empty AMM
840 ammAlice.deposit(
841 carol,
842 XRP(100),
843 USD(100),
844 std::nullopt,
847 });
848
849 // Invalid AMM
850 testAMM([&](AMM& ammAlice, Env& env) {
851 ammAlice.withdrawAll(alice);
852 ammAlice.deposit(
853 alice, 10'000, std::nullopt, std::nullopt, ter(terNO_AMM));
854 });
855
856 // Globally frozen asset
857 testAMM(
858 [&](AMM& ammAlice, Env& env) {
859 env(fset(gw, asfGlobalFreeze));
860 if (!features[featureAMMClawback])
861 // If the issuer set global freeze, the holder still can
862 // deposit the other non-frozen token when AMMClawback is
863 // not enabled.
864 ammAlice.deposit(carol, XRP(100));
865 else
866 // If the issuer set global freeze, the holder cannot
867 // deposit the other non-frozen token when AMMClawback is
868 // enabled.
869 ammAlice.deposit(
870 carol,
871 XRP(100),
872 std::nullopt,
873 std::nullopt,
874 std::nullopt,
875 ter(tecFROZEN));
876 ammAlice.deposit(
877 carol,
878 USD(100),
879 std::nullopt,
880 std::nullopt,
881 std::nullopt,
882 ter(tecFROZEN));
883 ammAlice.deposit(
884 carol,
885 1'000'000,
886 std::nullopt,
887 std::nullopt,
888 ter(tecFROZEN));
889 ammAlice.deposit(
890 carol,
891 XRP(100),
892 USD(100),
893 std::nullopt,
894 std::nullopt,
895 ter(tecFROZEN));
896 },
897 std::nullopt,
898 0,
899 std::nullopt,
900 {features});
901
902 // Individually frozen (AMM) account
903 testAMM(
904 [&](AMM& ammAlice, Env& env) {
905 env(trust(gw, carol["USD"](0), tfSetFreeze));
906 env.close();
907 if (!features[featureAMMClawback])
908 // Can deposit non-frozen token if AMMClawback is not
909 // enabled
910 ammAlice.deposit(carol, XRP(100));
911 else
912 // Cannot deposit non-frozen token if the other token is
913 // frozen when AMMClawback is enabled
914 ammAlice.deposit(
915 carol,
916 XRP(100),
917 std::nullopt,
918 std::nullopt,
919 std::nullopt,
920 ter(tecFROZEN));
921
922 ammAlice.deposit(
923 carol,
924 1'000'000,
925 std::nullopt,
926 std::nullopt,
927 ter(tecFROZEN));
928 ammAlice.deposit(
929 carol,
930 USD(100),
931 std::nullopt,
932 std::nullopt,
933 std::nullopt,
934 ter(tecFROZEN));
935 env(trust(gw, carol["USD"](0), tfClearFreeze));
936 // Individually frozen AMM
937 env(trust(
938 gw,
939 STAmount{
940 Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
941 tfSetFreeze));
942 env.close();
943 // Can deposit non-frozen token
944 ammAlice.deposit(carol, XRP(100));
945 ammAlice.deposit(
946 carol,
947 1'000'000,
948 std::nullopt,
949 std::nullopt,
950 ter(tecFROZEN));
951 ammAlice.deposit(
952 carol,
953 USD(100),
954 std::nullopt,
955 std::nullopt,
956 std::nullopt,
957 ter(tecFROZEN));
958 },
959 std::nullopt,
960 0,
961 std::nullopt,
962 {features});
963
964 // Individually frozen (AMM) account with IOU/IOU AMM
965 testAMM(
966 [&](AMM& ammAlice, Env& env) {
967 env(trust(gw, carol["USD"](0), tfSetFreeze));
968 env(trust(gw, carol["BTC"](0), tfSetFreeze));
969 env.close();
970 ammAlice.deposit(
971 carol,
972 1'000'000,
973 std::nullopt,
974 std::nullopt,
975 ter(tecFROZEN));
976 ammAlice.deposit(
977 carol,
978 USD(100),
979 std::nullopt,
980 std::nullopt,
981 std::nullopt,
982 ter(tecFROZEN));
983 env(trust(gw, carol["USD"](0), tfClearFreeze));
984 // Individually frozen AMM
985 env(trust(
986 gw,
987 STAmount{
988 Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
989 tfSetFreeze));
990 env.close();
991 // Cannot deposit non-frozen token
992 ammAlice.deposit(
993 carol,
994 1'000'000,
995 std::nullopt,
996 std::nullopt,
997 ter(tecFROZEN));
998 ammAlice.deposit(
999 carol,
1000 USD(100),
1001 BTC(0.01),
1002 std::nullopt,
1003 std::nullopt,
1004 ter(tecFROZEN));
1005 },
1006 {{USD(20'000), BTC(0.5)}});
1007
1008 // Deposit unauthorized token.
1009 {
1010 Env env(*this, features);
1011 env.fund(XRP(1000), gw, alice, bob);
1012 env(fset(gw, asfRequireAuth));
1013 env.close();
1014 env(trust(gw, alice["USD"](100)), txflags(tfSetfAuth));
1015 env(trust(alice, gw["USD"](20)));
1016 env.close();
1017 env(pay(gw, alice, gw["USD"](10)));
1018 env.close();
1019 env(trust(gw, bob["USD"](100)));
1020 env.close();
1021
1022 AMM amm(env, alice, XRP(10), gw["USD"](10), ter(tesSUCCESS));
1023 env.close();
1024
1025 if (features[featureAMMClawback])
1026 // if featureAMMClawback is enabled, bob can not deposit XRP
1027 // because he's not authorized to hold the paired token
1028 // gw["USD"].
1029 amm.deposit(
1030 bob,
1031 XRP(10),
1032 std::nullopt,
1033 std::nullopt,
1034 std::nullopt,
1035 ter(tecNO_AUTH));
1036 else
1037 amm.deposit(
1038 bob,
1039 XRP(10),
1040 std::nullopt,
1041 std::nullopt,
1042 std::nullopt,
1043 ter(tesSUCCESS));
1044 }
1045
1046 // Insufficient XRP balance
1047 testAMM([&](AMM& ammAlice, Env& env) {
1048 env.fund(XRP(1'000), bob);
1049 env.close();
1050 // Adds LPT trustline
1051 ammAlice.deposit(bob, XRP(10));
1052 ammAlice.deposit(
1053 bob,
1054 XRP(1'000),
1055 std::nullopt,
1056 std::nullopt,
1057 std::nullopt,
1059 });
1060
1061 // Insufficient USD balance
1062 testAMM([&](AMM& ammAlice, Env& env) {
1063 fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
1064 env.close();
1065 ammAlice.deposit(
1066 bob,
1067 USD(1'001),
1068 std::nullopt,
1069 std::nullopt,
1070 std::nullopt,
1072 });
1073
1074 // Insufficient USD balance by tokens
1075 testAMM([&](AMM& ammAlice, Env& env) {
1076 fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
1077 env.close();
1078 ammAlice.deposit(
1079 bob,
1080 10'000'000,
1081 std::nullopt,
1082 std::nullopt,
1083 std::nullopt,
1084 std::nullopt,
1085 std::nullopt,
1086 std::nullopt,
1087 std::nullopt,
1089 });
1090
1091 // Insufficient XRP balance by tokens
1092 testAMM([&](AMM& ammAlice, Env& env) {
1093 env.fund(XRP(1'000), bob);
1094 env.trust(USD(100'000), bob);
1095 env.close();
1096 env(pay(gw, bob, USD(90'000)));
1097 env.close();
1098 ammAlice.deposit(
1099 bob,
1100 10'000'000,
1101 std::nullopt,
1102 std::nullopt,
1103 std::nullopt,
1104 std::nullopt,
1105 std::nullopt,
1106 std::nullopt,
1107 std::nullopt,
1109 });
1110
1111 // Insufficient reserve, XRP/IOU
1112 {
1113 Env env(*this);
1114 auto const starting_xrp =
1115 reserve(env, 4) + env.current()->fees().base * 4;
1116 env.fund(XRP(10'000), gw);
1117 env.fund(XRP(10'000), alice);
1118 env.fund(starting_xrp, carol);
1119 env.trust(USD(2'000), alice);
1120 env.trust(USD(2'000), carol);
1121 env.close();
1122 env(pay(gw, alice, USD(2'000)));
1123 env(pay(gw, carol, USD(2'000)));
1124 env.close();
1125 env(offer(carol, XRP(100), USD(101)));
1126 env(offer(carol, XRP(100), USD(102)));
1127 AMM ammAlice(env, alice, XRP(1'000), USD(1'000));
1128 ammAlice.deposit(
1129 carol,
1130 XRP(100),
1131 std::nullopt,
1132 std::nullopt,
1133 std::nullopt,
1135
1136 env(offer(carol, XRP(100), USD(103)));
1137 ammAlice.deposit(
1138 carol,
1139 USD(100),
1140 std::nullopt,
1141 std::nullopt,
1142 std::nullopt,
1144 }
1145
1146 // Insufficient reserve, IOU/IOU
1147 {
1148 Env env(*this);
1149 auto const starting_xrp =
1150 reserve(env, 4) + env.current()->fees().base * 4;
1151 env.fund(XRP(10'000), gw);
1152 env.fund(XRP(10'000), alice);
1153 env.fund(starting_xrp, carol);
1154 env.trust(USD(2'000), alice);
1155 env.trust(EUR(2'000), alice);
1156 env.trust(USD(2'000), carol);
1157 env.trust(EUR(2'000), carol);
1158 env.close();
1159 env(pay(gw, alice, USD(2'000)));
1160 env(pay(gw, alice, EUR(2'000)));
1161 env(pay(gw, carol, USD(2'000)));
1162 env(pay(gw, carol, EUR(2'000)));
1163 env.close();
1164 env(offer(carol, XRP(100), USD(101)));
1165 env(offer(carol, XRP(100), USD(102)));
1166 AMM ammAlice(env, alice, XRP(1'000), USD(1'000));
1167 ammAlice.deposit(
1168 carol,
1169 XRP(100),
1170 std::nullopt,
1171 std::nullopt,
1172 std::nullopt,
1174 }
1175
1176 // Invalid min
1177 testAMM([&](AMM& ammAlice, Env& env) {
1178 // min tokens can't be <= zero
1179 ammAlice.deposit(
1181 ammAlice.deposit(
1183 ammAlice.deposit(
1184 carol,
1185 0,
1186 XRP(100),
1187 USD(100),
1188 std::nullopt,
1189 tfTwoAsset,
1190 std::nullopt,
1191 std::nullopt,
1192 std::nullopt,
1194 // min amounts can't be <= zero
1195 ammAlice.deposit(
1196 carol,
1197 1'000,
1198 XRP(0),
1199 USD(100),
1200 std::nullopt,
1201 tfTwoAsset,
1202 std::nullopt,
1203 std::nullopt,
1204 std::nullopt,
1206 ammAlice.deposit(
1207 carol,
1208 1'000,
1209 XRP(100),
1210 USD(-1),
1211 std::nullopt,
1212 tfTwoAsset,
1213 std::nullopt,
1214 std::nullopt,
1215 std::nullopt,
1217 // min amount bad currency
1218 ammAlice.deposit(
1219 carol,
1220 1'000,
1221 XRP(100),
1222 BAD(100),
1223 std::nullopt,
1224 tfTwoAsset,
1225 std::nullopt,
1226 std::nullopt,
1227 std::nullopt,
1229 // min amount bad token pair
1230 ammAlice.deposit(
1231 carol,
1232 1'000,
1233 XRP(100),
1234 XRP(100),
1235 std::nullopt,
1236 tfTwoAsset,
1237 std::nullopt,
1238 std::nullopt,
1239 std::nullopt,
1241 ammAlice.deposit(
1242 carol,
1243 1'000,
1244 XRP(100),
1245 GBP(100),
1246 std::nullopt,
1247 tfTwoAsset,
1248 std::nullopt,
1249 std::nullopt,
1250 std::nullopt,
1252 });
1253
1254 // Min deposit
1255 testAMM([&](AMM& ammAlice, Env& env) {
1256 // Equal deposit by tokens
1257 ammAlice.deposit(
1258 carol,
1259 1'000'000,
1260 XRP(1'000),
1261 USD(1'001),
1262 std::nullopt,
1263 tfLPToken,
1264 std::nullopt,
1265 std::nullopt,
1266 std::nullopt,
1268 ammAlice.deposit(
1269 carol,
1270 1'000'000,
1271 XRP(1'001),
1272 USD(1'000),
1273 std::nullopt,
1274 tfLPToken,
1275 std::nullopt,
1276 std::nullopt,
1277 std::nullopt,
1279 // Equal deposit by asset
1280 ammAlice.deposit(
1281 carol,
1282 100'001,
1283 XRP(100),
1284 USD(100),
1285 std::nullopt,
1286 tfTwoAsset,
1287 std::nullopt,
1288 std::nullopt,
1289 std::nullopt,
1291 // Single deposit by asset
1292 ammAlice.deposit(
1293 carol,
1294 488'090,
1295 XRP(1'000),
1296 std::nullopt,
1297 std::nullopt,
1299 std::nullopt,
1300 std::nullopt,
1301 std::nullopt,
1303 });
1304 }
1305
1306 void
1308 {
1309 testcase("Deposit");
1310
1311 using namespace jtx;
1312
1313 // Equal deposit: 1000000 tokens, 10% of the current pool
1314 testAMM([&](AMM& ammAlice, Env& env) {
1315 ammAlice.deposit(carol, 1'000'000);
1316 BEAST_EXPECT(ammAlice.expectBalances(
1317 XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1318 // 30,000 less deposited 1,000
1319 BEAST_EXPECT(expectLine(env, carol, USD(29'000)));
1320 // 30,000 less deposited 1,000 and 10 drops tx fee
1321 BEAST_EXPECT(
1322 expectLedgerEntryRoot(env, carol, XRPAmount{28'999'999'990}));
1323 });
1324
1325 // equal asset deposit: unit test to exercise the rounding-down of
1326 // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations
1327 // The LPTokens need to have 16 significant digits and a fractional part
1328 for (const Number deltaLPTokens :
1329 {Number{UINT64_C(100000'0000000009), -10},
1330 Number{UINT64_C(100000'0000000001), -10}})
1331 {
1332 testAMM([&](AMM& ammAlice, Env& env) {
1333 // initial LPToken balance
1334 IOUAmount const initLPToken = ammAlice.getLPTokensBalance();
1335 const IOUAmount newLPTokens{
1336 deltaLPTokens.mantissa(), deltaLPTokens.exponent()};
1337
1338 // carol performs a two-asset deposit
1339 ammAlice.deposit(
1340 DepositArg{.account = carol, .tokens = newLPTokens});
1341
1342 IOUAmount const finalLPToken = ammAlice.getLPTokensBalance();
1343
1344 // Change in behavior due to rounding down of LPTokens:
1345 // there is a decrease in the observed return of LPTokens --
1346 // Inputs Number{UINT64_C(100000'0000000001), -10} and
1347 // Number{UINT64_C(100000'0000000009), -10} are both rounded
1348 // down to 1e5
1349 BEAST_EXPECT((finalLPToken - initLPToken == IOUAmount{1, 5}));
1350 BEAST_EXPECT(finalLPToken - initLPToken < deltaLPTokens);
1351
1352 // fraction of newLPTokens/(existing LPToken balance). The
1353 // existing LPToken balance is 1e7
1354 const Number fr = deltaLPTokens / 1e7;
1355
1356 // The below equations are based on Equation 1, 2 from XLS-30d
1357 // specification, Section: 2.3.1.2
1358 const Number deltaXRP = fr * 1e10;
1359 const Number deltaUSD = fr * 1e4;
1360
1361 const STAmount depositUSD =
1362 STAmount{USD, deltaUSD.mantissa(), deltaUSD.exponent()};
1363
1364 const STAmount depositXRP =
1365 STAmount{XRP, deltaXRP.mantissa(), deltaXRP.exponent()};
1366
1367 // initial LPTokens (1e7) + newLPTokens
1368 BEAST_EXPECT(ammAlice.expectBalances(
1369 XRP(10'000) + depositXRP,
1370 USD(10'000) + depositUSD,
1371 IOUAmount{1, 7} + newLPTokens));
1372
1373 // 30,000 less deposited depositUSD
1374 BEAST_EXPECT(expectLine(env, carol, USD(30'000) - depositUSD));
1375 // 30,000 less deposited depositXRP and 10 drops tx fee
1376 BEAST_EXPECT(expectLedgerEntryRoot(
1377 env, carol, XRP(30'000) - depositXRP - txfee(env, 1)));
1378 });
1379 }
1380
1381 // Equal limit deposit: deposit USD100 and XRP proportionally
1382 // to the pool composition not to exceed 100XRP. If the amount
1383 // exceeds 100XRP then deposit 100XRP and USD proportionally
1384 // to the pool composition not to exceed 100USD. Fail if exceeded.
1385 // Deposit 100USD/100XRP
1386 testAMM([&](AMM& ammAlice, Env&) {
1387 ammAlice.deposit(carol, USD(100), XRP(100));
1388 BEAST_EXPECT(ammAlice.expectBalances(
1389 XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
1390 });
1391
1392 // Equal limit deposit.
1393 // Try to deposit 200USD/100XRP. Is truncated to 100USD/100XRP.
1394 testAMM([&](AMM& ammAlice, Env&) {
1395 ammAlice.deposit(carol, USD(200), XRP(100));
1396 BEAST_EXPECT(ammAlice.expectBalances(
1397 XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
1398 });
1399 // Try to deposit 100USD/200XRP. Is truncated to 100USD/100XRP.
1400 testAMM([&](AMM& ammAlice, Env&) {
1401 ammAlice.deposit(carol, USD(100), XRP(200));
1402 BEAST_EXPECT(ammAlice.expectBalances(
1403 XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
1404 });
1405
1406 // Single deposit: 1000 USD
1407 testAMM([&](AMM& ammAlice, Env&) {
1408 ammAlice.deposit(carol, USD(1'000));
1409 BEAST_EXPECT(ammAlice.expectBalances(
1410 XRP(10'000),
1411 STAmount{USD, UINT64_C(10'999'99999999999), -11},
1412 IOUAmount{10'488'088'48170151, -8}));
1413 });
1414
1415 // Single deposit: 1000 XRP
1416 testAMM([&](AMM& ammAlice, Env&) {
1417 ammAlice.deposit(carol, XRP(1'000));
1418 BEAST_EXPECT(ammAlice.expectBalances(
1419 XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8}));
1420 });
1421
1422 // Single deposit: 100000 tokens worth of USD
1423 testAMM([&](AMM& ammAlice, Env&) {
1424 ammAlice.deposit(carol, 100000, USD(205));
1425 BEAST_EXPECT(ammAlice.expectBalances(
1426 XRP(10'000), USD(10'201), IOUAmount{10'100'000, 0}));
1427 });
1428
1429 // Single deposit: 100000 tokens worth of XRP
1430 testAMM([&](AMM& ammAlice, Env&) {
1431 ammAlice.deposit(carol, 100'000, XRP(205));
1432 BEAST_EXPECT(ammAlice.expectBalances(
1433 XRP(10'201), USD(10'000), IOUAmount{10'100'000, 0}));
1434 });
1435
1436 // Single deposit with EP not exceeding specified:
1437 // 100USD with EP not to exceed 0.1 (AssetIn/TokensOut)
1438 testAMM([&](AMM& ammAlice, Env&) {
1439 ammAlice.deposit(
1440 carol, USD(1'000), std::nullopt, STAmount{USD, 1, -1});
1441 BEAST_EXPECT(ammAlice.expectBalances(
1442 XRP(10'000),
1443 STAmount{USD, UINT64_C(10'999'99999999999), -11},
1444 IOUAmount{10'488'088'48170151, -8}));
1445 });
1446
1447 // Single deposit with EP not exceeding specified:
1448 // 100USD with EP not to exceed 0.002004 (AssetIn/TokensOut)
1449 testAMM([&](AMM& ammAlice, Env&) {
1450 ammAlice.deposit(
1451 carol, USD(100), std::nullopt, STAmount{USD, 2004, -6});
1452 BEAST_EXPECT(ammAlice.expectBalances(
1453 XRP(10'000),
1454 STAmount{USD, 10'080'16, -2},
1455 IOUAmount{10'040'000, 0}));
1456 });
1457
1458 // Single deposit with EP not exceeding specified:
1459 // 0USD with EP not to exceed 0.002004 (AssetIn/TokensOut)
1460 testAMM([&](AMM& ammAlice, Env&) {
1461 ammAlice.deposit(
1462 carol, USD(0), std::nullopt, STAmount{USD, 2004, -6});
1463 BEAST_EXPECT(ammAlice.expectBalances(
1464 XRP(10'000),
1465 STAmount{USD, 10'080'16, -2},
1466 IOUAmount{10'040'000, 0}));
1467 });
1468
1469 // IOU to IOU + transfer fee
1470 {
1471 Env env{*this};
1472 fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
1473 env(rate(gw, 1.25));
1474 env.close();
1475 AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
1476 BEAST_EXPECT(ammAlice.expectBalances(
1477 USD(20'000), BTC(0.5), IOUAmount{100, 0}));
1478 BEAST_EXPECT(expectLine(env, alice, USD(0)));
1479 BEAST_EXPECT(expectLine(env, alice, BTC(0)));
1480 fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct);
1481 // no transfer fee on deposit
1482 ammAlice.deposit(carol, 10);
1483 BEAST_EXPECT(ammAlice.expectBalances(
1484 USD(22'000), BTC(0.55), IOUAmount{110, 0}));
1485 BEAST_EXPECT(expectLine(env, carol, USD(0)));
1486 BEAST_EXPECT(expectLine(env, carol, BTC(0)));
1487 }
1488
1489 // Tiny deposits
1490 testAMM([&](AMM& ammAlice, Env&) {
1491 ammAlice.deposit(carol, IOUAmount{1, -3});
1492 BEAST_EXPECT(ammAlice.expectBalances(
1493 XRPAmount{10'000'000'001},
1494 STAmount{USD, UINT64_C(10'000'000001), -6},
1495 IOUAmount{10'000'000'001, -3}));
1496 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1, -3}));
1497 });
1498 testAMM([&](AMM& ammAlice, Env&) {
1499 ammAlice.deposit(carol, XRPAmount{1});
1500 BEAST_EXPECT(ammAlice.expectBalances(
1501 XRPAmount{10'000'000'001},
1502 USD(10'000),
1503 IOUAmount{1'000'000'000049999, -8}));
1504 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{49999, -8}));
1505 });
1506 testAMM([&](AMM& ammAlice, Env&) {
1507 ammAlice.deposit(carol, STAmount{USD, 1, -10});
1508 BEAST_EXPECT(ammAlice.expectBalances(
1509 XRP(10'000),
1510 STAmount{USD, UINT64_C(10'000'00000000008), -11},
1511 IOUAmount{10'000'000'00000004, -8}));
1512 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4, -8}));
1513 });
1514
1515 // Issuer create/deposit
1516 {
1517 Env env(*this);
1518 env.fund(XRP(30000), gw);
1519 AMM ammGw(env, gw, XRP(10'000), USD(10'000));
1520 BEAST_EXPECT(
1521 ammGw.expectBalances(XRP(10'000), USD(10'000), ammGw.tokens()));
1522 ammGw.deposit(gw, 1'000'000);
1523 BEAST_EXPECT(ammGw.expectBalances(
1524 XRP(11'000), USD(11'000), IOUAmount{11'000'000}));
1525 ammGw.deposit(gw, USD(1'000));
1526 BEAST_EXPECT(ammGw.expectBalances(
1527 XRP(11'000),
1528 STAmount{USD, UINT64_C(11'999'99999999998), -11},
1529 IOUAmount{11'489'125'29307605, -8}));
1530 }
1531
1532 // Issuer deposit
1533 testAMM([&](AMM& ammAlice, Env& env) {
1534 ammAlice.deposit(gw, 1'000'000);
1535 BEAST_EXPECT(ammAlice.expectBalances(
1536 XRP(11'000), USD(11'000), IOUAmount{11'000'000}));
1537 ammAlice.deposit(gw, USD(1'000));
1538 BEAST_EXPECT(ammAlice.expectBalances(
1539 XRP(11'000),
1540 STAmount{USD, UINT64_C(11'999'99999999998), -11},
1541 IOUAmount{11'489'125'29307605, -8}));
1542 });
1543
1544 // Min deposit
1545 testAMM([&](AMM& ammAlice, Env& env) {
1546 // Equal deposit by tokens
1547 ammAlice.deposit(
1548 carol,
1549 1'000'000,
1550 XRP(1'000),
1551 USD(1'000),
1552 std::nullopt,
1553 tfLPToken,
1554 std::nullopt,
1555 std::nullopt);
1556 BEAST_EXPECT(ammAlice.expectBalances(
1557 XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1558 });
1559 testAMM([&](AMM& ammAlice, Env& env) {
1560 // Equal deposit by asset
1561 ammAlice.deposit(
1562 carol,
1563 1'000'000,
1564 XRP(1'000),
1565 USD(1'000),
1566 std::nullopt,
1567 tfTwoAsset,
1568 std::nullopt,
1569 std::nullopt);
1570 BEAST_EXPECT(ammAlice.expectBalances(
1571 XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1572 });
1573 testAMM([&](AMM& ammAlice, Env& env) {
1574 // Single deposit by asset
1575 ammAlice.deposit(
1576 carol,
1577 488'088,
1578 XRP(1'000),
1579 std::nullopt,
1580 std::nullopt,
1582 std::nullopt,
1583 std::nullopt);
1584 BEAST_EXPECT(ammAlice.expectBalances(
1585 XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8}));
1586 });
1587 testAMM([&](AMM& ammAlice, Env& env) {
1588 // Single deposit by asset
1589 ammAlice.deposit(
1590 carol,
1591 488'088,
1592 USD(1'000),
1593 std::nullopt,
1594 std::nullopt,
1596 std::nullopt,
1597 std::nullopt);
1598 BEAST_EXPECT(ammAlice.expectBalances(
1599 XRP(10'000),
1600 STAmount{USD, UINT64_C(10'999'99999999999), -11},
1601 IOUAmount{10'488'088'48170151, -8}));
1602 });
1603 }
1604
1605 void
1607 {
1608 testcase("Invalid Withdraw");
1609
1610 using namespace jtx;
1611
1612 testAMM(
1613 [&](AMM& ammAlice, Env& env) {
1614 WithdrawArg args{
1615 .asset1Out = XRP(100),
1616 .err = ter(tecAMM_BALANCE),
1617 };
1618 ammAlice.withdraw(args);
1619 },
1620 {{XRP(99), USD(99)}});
1621
1622 testAMM(
1623 [&](AMM& ammAlice, Env& env) {
1624 WithdrawArg args{
1625 .asset1Out = USD(100),
1626 .err = ter(tecAMM_BALANCE),
1627 };
1628 ammAlice.withdraw(args);
1629 },
1630 {{XRP(99), USD(99)}});
1631
1632 {
1633 Env env{*this};
1634 env.fund(XRP(30'000), gw, alice, bob);
1635 env.close();
1636 env(fset(gw, asfRequireAuth));
1637 env(trust(alice, gw["USD"](30'000), 0));
1638 env(trust(gw, alice["USD"](0), tfSetfAuth));
1639 // Bob trusts Gateway to owe him USD...
1640 env(trust(bob, gw["USD"](30'000), 0));
1641 // ...but Gateway does not authorize Bob to hold its USD.
1642 env.close();
1643 env(pay(gw, alice, USD(10'000)));
1644 env.close();
1645 AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
1646 WithdrawArg args{
1647 .account = bob,
1648 .asset1Out = USD(100),
1649 .err = ter(tecNO_AUTH),
1650 };
1651 ammAlice.withdraw(args);
1652 }
1653
1654 testAMM([&](AMM& ammAlice, Env& env) {
1655 // Invalid flags
1656 ammAlice.withdraw(
1657 alice,
1658 1'000'000,
1659 std::nullopt,
1660 std::nullopt,
1661 std::nullopt,
1662 tfBurnable,
1663 std::nullopt,
1664 std::nullopt,
1666 ammAlice.withdraw(
1667 alice,
1668 1'000'000,
1669 std::nullopt,
1670 std::nullopt,
1671 std::nullopt,
1673 std::nullopt,
1674 std::nullopt,
1676
1677 // Invalid options
1684 NotTEC>>
1685 invalidOptions = {
1686 // tokens, asset1Out, asset2Out, EPrice, flags, ter
1687 {std::nullopt,
1688 std::nullopt,
1689 std::nullopt,
1690 std::nullopt,
1691 std::nullopt,
1692 temMALFORMED},
1693 {std::nullopt,
1694 std::nullopt,
1695 std::nullopt,
1696 std::nullopt,
1698 temMALFORMED},
1699 {1'000,
1700 std::nullopt,
1701 std::nullopt,
1702 std::nullopt,
1704 temMALFORMED},
1705 {std::nullopt,
1706 USD(0),
1707 XRP(100),
1708 std::nullopt,
1710 temMALFORMED},
1711 {std::nullopt,
1712 std::nullopt,
1713 USD(100),
1714 std::nullopt,
1716 temMALFORMED},
1717 {std::nullopt,
1718 std::nullopt,
1719 std::nullopt,
1720 std::nullopt,
1722 temMALFORMED},
1723 {std::nullopt,
1724 USD(100),
1725 std::nullopt,
1726 std::nullopt,
1728 temMALFORMED},
1729 {std::nullopt,
1730 std::nullopt,
1731 std::nullopt,
1732 std::nullopt,
1734 temMALFORMED},
1735 {1'000,
1736 std::nullopt,
1737 USD(100),
1738 std::nullopt,
1739 std::nullopt,
1740 temMALFORMED},
1741 {std::nullopt,
1742 std::nullopt,
1743 std::nullopt,
1744 IOUAmount{250, 0},
1746 temMALFORMED},
1747 {1'000,
1748 std::nullopt,
1749 std::nullopt,
1750 IOUAmount{250, 0},
1751 std::nullopt,
1752 temMALFORMED},
1753 {std::nullopt,
1754 std::nullopt,
1755 USD(100),
1756 IOUAmount{250, 0},
1757 std::nullopt,
1758 temMALFORMED},
1759 {std::nullopt,
1760 XRP(100),
1761 USD(100),
1762 IOUAmount{250, 0},
1763 std::nullopt,
1764 temMALFORMED},
1765 {1'000,
1766 XRP(100),
1767 USD(100),
1768 std::nullopt,
1769 std::nullopt,
1770 temMALFORMED},
1771 {std::nullopt,
1772 XRP(100),
1773 USD(100),
1774 std::nullopt,
1776 temMALFORMED}};
1777 for (auto const& it : invalidOptions)
1778 {
1779 ammAlice.withdraw(
1780 alice,
1781 std::get<0>(it),
1782 std::get<1>(it),
1783 std::get<2>(it),
1784 std::get<3>(it),
1785 std::get<4>(it),
1786 std::nullopt,
1787 std::nullopt,
1788 ter(std::get<5>(it)));
1789 }
1790
1791 // Invalid tokens
1792 ammAlice.withdraw(
1793 alice, 0, std::nullopt, std::nullopt, ter(temBAD_AMM_TOKENS));
1794 ammAlice.withdraw(
1795 alice,
1796 IOUAmount{-1},
1797 std::nullopt,
1798 std::nullopt,
1800
1801 // Mismatched token, invalid Asset1Out issue
1802 ammAlice.withdraw(
1803 alice,
1804 GBP(100),
1805 std::nullopt,
1806 std::nullopt,
1808
1809 // Mismatched token, invalid Asset2Out issue
1810 ammAlice.withdraw(
1811 alice,
1812 USD(100),
1813 GBP(100),
1814 std::nullopt,
1816
1817 // Mismatched token, Asset1Out.issue == Asset2Out.issue
1818 ammAlice.withdraw(
1819 alice,
1820 USD(100),
1821 USD(100),
1822 std::nullopt,
1824
1825 // Invalid amount value
1826 ammAlice.withdraw(
1827 alice, USD(0), std::nullopt, std::nullopt, ter(temBAD_AMOUNT));
1828 ammAlice.withdraw(
1829 alice,
1830 USD(-100),
1831 std::nullopt,
1832 std::nullopt,
1834 ammAlice.withdraw(
1835 alice,
1836 USD(10),
1837 std::nullopt,
1838 IOUAmount{-1},
1840
1841 // Invalid amount/token value, withdraw all tokens from one side
1842 // of the pool.
1843 ammAlice.withdraw(
1844 alice,
1845 USD(10'000),
1846 std::nullopt,
1847 std::nullopt,
1849 ammAlice.withdraw(
1850 alice,
1851 XRP(10'000),
1852 std::nullopt,
1853 std::nullopt,
1855 ammAlice.withdraw(
1856 alice,
1857 std::nullopt,
1858 USD(0),
1859 std::nullopt,
1860 std::nullopt,
1862 std::nullopt,
1863 std::nullopt,
1865
1866 // Bad currency
1867 ammAlice.withdraw(
1868 alice,
1869 BAD(100),
1870 std::nullopt,
1871 std::nullopt,
1873
1874 // Invalid Account
1875 Account bad("bad");
1876 env.memoize(bad);
1877 ammAlice.withdraw(
1878 bad,
1879 1'000'000,
1880 std::nullopt,
1881 std::nullopt,
1882 std::nullopt,
1883 std::nullopt,
1884 std::nullopt,
1885 seq(1),
1887
1888 // Invalid AMM
1889 ammAlice.withdraw(
1890 alice,
1891 1'000,
1892 std::nullopt,
1893 std::nullopt,
1894 std::nullopt,
1895 std::nullopt,
1896 {{USD, GBP}},
1897 std::nullopt,
1898 ter(terNO_AMM));
1899
1900 // Carol is not a Liquidity Provider
1901 ammAlice.withdraw(
1902 carol, 10'000, std::nullopt, std::nullopt, ter(tecAMM_BALANCE));
1903
1904 // Withdraw entire one side of the pool.
1905 // Equal withdraw but due to XRP precision limit,
1906 // this results in full withdraw of XRP pool only,
1907 // while leaving a tiny amount in USD pool.
1908 ammAlice.withdraw(
1909 alice,
1910 IOUAmount{9'999'999'9999, -4},
1911 std::nullopt,
1912 std::nullopt,
1914 // Withdrawing from one side.
1915 // XRP by tokens
1916 ammAlice.withdraw(
1917 alice,
1918 IOUAmount(9'999'999'9999, -4),
1919 XRP(0),
1920 std::nullopt,
1922 // USD by tokens
1923 ammAlice.withdraw(
1924 alice,
1925 IOUAmount(9'999'999'9, -1),
1926 USD(0),
1927 std::nullopt,
1929 // XRP
1930 ammAlice.withdraw(
1931 alice,
1932 XRP(10'000),
1933 std::nullopt,
1934 std::nullopt,
1936 // USD
1937 ammAlice.withdraw(
1938 alice,
1939 STAmount{USD, UINT64_C(9'999'9999999999999), -13},
1940 std::nullopt,
1941 std::nullopt,
1943 });
1944
1945 // Invalid AMM
1946 testAMM([&](AMM& ammAlice, Env& env) {
1947 ammAlice.withdrawAll(alice);
1948 ammAlice.withdraw(
1949 alice, 10'000, std::nullopt, std::nullopt, ter(terNO_AMM));
1950 });
1951
1952 // Globally frozen asset
1953 testAMM([&](AMM& ammAlice, Env& env) {
1954 env(fset(gw, asfGlobalFreeze));
1955 env.close();
1956 // Can withdraw non-frozen token
1957 ammAlice.withdraw(alice, XRP(100));
1958 ammAlice.withdraw(
1959 alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
1960 ammAlice.withdraw(
1961 alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
1962 });
1963
1964 // Individually frozen (AMM) account
1965 testAMM([&](AMM& ammAlice, Env& env) {
1966 env(trust(gw, alice["USD"](0), tfSetFreeze));
1967 env.close();
1968 // Can withdraw non-frozen token
1969 ammAlice.withdraw(alice, XRP(100));
1970 ammAlice.withdraw(
1971 alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
1972 ammAlice.withdraw(
1973 alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
1974 env(trust(gw, alice["USD"](0), tfClearFreeze));
1975 // Individually frozen AMM
1976 env(trust(
1977 gw,
1978 STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
1979 tfSetFreeze));
1980 // Can withdraw non-frozen token
1981 ammAlice.withdraw(alice, XRP(100));
1982 ammAlice.withdraw(
1983 alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
1984 ammAlice.withdraw(
1985 alice, USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
1986 });
1987
1988 // Carol withdraws more than she owns
1989 testAMM([&](AMM& ammAlice, Env&) {
1990 // Single deposit of 100000 worth of tokens,
1991 // which is 10% of the pool. Carol is LP now.
1992 ammAlice.deposit(carol, 1'000'000);
1993 BEAST_EXPECT(ammAlice.expectBalances(
1994 XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1995
1996 ammAlice.withdraw(
1997 carol,
1998 2'000'000,
1999 std::nullopt,
2000 std::nullopt,
2002 BEAST_EXPECT(ammAlice.expectBalances(
2003 XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
2004 });
2005
2006 // Withdraw with EPrice limit. Fails to withdraw, calculated tokens
2007 // to withdraw are 0.
2008 testAMM([&](AMM& ammAlice, Env&) {
2009 ammAlice.deposit(carol, 1'000'000);
2010 ammAlice.withdraw(
2011 carol,
2012 USD(100),
2013 std::nullopt,
2014 IOUAmount{500, 0},
2016 });
2017
2018 // Withdraw with EPrice limit. Fails to withdraw, calculated tokens
2019 // to withdraw are greater than the LP shares.
2020 testAMM([&](AMM& ammAlice, Env&) {
2021 ammAlice.deposit(carol, 1'000'000);
2022 ammAlice.withdraw(
2023 carol,
2024 USD(100),
2025 std::nullopt,
2026 IOUAmount{600, 0},
2028 });
2029
2030 // Withdraw with EPrice limit. Fails to withdraw, amount1
2031 // to withdraw is less than 1700USD.
2032 testAMM([&](AMM& ammAlice, Env&) {
2033 ammAlice.deposit(carol, 1'000'000);
2034 ammAlice.withdraw(
2035 carol,
2036 USD(1'700),
2037 std::nullopt,
2038 IOUAmount{520, 0},
2040 });
2041
2042 // Deposit/Withdraw the same amount with the trading fee
2043 testAMM(
2044 [&](AMM& ammAlice, Env&) {
2045 ammAlice.deposit(carol, USD(1'000));
2046 ammAlice.withdraw(
2047 carol,
2048 USD(1'000),
2049 std::nullopt,
2050 std::nullopt,
2052 },
2053 std::nullopt,
2054 1'000);
2055 testAMM(
2056 [&](AMM& ammAlice, Env&) {
2057 ammAlice.deposit(carol, XRP(1'000));
2058 ammAlice.withdraw(
2059 carol,
2060 XRP(1'000),
2061 std::nullopt,
2062 std::nullopt,
2064 },
2065 std::nullopt,
2066 1'000);
2067
2068 // Deposit/Withdraw the same amount fails due to the tokens adjustment
2069 testAMM([&](AMM& ammAlice, Env&) {
2070 ammAlice.deposit(carol, STAmount{USD, 1, -6});
2071 ammAlice.withdraw(
2072 carol,
2073 STAmount{USD, 1, -6},
2074 std::nullopt,
2075 std::nullopt,
2077 });
2078
2079 // Withdraw close to one side of the pool. Account's LP tokens
2080 // are rounded to all LP tokens.
2081 testAMM([&](AMM& ammAlice, Env&) {
2082 ammAlice.withdraw(
2083 alice,
2084 STAmount{USD, UINT64_C(9'999'999999999999), -12},
2085 std::nullopt,
2086 std::nullopt,
2088 });
2089
2090 // Tiny withdraw
2091 testAMM([&](AMM& ammAlice, Env&) {
2092 // XRP amount to withdraw is 0
2093 ammAlice.withdraw(
2094 alice,
2095 IOUAmount{1, -5},
2096 std::nullopt,
2097 std::nullopt,
2099 // Calculated tokens to withdraw are 0
2100 ammAlice.withdraw(
2101 alice,
2102 std::nullopt,
2103 STAmount{USD, 1, -11},
2104 std::nullopt,
2106 ammAlice.deposit(carol, STAmount{USD, 1, -10});
2107 ammAlice.withdraw(
2108 carol,
2109 std::nullopt,
2110 STAmount{USD, 1, -9},
2111 std::nullopt,
2113 ammAlice.withdraw(
2114 carol,
2115 std::nullopt,
2116 XRPAmount{1},
2117 std::nullopt,
2119 });
2120 }
2121
2122 void
2124 {
2125 testcase("Withdraw");
2126
2127 using namespace jtx;
2128
2129 // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current
2130 // pool
2131 testAMM([&](AMM& ammAlice, Env& env) {
2132 // Single deposit of 100000 worth of tokens,
2133 // which is 10% of the pool. Carol is LP now.
2134 ammAlice.deposit(carol, 1'000'000);
2135 BEAST_EXPECT(ammAlice.expectBalances(
2136 XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
2137 BEAST_EXPECT(
2138 ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
2139 // 30,000 less deposited 1,000
2140 BEAST_EXPECT(expectLine(env, carol, USD(29'000)));
2141 // 30,000 less deposited 1,000 and 10 drops tx fee
2142 BEAST_EXPECT(
2143 expectLedgerEntryRoot(env, carol, XRPAmount{28'999'999'990}));
2144
2145 // Carol withdraws all tokens
2146 ammAlice.withdraw(carol, 1'000'000);
2147 BEAST_EXPECT(
2149 BEAST_EXPECT(expectLine(env, carol, USD(30'000)));
2150 BEAST_EXPECT(
2151 expectLedgerEntryRoot(env, carol, XRPAmount{29'999'999'980}));
2152 });
2153
2154 // Equal withdrawal by tokens 1000000, 10%
2155 // of the current pool
2156 testAMM([&](AMM& ammAlice, Env&) {
2157 ammAlice.withdraw(alice, 1'000'000);
2158 BEAST_EXPECT(ammAlice.expectBalances(
2159 XRP(9'000), USD(9'000), IOUAmount{9'000'000, 0}));
2160 });
2161
2162 // Equal withdrawal with a limit. Withdraw XRP200.
2163 // If proportional withdraw of USD is less than 100
2164 // then withdraw that amount, otherwise withdraw USD100
2165 // and proportionally withdraw XRP. It's the latter
2166 // in this case - XRP100/USD100.
2167 testAMM([&](AMM& ammAlice, Env&) {
2168 ammAlice.withdraw(alice, XRP(200), USD(100));
2169 BEAST_EXPECT(ammAlice.expectBalances(
2170 XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0}));
2171 });
2172
2173 // Equal withdrawal with a limit. XRP100/USD100.
2174 testAMM([&](AMM& ammAlice, Env&) {
2175 ammAlice.withdraw(alice, XRP(100), USD(200));
2176 BEAST_EXPECT(ammAlice.expectBalances(
2177 XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0}));
2178 });
2179
2180 // Single withdrawal by amount XRP1000
2181 testAMM([&](AMM& ammAlice, Env&) {
2182 ammAlice.withdraw(alice, XRP(1'000));
2183 BEAST_EXPECT(ammAlice.expectBalances(
2184 XRP(9'000), USD(10'000), IOUAmount{9'486'832'98050514, -8}));
2185 });
2186
2187 // Single withdrawal by tokens 10000.
2188 testAMM([&](AMM& ammAlice, Env&) {
2189 ammAlice.withdraw(alice, 10'000, USD(0));
2190 BEAST_EXPECT(ammAlice.expectBalances(
2191 XRP(10'000), USD(9980.01), IOUAmount{9'990'000, 0}));
2192 });
2193
2194 // Withdraw all tokens.
2195 testAMM([&](AMM& ammAlice, Env& env) {
2196 env(trust(carol, STAmount{ammAlice.lptIssue(), 10'000}));
2197 // Can SetTrust only for AMM LP tokens
2198 env(trust(
2199 carol,
2200 STAmount{
2201 Issue{EUR.currency, ammAlice.ammAccount()}, 10'000}),
2203 env.close();
2204 ammAlice.withdrawAll(alice);
2205 BEAST_EXPECT(!ammAlice.ammExists());
2206
2207 BEAST_EXPECT(!env.le(keylet::ownerDir(ammAlice.ammAccount())));
2208
2209 // Can create AMM for the XRP/USD pair
2210 AMM ammCarol(env, carol, XRP(10'000), USD(10'000));
2211 BEAST_EXPECT(ammCarol.expectBalances(
2212 XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
2213 });
2214
2215 // Single deposit 1000USD, withdraw all tokens in USD
2216 testAMM([&](AMM& ammAlice, Env& env) {
2217 ammAlice.deposit(carol, USD(1'000));
2218 ammAlice.withdrawAll(carol, USD(0));
2219 BEAST_EXPECT(ammAlice.expectBalances(
2220 XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
2221 BEAST_EXPECT(
2223 });
2224
2225 // Single deposit 1000USD, withdraw all tokens in XRP
2226 testAMM([&](AMM& ammAlice, Env&) {
2227 ammAlice.deposit(carol, USD(1'000));
2228 ammAlice.withdrawAll(carol, XRP(0));
2229 BEAST_EXPECT(ammAlice.expectBalances(
2230 XRPAmount(9'090'909'091),
2231 STAmount{USD, UINT64_C(10'999'99999999999), -11},
2232 IOUAmount{10'000'000, 0}));
2233 });
2234
2235 // Single deposit/withdraw by the same account
2236 testAMM([&](AMM& ammAlice, Env&) {
2237 // Since a smaller amount might be deposited due to
2238 // the lp tokens adjustment, withdrawing by tokens
2239 // is generally preferred to withdrawing by amount.
2240 auto lpTokens = ammAlice.deposit(carol, USD(1'000));
2241 ammAlice.withdraw(carol, lpTokens, USD(0));
2242 lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6));
2243 ammAlice.withdraw(carol, lpTokens, USD(0));
2244 lpTokens = ammAlice.deposit(carol, XRPAmount(1));
2245 ammAlice.withdraw(carol, lpTokens, XRPAmount(0));
2246 BEAST_EXPECT(ammAlice.expectBalances(
2247 XRP(10'000), USD(10'000), ammAlice.tokens()));
2248 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
2249 });
2250
2251 // Single deposit by different accounts and then withdraw
2252 // in reverse.
2253 testAMM([&](AMM& ammAlice, Env&) {
2254 auto const carolTokens = ammAlice.deposit(carol, USD(1'000));
2255 auto const aliceTokens = ammAlice.deposit(alice, USD(1'000));
2256 ammAlice.withdraw(alice, aliceTokens, USD(0));
2257 ammAlice.withdraw(carol, carolTokens, USD(0));
2258 BEAST_EXPECT(ammAlice.expectBalances(
2259 XRP(10'000), USD(10'000), ammAlice.tokens()));
2260 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
2261 BEAST_EXPECT(ammAlice.expectLPTokens(alice, ammAlice.tokens()));
2262 });
2263
2264 // Equal deposit 10%, withdraw all tokens
2265 testAMM([&](AMM& ammAlice, Env&) {
2266 ammAlice.deposit(carol, 1'000'000);
2267 ammAlice.withdrawAll(carol);
2268 BEAST_EXPECT(ammAlice.expectBalances(
2269 XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
2270 });
2271
2272 // Equal deposit 10%, withdraw all tokens in USD
2273 testAMM([&](AMM& ammAlice, Env&) {
2274 ammAlice.deposit(carol, 1'000'000);
2275 ammAlice.withdrawAll(carol, USD(0));
2276 BEAST_EXPECT(ammAlice.expectBalances(
2277 XRP(11'000),
2278 STAmount{USD, UINT64_C(9'090'909090909092), -12},
2279 IOUAmount{10'000'000, 0}));
2280 });
2281
2282 // Equal deposit 10%, withdraw all tokens in XRP
2283 testAMM([&](AMM& ammAlice, Env&) {
2284 ammAlice.deposit(carol, 1'000'000);
2285 ammAlice.withdrawAll(carol, XRP(0));
2286 BEAST_EXPECT(ammAlice.expectBalances(
2287 XRPAmount(9'090'909'091),
2288 USD(11'000),
2289 IOUAmount{10'000'000, 0}));
2290 });
2291
2292 auto const all = supported_amendments();
2293 // Withdraw with EPrice limit.
2294 testAMM(
2295 [&](AMM& ammAlice, Env& env) {
2296 ammAlice.deposit(carol, 1'000'000);
2297 ammAlice.withdraw(
2298 carol, USD(100), std::nullopt, IOUAmount{520, 0});
2299 if (!env.current()->rules().enabled(fixAMMv1_1))
2300 BEAST_EXPECT(
2301 ammAlice.expectBalances(
2302 XRPAmount(11'000'000'000),
2303 STAmount{USD, UINT64_C(9'372'781065088757), -12},
2304 IOUAmount{10'153'846'15384616, -8}) &&
2305 ammAlice.expectLPTokens(
2306 carol, IOUAmount{153'846'15384616, -8}));
2307 else
2308 BEAST_EXPECT(
2309 ammAlice.expectBalances(
2310 XRPAmount(11'000'000'000),
2311 STAmount{USD, UINT64_C(9'372'781065088769), -12},
2312 IOUAmount{10'153'846'15384616, -8}) &&
2313 ammAlice.expectLPTokens(
2314 carol, IOUAmount{153'846'15384616, -8}));
2315 ammAlice.withdrawAll(carol);
2316 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
2317 },
2318 std::nullopt,
2319 0,
2320 std::nullopt,
2321 {all, all - fixAMMv1_1});
2322
2323 // Withdraw with EPrice limit. AssetOut is 0.
2324 testAMM(
2325 [&](AMM& ammAlice, Env& env) {
2326 ammAlice.deposit(carol, 1'000'000);
2327 ammAlice.withdraw(
2328 carol, USD(0), std::nullopt, IOUAmount{520, 0});
2329 if (!env.current()->rules().enabled(fixAMMv1_1))
2330 BEAST_EXPECT(
2331 ammAlice.expectBalances(
2332 XRPAmount(11'000'000'000),
2333 STAmount{USD, UINT64_C(9'372'781065088757), -12},
2334 IOUAmount{10'153'846'15384616, -8}) &&
2335 ammAlice.expectLPTokens(
2336 carol, IOUAmount{153'846'15384616, -8}));
2337 else
2338 BEAST_EXPECT(
2339 ammAlice.expectBalances(
2340 XRPAmount(11'000'000'000),
2341 STAmount{USD, UINT64_C(9'372'781065088769), -12},
2342 IOUAmount{10'153'846'15384616, -8}) &&
2343 ammAlice.expectLPTokens(
2344 carol, IOUAmount{153'846'15384616, -8}));
2345 },
2346 std::nullopt,
2347 0,
2348 std::nullopt,
2349 {all, all - fixAMMv1_1});
2350
2351 // IOU to IOU + transfer fee
2352 {
2353 Env env{*this};
2354 fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
2355 env(rate(gw, 1.25));
2356 env.close();
2357 // no transfer fee on create
2358 AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
2359 BEAST_EXPECT(ammAlice.expectBalances(
2360 USD(20'000), BTC(0.5), IOUAmount{100, 0}));
2361 BEAST_EXPECT(expectLine(env, alice, USD(0)));
2362 BEAST_EXPECT(expectLine(env, alice, BTC(0)));
2363 fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct);
2364 // no transfer fee on deposit
2365 ammAlice.deposit(carol, 10);
2366 BEAST_EXPECT(ammAlice.expectBalances(
2367 USD(22'000), BTC(0.55), IOUAmount{110, 0}));
2368 BEAST_EXPECT(expectLine(env, carol, USD(0)));
2369 BEAST_EXPECT(expectLine(env, carol, BTC(0)));
2370 // no transfer fee on withdraw
2371 ammAlice.withdraw(carol, 10);
2372 BEAST_EXPECT(ammAlice.expectBalances(
2373 USD(20'000), BTC(0.5), IOUAmount{100, 0}));
2374 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0, 0}));
2375 BEAST_EXPECT(expectLine(env, carol, USD(2'000)));
2376 BEAST_EXPECT(expectLine(env, carol, BTC(0.05)));
2377 }
2378
2379 // Tiny withdraw
2380 testAMM([&](AMM& ammAlice, Env&) {
2381 // By tokens
2382 ammAlice.withdraw(alice, IOUAmount{1, -3});
2383 BEAST_EXPECT(ammAlice.expectBalances(
2384 XRPAmount{9'999'999'999},
2385 STAmount{USD, UINT64_C(9'999'999999), -6},
2386 IOUAmount{9'999'999'999, -3}));
2387 });
2388 testAMM([&](AMM& ammAlice, Env&) {
2389 // Single XRP pool
2390 ammAlice.withdraw(alice, std::nullopt, XRPAmount{1});
2391 BEAST_EXPECT(ammAlice.expectBalances(
2392 XRPAmount{9'999'999'999},
2393 USD(10'000),
2394 IOUAmount{9'999'999'9995, -4}));
2395 });
2396 testAMM([&](AMM& ammAlice, Env&) {
2397 // Single USD pool
2398 ammAlice.withdraw(alice, std::nullopt, STAmount{USD, 1, -10});
2399 BEAST_EXPECT(ammAlice.expectBalances(
2400 XRP(10'000),
2401 STAmount{USD, UINT64_C(9'999'9999999999), -10},
2402 IOUAmount{9'999'999'99999995, -8}));
2403 });
2404
2405 // Withdraw close to entire pool
2406 // Equal by tokens
2407 testAMM([&](AMM& ammAlice, Env&) {
2408 ammAlice.withdraw(alice, IOUAmount{9'999'999'999, -3});
2409 BEAST_EXPECT(ammAlice.expectBalances(
2410 XRPAmount{1}, STAmount{USD, 1, -6}, IOUAmount{1, -3}));
2411 });
2412 // USD by tokens
2413 testAMM([&](AMM& ammAlice, Env&) {
2414 ammAlice.withdraw(alice, IOUAmount{9'999'999}, USD(0));
2415 BEAST_EXPECT(ammAlice.expectBalances(
2416 XRP(10'000), STAmount{USD, 1, -10}, IOUAmount{1}));
2417 });
2418 // XRP by tokens
2419 testAMM([&](AMM& ammAlice, Env&) {
2420 ammAlice.withdraw(alice, IOUAmount{9'999'900}, XRP(0));
2421 BEAST_EXPECT(ammAlice.expectBalances(
2422 XRPAmount{1}, USD(10'000), IOUAmount{100}));
2423 });
2424 // USD
2425 testAMM([&](AMM& ammAlice, Env&) {
2426 ammAlice.withdraw(
2427 alice, STAmount{USD, UINT64_C(9'999'99999999999), -11});
2428 BEAST_EXPECT(ammAlice.expectBalances(
2429 XRP(10000), STAmount{USD, 1, -11}, IOUAmount{316227765, -9}));
2430 });
2431 // XRP
2432 testAMM([&](AMM& ammAlice, Env&) {
2433 ammAlice.withdraw(alice, XRPAmount{9'999'999'999});
2434 BEAST_EXPECT(ammAlice.expectBalances(
2435 XRPAmount{1}, USD(10'000), IOUAmount{100}));
2436 });
2437 }
2438
2439 void
2441 {
2442 testcase("Invalid Fee Vote");
2443 using namespace jtx;
2444
2445 testAMM([&](AMM& ammAlice, Env& env) {
2446 // Invalid flags
2447 ammAlice.vote(
2448 std::nullopt,
2449 1'000,
2451 std::nullopt,
2452 std::nullopt,
2454
2455 // Invalid fee.
2456 ammAlice.vote(
2457 std::nullopt,
2458 1'001,
2459 std::nullopt,
2460 std::nullopt,
2461 std::nullopt,
2462 ter(temBAD_FEE));
2463 BEAST_EXPECT(ammAlice.expectTradingFee(0));
2464
2465 // Invalid Account
2466 Account bad("bad");
2467 env.memoize(bad);
2468 ammAlice.vote(
2469 bad,
2470 1'000,
2471 std::nullopt,
2472 seq(1),
2473 std::nullopt,
2475
2476 // Invalid AMM
2477 ammAlice.vote(
2478 alice,
2479 1'000,
2480 std::nullopt,
2481 std::nullopt,
2482 {{USD, GBP}},
2483 ter(terNO_AMM));
2484
2485 // Account is not LP
2486 ammAlice.vote(
2487 carol,
2488 1'000,
2489 std::nullopt,
2490 std::nullopt,
2491 std::nullopt,
2493 });
2494
2495 // Invalid AMM
2496 testAMM([&](AMM& ammAlice, Env& env) {
2497 ammAlice.withdrawAll(alice);
2498 ammAlice.vote(
2499 alice,
2500 1'000,
2501 std::nullopt,
2502 std::nullopt,
2503 std::nullopt,
2504 ter(terNO_AMM));
2505 });
2506 }
2507
2508 void
2510 {
2511 testcase("Fee Vote");
2512 using namespace jtx;
2513
2514 // One vote sets fee to 1%.
2515 testAMM([&](AMM& ammAlice, Env& env) {
2516 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{0}));
2517 ammAlice.vote({}, 1'000);
2518 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
2519 // Discounted fee is 1/10 of trading fee.
2520 BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{0}));
2521 });
2522
2523 auto vote = [&](AMM& ammAlice,
2524 Env& env,
2525 int i,
2526 int fundUSD = 100'000,
2527 std::uint32_t tokens = 10'000'000,
2528 std::vector<Account>* accounts = nullptr) {
2529 Account a(std::to_string(i));
2530 fund(env, gw, {a}, {USD(fundUSD)}, Fund::Acct);
2531 ammAlice.deposit(a, tokens);
2532 ammAlice.vote(a, 50 * (i + 1));
2533 if (accounts)
2534 accounts->push_back(std::move(a));
2535 };
2536
2537 // Eight votes fill all voting slots, set fee 0.175%.
2538 testAMM([&](AMM& ammAlice, Env& env) {
2539 for (int i = 0; i < 7; ++i)
2540 vote(ammAlice, env, i, 10'000);
2541 BEAST_EXPECT(ammAlice.expectTradingFee(175));
2542 });
2543
2544 // Eight votes fill all voting slots, set fee 0.175%.
2545 // New vote, same account, sets fee 0.225%
2546 testAMM([&](AMM& ammAlice, Env& env) {
2547 for (int i = 0; i < 7; ++i)
2548 vote(ammAlice, env, i);
2549 BEAST_EXPECT(ammAlice.expectTradingFee(175));
2550 Account const a("0");
2551 ammAlice.vote(a, 450);
2552 BEAST_EXPECT(ammAlice.expectTradingFee(225));
2553 });
2554
2555 // Eight votes fill all voting slots, set fee 0.175%.
2556 // New vote, new account, higher vote weight, set higher fee 0.244%
2557 testAMM([&](AMM& ammAlice, Env& env) {
2558 for (int i = 0; i < 7; ++i)
2559 vote(ammAlice, env, i);
2560 BEAST_EXPECT(ammAlice.expectTradingFee(175));
2561 vote(ammAlice, env, 7, 100'000, 20'000'000);
2562 BEAST_EXPECT(ammAlice.expectTradingFee(244));
2563 });
2564
2565 // Eight votes fill all voting slots, set fee 0.219%.
2566 // New vote, new account, higher vote weight, set smaller fee 0.206%
2567 testAMM([&](AMM& ammAlice, Env& env) {
2568 for (int i = 7; i > 0; --i)
2569 vote(ammAlice, env, i);
2570 BEAST_EXPECT(ammAlice.expectTradingFee(219));
2571 vote(ammAlice, env, 0, 100'000, 20'000'000);
2572 BEAST_EXPECT(ammAlice.expectTradingFee(206));
2573 });
2574
2575 // Eight votes fill all voting slots. The accounts then withdraw all
2576 // tokens. An account sets a new fee and the previous slots are
2577 // deleted.
2578 testAMM([&](AMM& ammAlice, Env& env) {
2579 std::vector<Account> accounts;
2580 for (int i = 0; i < 7; ++i)
2581 vote(ammAlice, env, i, 100'000, 10'000'000, &accounts);
2582 BEAST_EXPECT(ammAlice.expectTradingFee(175));
2583 for (int i = 0; i < 7; ++i)
2584 ammAlice.withdrawAll(accounts[i]);
2585 ammAlice.deposit(carol, 10'000'000);
2586 ammAlice.vote(carol, 1'000);
2587 // The initial LP set the fee to 1000. Carol gets 50% voting
2588 // power, and the new fee is 500.
2589 BEAST_EXPECT(ammAlice.expectTradingFee(500));
2590 });
2591
2592 // Eight votes fill all voting slots. The accounts then withdraw some
2593 // tokens. The new vote doesn't get the voting power but
2594 // the slots are refreshed and the fee is updated.
2595 testAMM([&](AMM& ammAlice, Env& env) {
2596 std::vector<Account> accounts;
2597 for (int i = 0; i < 7; ++i)
2598 vote(ammAlice, env, i, 100'000, 10'000'000, &accounts);
2599 BEAST_EXPECT(ammAlice.expectTradingFee(175));
2600 for (int i = 0; i < 7; ++i)
2601 ammAlice.withdraw(accounts[i], 9'000'000);
2602 ammAlice.deposit(carol, 1'000);
2603 // The vote is not added to the slots
2604 ammAlice.vote(carol, 1'000);
2605 auto const info = ammAlice.ammRpcInfo()[jss::amm][jss::vote_slots];
2606 for (std::uint16_t i = 0; i < info.size(); ++i)
2607 BEAST_EXPECT(info[i][jss::account] != carol.human());
2608 // But the slots are refreshed and the fee is changed
2609 BEAST_EXPECT(ammAlice.expectTradingFee(82));
2610 });
2611 }
2612
2613 void
2615 {
2616 testcase("Invalid Bid");
2617 using namespace jtx;
2618 using namespace std::chrono;
2619
2620 // burn all the LPTokens through a AMMBid transaction
2621 {
2622 Env env(*this);
2623 fund(env, gw, {alice}, XRP(2'000), {USD(2'000)});
2624 AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000);
2625
2626 // auction slot is owned by the creator of the AMM i.e. gw
2627 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
2628
2629 // gw attempts to burn all her LPTokens through a bid transaction
2630 // this transaction fails because AMMBid transaction can not burn
2631 // all the outstanding LPTokens
2632 env(amm.bid({
2633 .account = gw,
2634 .bidMin = 1'000'000,
2635 }),
2637 }
2638
2639 // burn all the LPTokens through a AMMBid transaction
2640 {
2641 Env env(*this);
2642 fund(env, gw, {alice}, XRP(2'000), {USD(2'000)});
2643 AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000);
2644
2645 // auction slot is owned by the creator of the AMM i.e. gw
2646 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
2647
2648 // gw burns all but one of its LPTokens through a bid transaction
2649 // this transaction suceeds because the bid price is less than
2650 // the total outstanding LPToken balance
2651 env(amm.bid({
2652 .account = gw,
2653 .bidMin = STAmount{amm.lptIssue(), UINT64_C(999'999)},
2654 }),
2655 ter(tesSUCCESS))
2656 .close();
2657
2658 // gw must own the auction slot
2659 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{999'999}));
2660
2661 // 999'999 tokens are burned, only 1 LPToken is owned by gw
2662 BEAST_EXPECT(
2663 amm.expectBalances(XRP(1'000), USD(1'000), IOUAmount{1}));
2664
2665 // gw owns only 1 LPToken in its balance
2666 BEAST_EXPECT(Number{amm.getLPTokensBalance(gw)} == 1);
2667
2668 // gw attempts to burn the last of its LPTokens in an AMMBid
2669 // transaction. This transaction fails because it would burn all
2670 // the remaining LPTokens
2671 env(amm.bid({
2672 .account = gw,
2673 .bidMin = 1,
2674 }),
2676 }
2677
2678 testAMM([&](AMM& ammAlice, Env& env) {
2679 // Invalid flags
2680 env(ammAlice.bid({
2681 .account = carol,
2682 .bidMin = 0,
2683 .flags = tfWithdrawAll,
2684 }),
2685 ter(temINVALID_FLAG));
2686
2687 ammAlice.deposit(carol, 1'000'000);
2688 // Invalid Bid price <= 0
2689 for (auto bid : {0, -100})
2690 {
2691 env(ammAlice.bid({
2692 .account = carol,
2693 .bidMin = bid,
2694 }),
2695 ter(temBAD_AMOUNT));
2696 env(ammAlice.bid({
2697 .account = carol,
2698 .bidMax = bid,
2699 }),
2700 ter(temBAD_AMOUNT));
2701 }
2702
2703 // Invlaid Min/Max combination
2704 env(ammAlice.bid({
2705 .account = carol,
2706 .bidMin = 200,
2707 .bidMax = 100,
2708 }),
2709 ter(tecAMM_INVALID_TOKENS));
2710
2711 // Invalid Account
2712 Account bad("bad");
2713 env.memoize(bad);
2714 env(ammAlice.bid({
2715 .account = bad,
2716 .bidMax = 100,
2717 }),
2718 seq(1),
2719 ter(terNO_ACCOUNT));
2720
2721 // Account is not LP
2722 Account const dan("dan");
2723 env.fund(XRP(1'000), dan);
2724 env(ammAlice.bid({
2725 .account = dan,
2726 .bidMin = 100,
2727 }),
2728 ter(tecAMM_INVALID_TOKENS));
2729 env(ammAlice.bid({
2730 .account = dan,
2731 }),
2732 ter(tecAMM_INVALID_TOKENS));
2733
2734 // Auth account is invalid.
2735 env(ammAlice.bid({
2736 .account = carol,
2737 .bidMin = 100,
2738 .authAccounts = {bob},
2739 }),
2740 ter(terNO_ACCOUNT));
2741
2742 // Invalid Assets
2743 env(ammAlice.bid({
2744 .account = alice,
2745 .bidMax = 100,
2746 .assets = {{USD, GBP}},
2747 }),
2748 ter(terNO_AMM));
2749
2750 // Invalid Min/Max issue
2751 env(ammAlice.bid({
2752 .account = alice,
2753 .bidMax = STAmount{USD, 100},
2754 }),
2755 ter(temBAD_AMM_TOKENS));
2756 env(ammAlice.bid({
2757 .account = alice,
2758 .bidMin = STAmount{USD, 100},
2759 }),
2760 ter(temBAD_AMM_TOKENS));
2761 });
2762
2763 // Invalid AMM
2764 testAMM([&](AMM& ammAlice, Env& env) {
2765 ammAlice.withdrawAll(alice);
2766 env(ammAlice.bid({
2767 .account = alice,
2768 .bidMax = 100,
2769 }),
2770 ter(terNO_AMM));
2771 });
2772
2773 // More than four Auth accounts.
2774 testAMM([&](AMM& ammAlice, Env& env) {
2775 Account ed("ed");
2776 Account bill("bill");
2777 Account scott("scott");
2778 Account james("james");
2779 env.fund(XRP(1'000), bob, ed, bill, scott, james);
2780 env.close();
2781 ammAlice.deposit(carol, 1'000'000);
2782 env(ammAlice.bid({
2783 .account = carol,
2784 .bidMin = 100,
2785 .authAccounts = {bob, ed, bill, scott, james},
2786 }),
2787 ter(temMALFORMED));
2788 });
2789
2790 // Bid price exceeds LP owned tokens
2791 testAMM([&](AMM& ammAlice, Env& env) {
2792 fund(env, gw, {bob}, XRP(1'000), {USD(100)}, Fund::Acct);
2793 ammAlice.deposit(carol, 1'000'000);
2794 ammAlice.deposit(bob, 10);
2795 env(ammAlice.bid({
2796 .account = carol,
2797 .bidMin = 1'000'001,
2798 }),
2799 ter(tecAMM_INVALID_TOKENS));
2800 env(ammAlice.bid({
2801 .account = carol,
2802 .bidMax = 1'000'001,
2803 }),
2804 ter(tecAMM_INVALID_TOKENS));
2805 env(ammAlice.bid({
2806 .account = carol,
2807 .bidMin = 1'000,
2808 }));
2809 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{1'000}));
2810 // Slot purchase price is more than 1000 but bob only has 10 tokens
2811 env(ammAlice.bid({
2812 .account = bob,
2813 }),
2814 ter(tecAMM_INVALID_TOKENS));
2815 });
2816
2817 // Bid all tokens, still own the slot
2818 {
2819 Env env(*this);
2820 fund(env, gw, {alice, bob}, XRP(1'000), {USD(1'000)});
2821 AMM amm(env, gw, XRP(10), USD(1'000));
2822 auto const lpIssue = amm.lptIssue();
2823 env.trust(STAmount{lpIssue, 100}, alice);
2824 env.trust(STAmount{lpIssue, 50}, bob);
2825 env(pay(gw, alice, STAmount{lpIssue, 100}));
2826 env(pay(gw, bob, STAmount{lpIssue, 50}));
2827 env(amm.bid({.account = alice, .bidMin = 100}));
2828 // Alice doesn't have any more tokens, but
2829 // she still owns the slot.
2830 env(amm.bid({
2831 .account = bob,
2832 .bidMax = 50,
2833 }),
2834 ter(tecAMM_FAILED));
2835 }
2836 }
2837
2838 void
2840 {
2841 testcase("Bid");
2842 using namespace jtx;
2843 using namespace std::chrono;
2844
2845 // Auction slot initially is owned by AMM creator, who pays 0 price.
2846
2847 // Bid 110 tokens. Pay bidMin.
2848 testAMM(
2849 [&](AMM& ammAlice, Env& env) {
2850 ammAlice.deposit(carol, 1'000'000);
2851 env(ammAlice.bid({.account = carol, .bidMin = 110}));
2852 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2853 // 110 tokens are burned.
2854 BEAST_EXPECT(ammAlice.expectBalances(
2855 XRP(11'000), USD(11'000), IOUAmount{10'999'890, 0}));
2856 },
2857 std::nullopt,
2858 0,
2859 std::nullopt,
2860 {features});
2861
2862 // Bid with min/max when the pay price is less than min.
2863 testAMM(
2864 [&](AMM& ammAlice, Env& env) {
2865 ammAlice.deposit(carol, 1'000'000);
2866 // Bid exactly 110. Pay 110 because the pay price is < 110.
2867 env(ammAlice.bid(
2868 {.account = carol, .bidMin = 110, .bidMax = 110}));
2869 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2870 BEAST_EXPECT(ammAlice.expectBalances(
2871 XRP(11'000), USD(11'000), IOUAmount{10'999'890}));
2872 // Bid exactly 180-200. Pay 180 because the pay price is < 180.
2873 env(ammAlice.bid(
2874 {.account = alice, .bidMin = 180, .bidMax = 200}));
2875 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{180}));
2876 BEAST_EXPECT(ammAlice.expectBalances(
2877 XRP(11'000), USD(11'000), IOUAmount{10'999'814'5, -1}));
2878 },
2879 std::nullopt,
2880 0,
2881 std::nullopt,
2882 {features});
2883
2884 // Start bid at bidMin 110.
2885 testAMM(
2886 [&](AMM& ammAlice, Env& env) {
2887 ammAlice.deposit(carol, 1'000'000);
2888 // Bid, pay bidMin.
2889 env(ammAlice.bid({.account = carol, .bidMin = 110}));
2890 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2891
2892 fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct);
2893 ammAlice.deposit(bob, 1'000'000);
2894 // Bid, pay the computed price.
2895 env(ammAlice.bid({.account = bob}));
2896 BEAST_EXPECT(
2897 ammAlice.expectAuctionSlot(0, 0, IOUAmount(1155, -1)));
2898
2899 // Bid bidMax fails because the computed price is higher.
2900 env(ammAlice.bid({
2901 .account = carol,
2902 .bidMax = 120,
2903 }),
2905 // Bid MaxSlotPrice succeeds - pay computed price
2906 env(ammAlice.bid({.account = carol, .bidMax = 600}));
2907 BEAST_EXPECT(
2908 ammAlice.expectAuctionSlot(0, 0, IOUAmount{121'275, -3}));
2909
2910 // Bid Min/MaxSlotPrice fails because the computed price is not
2911 // in range
2912 env(ammAlice.bid({
2913 .account = carol,
2914 .bidMin = 10,
2915 .bidMax = 100,
2916 }),
2918 // Bid Min/MaxSlotPrice succeeds - pay computed price
2919 env(ammAlice.bid(
2920 {.account = carol, .bidMin = 100, .bidMax = 600}));
2921 BEAST_EXPECT(
2922 ammAlice.expectAuctionSlot(0, 0, IOUAmount{127'33875, -5}));
2923 },
2924 std::nullopt,
2925 0,
2926 std::nullopt,
2927 {features});
2928
2929 // Slot states.
2930 testAMM(
2931 [&](AMM& ammAlice, Env& env) {
2932 ammAlice.deposit(carol, 1'000'000);
2933
2934 fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct);
2935 ammAlice.deposit(bob, 1'000'000);
2936 BEAST_EXPECT(ammAlice.expectBalances(
2937 XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0}));
2938
2939 // Initial state. Pay bidMin.
2940 env(ammAlice.bid({.account = carol, .bidMin = 110})).close();
2941 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2942
2943 // 1st Interval after close, price for 0th interval.
2944 env(ammAlice.bid({.account = bob}));
2946 BEAST_EXPECT(
2947 ammAlice.expectAuctionSlot(0, 1, IOUAmount{1'155, -1}));
2948
2949 // 10th Interval after close, price for 1st interval.
2950 env(ammAlice.bid({.account = carol}));
2952 BEAST_EXPECT(
2953 ammAlice.expectAuctionSlot(0, 10, IOUAmount{121'275, -3}));
2954
2955 // 20th Interval (expired) after close, price for 10th interval.
2956 env(ammAlice.bid({.account = bob}));
2957 env.close(seconds(
2960 1));
2961 BEAST_EXPECT(ammAlice.expectAuctionSlot(
2962 0, std::nullopt, IOUAmount{127'33875, -5}));
2963
2964 // 0 Interval.
2965 env(ammAlice.bid({.account = carol, .bidMin = 110})).close();
2966 BEAST_EXPECT(ammAlice.expectAuctionSlot(
2967 0, std::nullopt, IOUAmount{110}));
2968 // ~321.09 tokens burnt on bidding fees.
2969 BEAST_EXPECT(ammAlice.expectBalances(
2970 XRP(12'000), USD(12'000), IOUAmount{11'999'678'91, -2}));
2971 },
2972 std::nullopt,
2973 0,
2974 std::nullopt,
2975 {features});
2976
2977 // Pool's fee 1%. Bid bidMin.
2978 // Auction slot owner and auth account trade at discounted fee -
2979 // 1/10 of the trading fee.
2980 // Other accounts trade at 1% fee.
2981 testAMM(
2982 [&](AMM& ammAlice, Env& env) {
2983 Account const dan("dan");
2984 Account const ed("ed");
2985 fund(env, gw, {bob, dan, ed}, {USD(20'000)}, Fund::Acct);
2986 ammAlice.deposit(bob, 1'000'000);
2987 ammAlice.deposit(ed, 1'000'000);
2988 ammAlice.deposit(carol, 500'000);
2989 ammAlice.deposit(dan, 500'000);
2990 auto ammTokens = ammAlice.getLPTokensBalance();
2991 env(ammAlice.bid({
2992 .account = carol,
2993 .bidMin = 120,
2994 .authAccounts = {bob, ed},
2995 }));
2996 auto const slotPrice = IOUAmount{5'200};
2997 ammTokens -= slotPrice;
2998 BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice));
2999 BEAST_EXPECT(ammAlice.expectBalances(
3000 XRP(13'000), USD(13'000), ammTokens));
3001 // Discounted trade
3002 for (int i = 0; i < 10; ++i)
3003 {
3004 auto tokens = ammAlice.deposit(carol, USD(100));
3005 ammAlice.withdraw(carol, tokens, USD(0));
3006 tokens = ammAlice.deposit(bob, USD(100));
3007 ammAlice.withdraw(bob, tokens, USD(0));
3008 tokens = ammAlice.deposit(ed, USD(100));
3009 ammAlice.withdraw(ed, tokens, USD(0));
3010 }
3011 // carol, bob, and ed pay ~0.99USD in fees.
3012 if (!features[fixAMMv1_1])
3013 {
3014 BEAST_EXPECT(
3015 env.balance(carol, USD) ==
3016 STAmount(USD, UINT64_C(29'499'00572620545), -11));
3017 BEAST_EXPECT(
3018 env.balance(bob, USD) ==
3019 STAmount(USD, UINT64_C(18'999'00572616195), -11));
3020 BEAST_EXPECT(
3021 env.balance(ed, USD) ==
3022 STAmount(USD, UINT64_C(18'999'00572611841), -11));
3023 // USD pool is slightly higher because of the fees.
3024 BEAST_EXPECT(ammAlice.expectBalances(
3025 XRP(13'000),
3026 STAmount(USD, UINT64_C(13'002'98282151419), -11),
3027 ammTokens));
3028 }
3029 else
3030 {
3031 BEAST_EXPECT(
3032 env.balance(carol, USD) ==
3033 STAmount(USD, UINT64_C(29'499'00572620544), -11));
3034 BEAST_EXPECT(
3035 env.balance(bob, USD) ==
3036 STAmount(USD, UINT64_C(18'999'00572616194), -11));
3037 BEAST_EXPECT(
3038 env.balance(ed, USD) ==
3039 STAmount(USD, UINT64_C(18'999'0057261184), -10));
3040 // USD pool is slightly higher because of the fees.
3041 BEAST_EXPECT(ammAlice.expectBalances(
3042 XRP(13'000),
3043 STAmount(USD, UINT64_C(13'002'98282151422), -11),
3044 ammTokens));
3045 }
3046 ammTokens = ammAlice.getLPTokensBalance();
3047 // Trade with the fee
3048 for (int i = 0; i < 10; ++i)
3049 {
3050 auto const tokens = ammAlice.deposit(dan, USD(100));
3051 ammAlice.withdraw(dan, tokens, USD(0));
3052 }
3053 // dan pays ~9.94USD, which is ~10 times more in fees than
3054 // carol, bob, ed. the discounted fee is 10 times less
3055 // than the trading fee.
3056 if (!features[fixAMMv1_1])
3057 {
3058 BEAST_EXPECT(
3059 env.balance(dan, USD) ==
3060 STAmount(USD, UINT64_C(19'490'056722744), -9));
3061 // USD pool gains more in dan's fees.
3062 BEAST_EXPECT(ammAlice.expectBalances(
3063 XRP(13'000),
3064 STAmount{USD, UINT64_C(13'012'92609877019), -11},
3065 ammTokens));
3066 // Discounted fee payment
3067 ammAlice.deposit(carol, USD(100));
3068 ammTokens = ammAlice.getLPTokensBalance();
3069 BEAST_EXPECT(ammAlice.expectBalances(
3070 XRP(13'000),
3071 STAmount{USD, UINT64_C(13'112'92609877019), -11},
3072 ammTokens));
3073 env(pay(carol, bob, USD(100)),
3074 path(~USD),
3075 sendmax(XRP(110)));
3076 env.close();
3077 // carol pays 100000 drops in fees
3078 // 99900668XRP swapped in for 100USD
3079 BEAST_EXPECT(ammAlice.expectBalances(
3080 XRPAmount{13'100'000'668},
3081 STAmount{USD, UINT64_C(13'012'92609877019), -11},
3082 ammTokens));
3083 }
3084 else
3085 {
3086 BEAST_EXPECT(
3087 env.balance(dan, USD) ==
3088 STAmount(USD, UINT64_C(19'490'05672274399), -11));
3089 // USD pool gains more in dan's fees.
3090 BEAST_EXPECT(ammAlice.expectBalances(
3091 XRP(13'000),
3092 STAmount{USD, UINT64_C(13'012'92609877023), -11},
3093 ammTokens));
3094 // Discounted fee payment
3095 ammAlice.deposit(carol, USD(100));
3096 ammTokens = ammAlice.getLPTokensBalance();
3097 BEAST_EXPECT(ammAlice.expectBalances(
3098 XRP(13'000),
3099 STAmount{USD, UINT64_C(13'112'92609877023), -11},
3100 ammTokens));
3101 env(pay(carol, bob, USD(100)),
3102 path(~USD),
3103 sendmax(XRP(110)));
3104 env.close();
3105 // carol pays 100000 drops in fees
3106 // 99900668XRP swapped in for 100USD
3107 BEAST_EXPECT(ammAlice.expectBalances(
3108 XRPAmount{13'100'000'668},
3109 STAmount{USD, UINT64_C(13'012'92609877023), -11},
3110 ammTokens));
3111 }
3112 // Payment with the trading fee
3113 env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(110)));
3114 env.close();
3115 // alice pays ~1.011USD in fees, which is ~10 times more
3116 // than carol's fee
3117 // 100.099431529USD swapped in for 100XRP
3118 if (!features[fixAMMv1_1])
3119 {
3120 BEAST_EXPECT(ammAlice.expectBalances(
3121 XRPAmount{13'000'000'668},
3122 STAmount{USD, UINT64_C(13'114'03663047264), -11},
3123 ammTokens));
3124 }
3125 else
3126 {
3127 BEAST_EXPECT(ammAlice.expectBalances(
3128 XRPAmount{13'000'000'668},
3129 STAmount{USD, UINT64_C(13'114'03663047269), -11},
3130 ammTokens));
3131 }
3132 // Auction slot expired, no discounted fee
3134 // clock is parent's based
3135 env.close();
3136 if (!features[fixAMMv1_1])
3137 BEAST_EXPECT(
3138 env.balance(carol, USD) ==
3139 STAmount(USD, UINT64_C(29'399'00572620545), -11));
3140 else
3141 BEAST_EXPECT(
3142 env.balance(carol, USD) ==
3143 STAmount(USD, UINT64_C(29'399'00572620544), -11));
3144 ammTokens = ammAlice.getLPTokensBalance();
3145 for (int i = 0; i < 10; ++i)
3146 {
3147 auto const tokens = ammAlice.deposit(carol, USD(100));
3148 ammAlice.withdraw(carol, tokens, USD(0));
3149 }
3150 // carol pays ~9.94USD in fees, which is ~10 times more in
3151 // trading fees vs discounted fee.
3152 if (!features[fixAMMv1_1])
3153 {
3154 BEAST_EXPECT(
3155 env.balance(carol, USD) ==
3156 STAmount(USD, UINT64_C(29'389'06197177128), -11));
3157 BEAST_EXPECT(ammAlice.expectBalances(
3158 XRPAmount{13'000'000'668},
3159 STAmount{USD, UINT64_C(13'123'98038490681), -11},
3160 ammTokens));
3161 }
3162 else
3163 {
3164 BEAST_EXPECT(
3165 env.balance(carol, USD) ==
3166 STAmount(USD, UINT64_C(29'389'06197177124), -11));
3167 BEAST_EXPECT(ammAlice.expectBalances(
3168 XRPAmount{13'000'000'668},
3169 STAmount{USD, UINT64_C(13'123'98038490689), -11},
3170 ammTokens));
3171 }
3172 env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110)));
3173 env.close();
3174 // carol pays ~1.008XRP in trading fee, which is
3175 // ~10 times more than the discounted fee.
3176 // 99.815876XRP is swapped in for 100USD
3177 if (!features[fixAMMv1_1])
3178 {
3179 BEAST_EXPECT(ammAlice.expectBalances(
3180 XRPAmount(13'100'824'790),
3181 STAmount{USD, UINT64_C(13'023'98038490681), -11},
3182 ammTokens));
3183 }
3184 else
3185 {
3186 BEAST_EXPECT(ammAlice.expectBalances(
3187 XRPAmount(13'100'824'790),
3188 STAmount{USD, UINT64_C(13'023'98038490689), -11},
3189 ammTokens));
3190 }
3191 },
3192 std::nullopt,
3193 1'000,
3194 std::nullopt,
3195 {features});
3196
3197 // Bid tiny amount
3198 testAMM(
3199 [&](AMM& ammAlice, Env& env) {
3200 // Bid a tiny amount
3201 auto const tiny =
3202 Number{STAmount::cMinValue, STAmount::cMinOffset};
3203 env(ammAlice.bid(
3204 {.account = alice, .bidMin = IOUAmount{tiny}}));
3205 // Auction slot purchase price is equal to the tiny amount
3206 // since the minSlotPrice is 0 with no trading fee.
3207 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny}));
3208 // The purchase price is too small to affect the total tokens
3209 BEAST_EXPECT(ammAlice.expectBalances(
3210 XRP(10'000), USD(10'000), ammAlice.tokens()));
3211 // Bid the tiny amount
3212 env(ammAlice.bid({
3213 .account = alice,
3214 .bidMin =
3215 IOUAmount{STAmount::cMinValue, STAmount::cMinOffset},
3216 }));
3217 // Pay slightly higher price
3218 BEAST_EXPECT(ammAlice.expectAuctionSlot(
3219 0, 0, IOUAmount{tiny * Number{105, -2}}));
3220 // The purchase price is still too small to affect the total
3221 // tokens
3222 BEAST_EXPECT(ammAlice.expectBalances(
3223 XRP(10'000), USD(10'000), ammAlice.tokens()));
3224 },
3225 std::nullopt,
3226 0,
3227 std::nullopt,
3228 {features});
3229
3230 // Reset auth account
3231 testAMM(
3232 [&](AMM& ammAlice, Env& env) {
3233 env(ammAlice.bid({
3234 .account = alice,
3235 .bidMin = IOUAmount{100},
3236 .authAccounts = {carol},
3237 }));
3238 BEAST_EXPECT(ammAlice.expectAuctionSlot({carol}));
3239 env(ammAlice.bid({.account = alice, .bidMin = IOUAmount{100}}));
3240 BEAST_EXPECT(ammAlice.expectAuctionSlot({}));
3241 Account bob("bob");
3242 Account dan("dan");
3243 fund(env, {bob, dan}, XRP(1'000));
3244 env(ammAlice.bid({
3245 .account = alice,
3246 .bidMin = IOUAmount{100},
3247 .authAccounts = {bob, dan},
3248 }));
3249 BEAST_EXPECT(ammAlice.expectAuctionSlot({bob, dan}));
3250 },
3251 std::nullopt,
3252 0,
3253 std::nullopt,
3254 {features});
3255
3256 // Bid all tokens, still own the slot and trade at a discount
3257 {
3258 Env env(*this, features);
3259 fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)});
3260 AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000);
3261 auto const lpIssue = amm.lptIssue();
3262 env.trust(STAmount{lpIssue, 500}, alice);
3263 env.trust(STAmount{lpIssue, 50}, bob);
3264 env(pay(gw, alice, STAmount{lpIssue, 500}));
3265 env(pay(gw, bob, STAmount{lpIssue, 50}));
3266 // Alice doesn't have anymore lp tokens
3267 env(amm.bid({.account = alice, .bidMin = 500}));
3268 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{500}));
3269 BEAST_EXPECT(expectLine(env, alice, STAmount{lpIssue, 0}));
3270 // But trades with the discounted fee since she still owns the slot.
3271 // Alice pays 10011 drops in fees
3272 env(pay(alice, bob, USD(10)), path(~USD), sendmax(XRP(11)));
3273 BEAST_EXPECT(amm.expectBalances(
3274 XRPAmount{1'010'010'011},
3275 USD(1'000),
3276 IOUAmount{1'004'487'562112089, -9}));
3277 // Bob pays the full fee ~0.1USD
3278 env(pay(bob, alice, XRP(10)), path(~XRP), sendmax(USD(11)));
3279 if (!features[fixAMMv1_1])
3280 {
3281 BEAST_EXPECT(amm.expectBalances(
3282 XRPAmount{1'000'010'011},
3283 STAmount{USD, UINT64_C(1'010'10090898081), -11},
3284 IOUAmount{1'004'487'562112089, -9}));
3285 }
3286 else
3287 {
3288 BEAST_EXPECT(amm.expectBalances(
3289 XRPAmount{1'000'010'011},
3290 STAmount{USD, UINT64_C(1'010'100908980811), -12},
3291 IOUAmount{1'004'487'562112089, -9}));
3292 }
3293 }
3294
3295 // preflight tests
3296 {
3297 Env env(*this, features);
3298 fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)});
3299 AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000);
3300 Json::Value tx = amm.bid({.account = alice, .bidMin = 500});
3301
3302 {
3303 auto jtx = env.jt(tx, seq(1), fee(10));
3304 env.app().config().features.erase(featureAMM);
3305 PreflightContext pfctx(
3306 env.app(),
3307 *jtx.stx,
3308 env.current()->rules(),
3309 tapNONE,
3310 env.journal);
3311 auto pf = AMMBid::preflight(pfctx);
3312 BEAST_EXPECT(pf == temDISABLED);
3313 env.app().config().features.insert(featureAMM);
3314 }
3315
3316 {
3317 auto jtx = env.jt(tx, seq(1), fee(10));
3318 jtx.jv["TxnSignature"] = "deadbeef";
3319 jtx.stx = env.ust(jtx);
3320 PreflightContext pfctx(
3321 env.app(),
3322 *jtx.stx,
3323 env.current()->rules(),
3324 tapNONE,
3325 env.journal);
3326 auto pf = AMMBid::preflight(pfctx);
3327 BEAST_EXPECT(pf != tesSUCCESS);
3328 }
3329
3330 {
3331 auto jtx = env.jt(tx, seq(1), fee(10));
3332 jtx.jv["Asset2"]["currency"] = "XRP";
3333 jtx.jv["Asset2"].removeMember("issuer");
3334 jtx.stx = env.ust(jtx);
3335 PreflightContext pfctx(
3336 env.app(),
3337 *jtx.stx,
3338 env.current()->rules(),
3339 tapNONE,
3340 env.journal);
3341 auto pf = AMMBid::preflight(pfctx);
3342 BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
3343 }
3344 }
3345 }
3346
3347 void
3349 {
3350 testcase("Invalid AMM Payment");
3351 using namespace jtx;
3352 using namespace std::chrono;
3353 using namespace std::literals::chrono_literals;
3354
3355 // Can't pay into AMM account.
3356 // Can't pay out since there is no keys
3357 for (auto const& acct : {gw, alice})
3358 {
3359 {
3360 Env env(*this);
3361 fund(env, gw, {alice, carol}, XRP(1'000), {USD(100)});
3362 // XRP balance is below reserve
3363 AMM ammAlice(env, acct, XRP(10), USD(10));
3364 // Pay below reserve
3365 env(pay(carol, ammAlice.ammAccount(), XRP(10)),
3367 // Pay above reserve
3368 env(pay(carol, ammAlice.ammAccount(), XRP(300)),
3370 // Pay IOU
3371 env(pay(carol, ammAlice.ammAccount(), USD(10)),
3373 }
3374 {
3375 Env env(*this);
3376 fund(env, gw, {alice, carol}, XRP(10'000'000), {USD(10'000)});
3377 // XRP balance is above reserve
3378 AMM ammAlice(env, acct, XRP(1'000'000), USD(100));
3379 // Pay below reserve
3380 env(pay(carol, ammAlice.ammAccount(), XRP(10)),
3382 // Pay above reserve
3383 env(pay(carol, ammAlice.ammAccount(), XRP(1'000'000)),
3385 }
3386 }
3387
3388 // Can't pay into AMM with escrow.
3389 testAMM([&](AMM& ammAlice, Env& env) {
3390 env(escrow(carol, ammAlice.ammAccount(), XRP(1)),
3391 condition(cb1),
3392 finish_time(env.now() + 1s),
3393 cancel_time(env.now() + 2s),
3394 fee(1'500),
3396 });
3397
3398 // Can't pay into AMM with paychan.
3399 testAMM([&](AMM& ammAlice, Env& env) {
3400 auto const pk = carol.pk();
3401 auto const settleDelay = 100s;
3402 NetClock::time_point const cancelAfter =
3403 env.current()->info().parentCloseTime + 200s;
3404 env(create(
3405 carol,
3406 ammAlice.ammAccount(),
3407 XRP(1'000),
3408 settleDelay,
3409 pk,
3410 cancelAfter),
3412 });
3413
3414 // Can't pay into AMM with checks.
3415 testAMM([&](AMM& ammAlice, Env& env) {
3416 env(check::create(env.master.id(), ammAlice.ammAccount(), XRP(100)),
3418 });
3419
3420 // Pay amounts close to one side of the pool
3421 testAMM(
3422 [&](AMM& ammAlice, Env& env) {
3423 // Can't consume whole pool
3424 env(pay(alice, carol, USD(100)),
3425 path(~USD),
3426 sendmax(XRP(1'000'000'000)),
3428 env(pay(alice, carol, XRP(100)),
3429 path(~XRP),
3430 sendmax(USD(1'000'000'000)),
3432 // Overflow
3433 env(pay(alice,
3434 carol,
3435 STAmount{USD, UINT64_C(99'999999999), -9}),
3436 path(~USD),
3437 sendmax(XRP(1'000'000'000)),
3439 env(pay(alice,
3440 carol,
3441 STAmount{USD, UINT64_C(999'99999999), -8}),
3442 path(~USD),
3443 sendmax(XRP(1'000'000'000)),
3445 env(pay(alice, carol, STAmount{xrpIssue(), 99'999'999}),
3446 path(~XRP),
3447 sendmax(USD(1'000'000'000)),
3449 // Sender doesn't have enough funds
3450 env(pay(alice, carol, USD(99.99)),
3451 path(~USD),
3452 sendmax(XRP(1'000'000'000)),
3454 env(pay(alice, carol, STAmount{xrpIssue(), 99'990'000}),
3455 path(~XRP),
3456 sendmax(USD(1'000'000'000)),
3458 },
3459 {{XRP(100), USD(100)}});
3460
3461 // Globally frozen
3462 testAMM([&](AMM& ammAlice, Env& env) {
3463 env(fset(gw, asfGlobalFreeze));
3464 env.close();
3465 env(pay(alice, carol, USD(1)),
3466 path(~USD),
3468 sendmax(XRP(10)),
3469 ter(tecPATH_DRY));
3470 env(pay(alice, carol, XRP(1)),
3471 path(~XRP),
3473 sendmax(USD(10)),
3474 ter(tecPATH_DRY));
3475 });
3476
3477 // Individually frozen AMM
3478 testAMM([&](AMM& ammAlice, Env& env) {
3479 env(trust(
3480 gw,
3481 STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0},
3482 tfSetFreeze));
3483 env.close();
3484 env(pay(alice, carol, USD(1)),
3485 path(~USD),
3487 sendmax(XRP(10)),
3488 ter(tecPATH_DRY));
3489 env(pay(alice, carol, XRP(1)),
3490 path(~XRP),
3492 sendmax(USD(10)),
3493 ter(tecPATH_DRY));
3494 });
3495
3496 // Individually frozen accounts
3497 testAMM([&](AMM& ammAlice, Env& env) {
3498 env(trust(gw, carol["USD"](0), tfSetFreeze));
3499 env(trust(gw, alice["USD"](0), tfSetFreeze));
3500 env.close();
3501 env(pay(alice, carol, XRP(1)),
3502 path(~XRP),
3503 sendmax(USD(10)),
3505 ter(tecPATH_DRY));
3506 });
3507 }
3508
3509 void
3511 {
3512 testcase("Basic Payment");
3513 using namespace jtx;
3514
3515 // Payment 100USD for 100XRP.
3516 // Force one path with tfNoRippleDirect.
3517 testAMM(
3518 [&](AMM& ammAlice, Env& env) {
3519 env.fund(jtx::XRP(30'000), bob);
3520 env.close();
3521 env(pay(bob, carol, USD(100)),
3522 path(~USD),
3523 sendmax(XRP(100)),
3525 env.close();
3526 BEAST_EXPECT(ammAlice.expectBalances(
3527 XRP(10'100), USD(10'000), ammAlice.tokens()));
3528 // Initial balance 30,000 + 100
3529 BEAST_EXPECT(expectLine(env, carol, USD(30'100)));
3530 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
3531 BEAST_EXPECT(expectLedgerEntryRoot(
3532 env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
3533 },
3534 {{XRP(10'000), USD(10'100)}},
3535 0,
3536 std::nullopt,
3537 {features});
3538
3539 // Payment 100USD for 100XRP, use default path.
3540 testAMM(
3541 [&](AMM& ammAlice, Env& env) {
3542 env.fund(jtx::XRP(30'000), bob);
3543 env.close();
3544 env(pay(bob, carol, USD(100)), sendmax(XRP(100)));
3545 env.close();
3546 BEAST_EXPECT(ammAlice.expectBalances(
3547 XRP(10'100), USD(10'000), ammAlice.tokens()));
3548 // Initial balance 30,000 + 100
3549 BEAST_EXPECT(expectLine(env, carol, USD(30'100)));
3550 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
3551 BEAST_EXPECT(expectLedgerEntryRoot(
3552 env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
3553 },
3554 {{XRP(10'000), USD(10'100)}},
3555 0,
3556 std::nullopt,
3557 {features});
3558
3559 // This payment is identical to above. While it has
3560 // both default path and path, activeStrands has one path.
3561 testAMM(
3562 [&](AMM& ammAlice, Env& env) {
3563 env.fund(jtx::XRP(30'000), bob);
3564 env.close();
3565 env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(100)));
3566 env.close();
3567 BEAST_EXPECT(ammAlice.expectBalances(
3568 XRP(10'100), USD(10'000), ammAlice.tokens()));
3569 // Initial balance 30,000 + 100
3570 BEAST_EXPECT(expectLine(env, carol, USD(30'100)));
3571 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
3572 BEAST_EXPECT(expectLedgerEntryRoot(
3573 env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
3574 },
3575 {{XRP(10'000), USD(10'100)}},
3576 0,
3577 std::nullopt,
3578 {features});
3579
3580 // Payment with limitQuality set.
3581 testAMM(
3582 [&](AMM& ammAlice, Env& env) {
3583 env.fund(jtx::XRP(30'000), bob);
3584 env.close();
3585 // Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP
3586 // would have been sent has it not been for limitQuality.
3587 env(pay(bob, carol, USD(100)),
3588 path(~USD),
3589 sendmax(XRP(100)),
3590 txflags(
3592 env.close();
3593 BEAST_EXPECT(ammAlice.expectBalances(
3594 XRP(10'010), USD(10'000), ammAlice.tokens()));
3595 // Initial balance 30,000 + 10(limited by limitQuality)
3596 BEAST_EXPECT(expectLine(env, carol, USD(30'010)));
3597 // Initial balance 30,000 - 10(limited by limitQuality) - 10(tx
3598 // fee)
3599 BEAST_EXPECT(expectLedgerEntryRoot(
3600 env, bob, XRP(30'000) - XRP(10) - txfee(env, 1)));
3601
3602 // Fails because of limitQuality. Would have sent
3603 // ~98.91USD/110XRP has it not been for limitQuality.
3604 env(pay(bob, carol, USD(100)),
3605 path(~USD),
3606 sendmax(XRP(100)),
3607 txflags(
3609 ter(tecPATH_DRY));
3610 env.close();
3611 },
3612 {{XRP(10'000), USD(10'010)}},
3613 0,
3614 std::nullopt,
3615 {features});
3616
3617 // Payment with limitQuality and transfer fee set.
3618 testAMM(
3619 [&](AMM& ammAlice, Env& env) {
3620 env(rate(gw, 1.1));
3621 env.close();
3622 env.fund(jtx::XRP(30'000), bob);
3623 env.close();
3624 // Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP
3625 // would have been sent has it not been for limitQuality and
3626 // the transfer fee.
3627 env(pay(bob, carol, USD(100)),
3628 path(~USD),
3629 sendmax(XRP(110)),
3630 txflags(
3632 env.close();
3633 BEAST_EXPECT(ammAlice.expectBalances(
3634 XRP(10'010), USD(10'000), ammAlice.tokens()));
3635 // 10USD - 10% transfer fee
3636 BEAST_EXPECT(expectLine(
3637 env,
3638 carol,
3639 STAmount{USD, UINT64_C(30'009'09090909091), -11}));
3640 BEAST_EXPECT(expectLedgerEntryRoot(
3641 env, bob, XRP(30'000) - XRP(10) - txfee(env, 1)));
3642 },
3643 {{XRP(10'000), USD(10'010)}},
3644 0,
3645 std::nullopt,
3646 {features});
3647
3648 // Fail when partial payment is not set.
3649 testAMM(
3650 [&](AMM& ammAlice, Env& env) {
3651 env.fund(jtx::XRP(30'000), bob);
3652 env.close();
3653 env(pay(bob, carol, USD(100)),
3654 path(~USD),
3655 sendmax(XRP(100)),
3658 },
3659 {{XRP(10'000), USD(10'000)}},
3660 0,
3661 std::nullopt,
3662 {features});
3663
3664 // Non-default path (with AMM) has a better quality than default path.
3665 // The max possible liquidity is taken out of non-default
3666 // path ~29.9XRP/29.9EUR, ~29.9EUR/~29.99USD. The rest
3667 // is taken from the offer.
3668 {
3669 Env env(*this, features);
3670 fund(
3671 env, gw, {alice, carol}, {USD(30'000), EUR(30'000)}, Fund::All);
3672 env.close();
3673 env.fund(XRP(1'000), bob);
3674 env.close();
3675 auto ammEUR_XRP = AMM(env, alice, XRP(10'000), EUR(10'000));
3676 auto ammUSD_EUR = AMM(env, alice, EUR(10'000), USD(10'000));
3677 env(offer(alice, XRP(101), USD(100)), txflags(tfPassive));
3678 env.close();
3679 env(pay(bob, carol, USD(100)),
3680 path(~EUR, ~USD),
3681 sendmax(XRP(102)),
3683 env.close();
3684 BEAST_EXPECT(ammEUR_XRP.expectBalances(
3685 XRPAmount(10'030'082'730),
3686 STAmount(EUR, UINT64_C(9'970'007498125468), -12),
3687 ammEUR_XRP.tokens()));
3688 if (!features[fixAMMv1_1])
3689 {
3690 BEAST_EXPECT(ammUSD_EUR.expectBalances(
3691 STAmount(USD, UINT64_C(9'970'097277662122), -12),
3692 STAmount(EUR, UINT64_C(10'029'99250187452), -11),
3693 ammUSD_EUR.tokens()));
3694
3695 // fixReducedOffersV2 changes the expected results slightly.
3696 Amounts const expectedAmounts =
3697 env.closed()->rules().enabled(fixReducedOffersV2)
3698 ? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233787816), -14)}
3699 : Amounts{
3700 XRPAmount(30'201'749),
3701 STAmount(USD, UINT64_C(29'90272233787818), -14)};
3702
3703 BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}}));
3704 }
3705 else
3706 {
3707 BEAST_EXPECT(ammUSD_EUR.expectBalances(
3708 STAmount(USD, UINT64_C(9'970'097277662172), -12),
3709 STAmount(EUR, UINT64_C(10'029'99250187452), -11),
3710 ammUSD_EUR.tokens()));
3711
3712 // fixReducedOffersV2 changes the expected results slightly.
3713 Amounts const expectedAmounts =
3714 env.closed()->rules().enabled(fixReducedOffersV2)
3715 ? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233782839), -14)}
3716 : Amounts{
3717 XRPAmount(30'201'749),
3718 STAmount(USD, UINT64_C(29'90272233782840), -14)};
3719
3720 BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}}));
3721 }
3722 // Initial 30,000 + 100
3723 BEAST_EXPECT(expectLine(env, carol, STAmount{USD, 30'100}));
3724 // Initial 1,000 - 30082730(AMM pool) - 70798251(offer) - 10(tx fee)
3725 BEAST_EXPECT(expectLedgerEntryRoot(
3726 env,
3727 bob,
3728 XRP(1'000) - XRPAmount{30'082'730} - XRPAmount{70'798'251} -
3729 txfee(env, 1)));
3730 }
3731
3732 // Default path (with AMM) has a better quality than a non-default path.
3733 // The max possible liquidity is taken out of default
3734 // path ~49XRP/49USD. The rest is taken from the offer.
3735 testAMM(
3736 [&](AMM& ammAlice, Env& env) {
3737 env.fund(XRP(1'000), bob);
3738 env.close();
3739 env.trust(EUR(2'000), alice);
3740 env.close();
3741 env(pay(gw, alice, EUR(1'000)));
3742 env(offer(alice, XRP(101), EUR(100)), txflags(tfPassive));
3743 env.close();
3744 env(offer(alice, EUR(100), USD(100)), txflags(tfPassive));
3745 env.close();
3746 env(pay(bob, carol, USD(100)),
3747 path(~EUR, ~USD),
3748 sendmax(XRP(102)),
3750 env.close();
3751 BEAST_EXPECT(ammAlice.expectBalances(
3752 XRPAmount(10'050'238'637),
3753 STAmount(USD, UINT64_C(9'950'01249687578), -11),
3754 ammAlice.tokens()));
3755 BEAST_EXPECT(expectOffers(
3756 env,
3757 alice,
3758 2,
3759 {{Amounts{
3760 XRPAmount(50'487'378),
3761 STAmount(EUR, UINT64_C(49'98750312422), -11)},
3762 Amounts{
3763 STAmount(EUR, UINT64_C(49'98750312422), -11),
3764 STAmount(USD, UINT64_C(49'98750312422), -11)}}}));
3765 // Initial 30,000 + 99.99999999999
3766 BEAST_EXPECT(expectLine(
3767 env,
3768 carol,
3769 STAmount{USD, UINT64_C(30'099'99999999999), -11}));
3770 // Initial 1,000 - 50238637(AMM pool) - 50512622(offer) - 10(tx
3771 // fee)
3772 BEAST_EXPECT(expectLedgerEntryRoot(
3773 env,
3774 bob,
3775 XRP(1'000) - XRPAmount{50'238'637} - XRPAmount{50'512'622} -
3776 txfee(env, 1)));
3777 },
3778 std::nullopt,
3779 0,
3780 std::nullopt,
3781 {features});
3782
3783 // Default path with AMM and Order Book offer. AMM is consumed first,
3784 // remaining amount is consumed by the offer.
3785 testAMM(
3786 [&](AMM& ammAlice, Env& env) {
3787 fund(env, gw, {bob}, {USD(100)}, Fund::Acct);
3788 env.close();
3789 env(offer(bob, XRP(100), USD(100)), txflags(tfPassive));
3790 env.close();
3791 env(pay(alice, carol, USD(200)),
3792 sendmax(XRP(200)),
3794 env.close();
3795 if (!features[fixAMMv1_1])
3796 {
3797 BEAST_EXPECT(ammAlice.expectBalances(
3798 XRP(10'100), USD(10'000), ammAlice.tokens()));
3799 // Initial 30,000 + 200
3800 BEAST_EXPECT(expectLine(env, carol, USD(30'200)));
3801 }
3802 else
3803 {
3804 BEAST_EXPECT(ammAlice.expectBalances(
3805 XRP(10'100),
3806 STAmount(USD, UINT64_C(10'000'00000000001), -11),
3807 ammAlice.tokens()));
3808 BEAST_EXPECT(expectLine(
3809 env,
3810 carol,
3811 STAmount(USD, UINT64_C(30'199'99999999999), -11)));
3812 }
3813 // Initial 30,000 - 10000(AMM pool LP) - 100(AMM offer) -
3814 // - 100(offer) - 10(tx fee) - one reserve
3815 BEAST_EXPECT(expectLedgerEntryRoot(
3816 env,
3817 alice,
3818 XRP(30'000) - XRP(10'000) - XRP(100) - XRP(100) -
3819 ammCrtFee(env) - txfee(env, 1)));
3820 BEAST_EXPECT(expectOffers(env, bob, 0));
3821 },
3822 {{XRP(10'000), USD(10'100)}},
3823 0,
3824 std::nullopt,
3825 {features});
3826
3827 // Default path with AMM and Order Book offer.
3828 // Order Book offer is consumed first.
3829 // Remaining amount is consumed by AMM.
3830 {
3831 Env env(*this, features);
3832 fund(env, gw, {alice, bob, carol}, XRP(20'000), {USD(2'000)});
3833 env(offer(bob, XRP(50), USD(150)), txflags(tfPassive));
3834 AMM ammAlice(env, alice, XRP(1'000), USD(1'050));
3835 env(pay(alice, carol, USD(200)),
3836 sendmax(XRP(200)),
3838 BEAST_EXPECT(ammAlice.expectBalances(
3839 XRP(1'050), USD(1'000), ammAlice.tokens()));
3840 BEAST_EXPECT(expectLine(env, carol, USD(2'200)));
3841 BEAST_EXPECT(expectOffers(env, bob, 0));
3842 }
3843
3844 // Offer crossing XRP/IOU
3845 testAMM(
3846 [&](AMM& ammAlice, Env& env) {
3847 fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
3848 env.close();
3849 env(offer(bob, USD(100), XRP(100)));
3850 env.close();
3851 BEAST_EXPECT(ammAlice.expectBalances(
3852 XRP(10'100), USD(10'000), ammAlice.tokens()));
3853 // Initial 1,000 + 100
3854 BEAST_EXPECT(expectLine(env, bob, USD(1'100)));
3855 // Initial 30,000 - 100(offer) - 10(tx fee)
3856 BEAST_EXPECT(expectLedgerEntryRoot(
3857 env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
3858 BEAST_EXPECT(expectOffers(env, bob, 0));
3859 },
3860 {{XRP(10'000), USD(10'100)}},
3861 0,
3862 std::nullopt,
3863 {features});
3864
3865 // Offer crossing IOU/IOU and transfer rate
3866 // Single path AMM offer
3867 testAMM(
3868 [&](AMM& ammAlice, Env& env) {
3869 env(rate(gw, 1.25));
3870 env.close();
3871 // This offer succeeds to cross pre- and post-amendment
3872 // because the strand's out amount is small enough to match
3873 // limitQuality value and limitOut() function in StrandFlow
3874 // doesn't require an adjustment to out value.
3875 env(offer(carol, EUR(100), GBP(100)));
3876 env.close();
3877 // No transfer fee
3878 BEAST_EXPECT(ammAlice.expectBalances(
3879 GBP(1'100), EUR(1'000), ammAlice.tokens()));
3880 // Initial 30,000 - 100(offer) - 25% transfer fee
3881 BEAST_EXPECT(expectLine(env, carol, GBP(29'875)));
3882 // Initial 30,000 + 100(offer)
3883 BEAST_EXPECT(expectLine(env, carol, EUR(30'100)));
3884 BEAST_EXPECT(expectOffers(env, bob, 0));
3885 },
3886 {{GBP(1'000), EUR(1'100)}},
3887 0,
3888 std::nullopt,
3889 {features});
3890 // Single-path AMM offer
3891 testAMM(
3892 [&](AMM& amm, Env& env) {
3893 env(rate(gw, 1.001));
3894 env.close();
3895 env(offer(carol, XRP(100), USD(55)));
3896 env.close();
3897 if (!features[fixAMMv1_1])
3898 {
3899 // Pre-amendment the transfer fee is not taken into
3900 // account when calculating the limit out based on
3901 // limitQuality. Carol pays 0.1% on the takerGets, which
3902 // lowers the overall quality. AMM offer is generated based
3903 // on higher limit out, which generates a larger offer
3904 // with lower quality. Consequently, the offer fails
3905 // to cross.
3906 BEAST_EXPECT(
3907 amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
3908 BEAST_EXPECT(expectOffers(
3909 env, carol, 1, {{Amounts{XRP(100), USD(55)}}}));
3910 }
3911 else
3912 {
3913 // Post-amendment the transfer fee is taken into account
3914 // when calculating the limit out based on limitQuality.
3915 // This increases the limitQuality and decreases
3916 // the limit out. Consequently, AMM offer size is decreased,
3917 // and the quality is increased, matching the overall
3918 // quality.
3919 // AMM offer ~50USD/91XRP
3920 BEAST_EXPECT(amm.expectBalances(
3921 XRPAmount(909'090'909),
3922 STAmount{USD, UINT64_C(550'000000055), -9},
3923 amm.tokens()));
3924 // Offer ~91XRP/49.99USD
3925 BEAST_EXPECT(expectOffers(
3926 env,
3927 carol,
3928 1,
3929 {{Amounts{
3930 XRPAmount{9'090'909},
3931 STAmount{USD, 4'99999995, -8}}}}));
3932 // Carol pays 0.1% fee on ~50USD =~ 0.05USD
3933 BEAST_EXPECT(
3934 env.balance(carol, USD) ==
3935 STAmount(USD, UINT64_C(29'949'94999999494), -11));
3936 }
3937 },
3938 {{XRP(1'000), USD(500)}},
3939 0,
3940 std::nullopt,
3941 {features});
3942 testAMM(
3943 [&](AMM& amm, Env& env) {
3944 env(rate(gw, 1.001));
3945 env.close();
3946 env(offer(carol, XRP(10), USD(5.5)));
3947 env.close();
3948 if (!features[fixAMMv1_1])
3949 {
3950 BEAST_EXPECT(amm.expectBalances(
3951 XRP(990),
3952 STAmount{USD, UINT64_C(505'050505050505), -12},
3953 amm.tokens()));
3954 BEAST_EXPECT(expectOffers(env, carol, 0));
3955 }
3956 else
3957 {
3958 BEAST_EXPECT(amm.expectBalances(
3959 XRP(990),
3960 STAmount{USD, UINT64_C(505'0505050505051), -13},
3961 amm.tokens()));
3962 BEAST_EXPECT(expectOffers(env, carol, 0));
3963 }
3964 },
3965 {{XRP(1'000), USD(500)}},
3966 0,
3967 std::nullopt,
3968 {features});
3969 // Multi-path AMM offer
3970 testAMM(
3971 [&](AMM& ammAlice, Env& env) {
3972 Account const ed("ed");
3973 fund(
3974 env,
3975 gw,
3976 {bob, ed},
3977 XRP(30'000),
3978 {GBP(2'000), EUR(2'000)},
3979 Fund::Acct);
3980 env(rate(gw, 1.25));
3981 env.close();
3982 // The auto-bridge is worse quality than AMM, is not consumed
3983 // first and initially forces multi-path AMM offer generation.
3984 // Multi-path AMM offers are consumed until their quality
3985 // is less than the auto-bridge offers quality. Auto-bridge
3986 // offers are consumed afterward. Then the behavior is
3987 // different pre-amendment and post-amendment.
3988 env(offer(bob, GBP(10), XRP(10)), txflags(tfPassive));
3989 env(offer(ed, XRP(10), EUR(10)), txflags(tfPassive));
3990 env.close();
3991 env(offer(carol, EUR(100), GBP(100)));
3992 env.close();
3993 if (!features[fixAMMv1_1])
3994 {
3995 // After the auto-bridge offers are consumed, single path
3996 // AMM offer is generated with the limit out not taking
3997 // into consideration the transfer fee. This results
3998 // in an overall lower quality offer than the limit quality
3999 // and the single path AMM offer fails to consume.
4000 // Total consumed ~37.06GBP/39.32EUR
4001 BEAST_EXPECT(ammAlice.expectBalances(
4002 STAmount{GBP, UINT64_C(1'037'06583722133), -11},
4003 STAmount{EUR, UINT64_C(1'060'684828792831), -12},
4004 ammAlice.tokens()));
4005 // Consumed offer ~49.32EUR/49.32GBP
4006 BEAST_EXPECT(expectOffers(
4007 env,
4008 carol,
4009 1,
4010 {Amounts{
4011 STAmount{EUR, UINT64_C(50'684828792831), -12},
4012 STAmount{GBP, UINT64_C(50'684828792831), -12}}}));
4013 BEAST_EXPECT(expectOffers(env, bob, 0));
4014 BEAST_EXPECT(expectOffers(env, ed, 0));
4015
4016 // Initial 30,000 - ~47.06(offers = 37.06(AMM) + 10(LOB))
4017 // * 1.25
4018 // = 58.825 = ~29941.17
4019 // carol bought ~72.93EUR at the cost of ~70.68GBP
4020 // the offer is partially consumed
4021 BEAST_EXPECT(expectLine(
4022 env,
4023 carol,
4024 STAmount{GBP, UINT64_C(29'941'16770347333), -11}));
4025 // Initial 30,000 + ~49.3(offers = 39.3(AMM) + 10(LOB))
4026 BEAST_EXPECT(expectLine(
4027 env,
4028 carol,
4029 STAmount{EUR, UINT64_C(30'049'31517120716), -11}));
4030 }
4031 else
4032 {
4033 // After the auto-bridge offers are consumed, single path
4034 // AMM offer is generated with the limit out taking
4035 // into consideration the transfer fee. This results
4036 // in an overall quality offer matching the limit quality
4037 // and the single path AMM offer is consumed. More
4038 // liquidity is consumed overall in post-amendment.
4039 // Total consumed ~60.68GBP/62.93EUR
4040 BEAST_EXPECT(ammAlice.expectBalances(
4041 STAmount{GBP, UINT64_C(1'060'684828792832), -12},
4042 STAmount{EUR, UINT64_C(1'037'06583722134), -11},
4043 ammAlice.tokens()));
4044 // Consumed offer ~72.93EUR/72.93GBP
4045 BEAST_EXPECT(expectOffers(
4046 env,
4047 carol,
4048 1,
4049 {Amounts{
4050 STAmount{EUR, UINT64_C(27'06583722134028), -14},
4051 STAmount{GBP, UINT64_C(27'06583722134028), -14}}}));
4052 BEAST_EXPECT(expectOffers(env, bob, 0));
4053 BEAST_EXPECT(expectOffers(env, ed, 0));
4054
4055 // Initial 30,000 - ~70.68(offers = 60.68(AMM) + 10(LOB))
4056 // * 1.25
4057 // = 88.35 = ~29911.64
4058 // carol bought ~72.93EUR at the cost of ~70.68GBP
4059 // the offer is partially consumed
4060 BEAST_EXPECT(expectLine(
4061 env,
4062 carol,
4063 STAmount{GBP, UINT64_C(29'911'64396400896), -11}));
4064 // Initial 30,000 + ~72.93(offers = 62.93(AMM) + 10(LOB))
4065 BEAST_EXPECT(expectLine(
4066 env,
4067 carol,
4068 STAmount{EUR, UINT64_C(30'072'93416277865), -11}));
4069 }
4070 // Initial 2000 + 10 = 2010
4071 BEAST_EXPECT(expectLine(env, bob, GBP(2'010)));
4072 // Initial 2000 - 10 * 1.25 = 1987.5
4073 BEAST_EXPECT(expectLine(env, ed, EUR(1'987.5)));
4074 },
4075 {{GBP(1'000), EUR(1'100)}},
4076 0,
4077 std::nullopt,
4078 {features});
4079
4080 // Payment and transfer fee
4081 // Scenario:
4082 // Bob sends 125GBP to pay 80EUR to Carol
4083 // Payment execution:
4084 // bob's 125GBP/1.25 = 100GBP
4085 // 100GBP/100EUR AMM offer
4086 // 100EUR/1.25 = 80EUR paid to carol
4087 testAMM(
4088 [&](AMM& ammAlice, Env& env) {
4089 fund(env, gw, {bob}, {GBP(200), EUR(200)}, Fund::Acct);
4090 env(rate(gw, 1.25));
4091 env.close();
4092 env(pay(bob, carol, EUR(100)),
4093 path(~EUR),
4094 sendmax(GBP(125)),
4096 env.close();
4097 BEAST_EXPECT(ammAlice.expectBalances(
4098 GBP(1'100), EUR(1'000), ammAlice.tokens()));
4099 BEAST_EXPECT(expectLine(env, bob, GBP(75)));
4100 BEAST_EXPECT(expectLine(env, carol, EUR(30'080)));
4101 },
4102 {{GBP(1'000), EUR(1'100)}},
4103 0,
4104 std::nullopt,
4105 {features});
4106
4107 // Payment and transfer fee, multiple steps
4108 // Scenario:
4109 // Dan's offer 200CAN/200GBP
4110 // AMM 1000GBP/10125EUR
4111 // Ed's offer 200EUR/200USD
4112 // Bob sends 195.3125CAN to pay 100USD to Carol
4113 // Payment execution:
4114 // bob's 195.3125CAN/1.25 = 156.25CAN -> dan's offer
4115 // 156.25CAN/156.25GBP 156.25GBP/1.25 = 125GBP -> AMM's offer
4116 // 125GBP/125EUR 125EUR/1.25 = 100EUR -> ed's offer
4117 // 100EUR/100USD 100USD/1.25 = 80USD paid to carol
4118 testAMM(
4119 [&](AMM& ammAlice, Env& env) {
4120 Account const dan("dan");
4121 Account const ed("ed");
4122 auto const CAN = gw["CAN"];
4123 fund(env, gw, {dan}, {CAN(200), GBP(200)}, Fund::Acct);
4124 fund(env, gw, {ed}, {EUR(200), USD(200)}, Fund::Acct);
4125 fund(env, gw, {bob}, {CAN(195.3125)}, Fund::Acct);
4126 env(trust(carol, USD(100)));
4127 env(rate(gw, 1.25));
4128 env.close();
4129 env(offer(dan, CAN(200), GBP(200)));
4130 env(offer(ed, EUR(200), USD(200)));
4131 env.close();
4132 env(pay(bob, carol, USD(100)),
4133 path(~GBP, ~EUR, ~USD),
4134 sendmax(CAN(195.3125)),
4136 env.close();
4137 BEAST_EXPECT(expectLine(env, bob, CAN(0)));
4138 BEAST_EXPECT(expectLine(env, dan, CAN(356.25), GBP(43.75)));
4139 BEAST_EXPECT(ammAlice.expectBalances(
4140 GBP(10'125), EUR(10'000), ammAlice.tokens()));
4141 BEAST_EXPECT(expectLine(env, ed, EUR(300), USD(100)));
4142 BEAST_EXPECT(expectLine(env, carol, USD(80)));
4143 },
4144 {{GBP(10'000), EUR(10'125)}},
4145 0,
4146 std::nullopt,
4147 {features});
4148
4149 // Pay amounts close to one side of the pool
4150 testAMM(
4151 [&](AMM& ammAlice, Env& env) {
4152 env(pay(alice, carol, USD(99.99)),
4153 path(~USD),
4154 sendmax(XRP(1)),
4156 ter(tesSUCCESS));
4157 env(pay(alice, carol, USD(100)),
4158 path(~USD),
4159 sendmax(XRP(1)),
4161 ter(tesSUCCESS));
4162 env(pay(alice, carol, XRP(100)),
4163 path(~XRP),
4164 sendmax(USD(1)),
4166 ter(tesSUCCESS));
4167 env(pay(alice, carol, STAmount{xrpIssue(), 99'999'900}),
4168 path(~XRP),
4169 sendmax(USD(1)),
4171 ter(tesSUCCESS));
4172 },
4173 {{XRP(100), USD(100)}},
4174 0,
4175 std::nullopt,
4176 {features});
4177
4178 // Multiple paths/steps
4179 {
4180 Env env(*this, features);
4181 auto const ETH = gw["ETH"];
4182 fund(
4183 env,
4184 gw,
4185 {alice},
4186 XRP(100'000),
4187 {EUR(50'000), BTC(50'000), ETH(50'000), USD(50'000)});
4188 fund(env, gw, {carol, bob}, XRP(1'000), {USD(200)}, Fund::Acct);
4189 AMM xrp_eur(env, alice, XRP(10'100), EUR(10'000));
4190 AMM eur_btc(env, alice, EUR(10'000), BTC(10'200));
4191 AMM btc_usd(env, alice, BTC(10'100), USD(10'000));
4192 AMM xrp_usd(env, alice, XRP(10'150), USD(10'200));
4193 AMM xrp_eth(env, alice, XRP(10'000), ETH(10'100));
4194 AMM eth_eur(env, alice, ETH(10'900), EUR(11'000));
4195 AMM eur_usd(env, alice, EUR(10'100), USD(10'000));
4196 env(pay(bob, carol, USD(100)),
4197 path(~EUR, ~BTC, ~USD),
4198 path(~USD),
4199 path(~ETH, ~EUR, ~USD),
4200 sendmax(XRP(200)));
4201 if (!features[fixAMMv1_1])
4202 {
4203 // XRP-ETH-EUR-USD
4204 // This path provides ~26.06USD/26.2XRP
4205 BEAST_EXPECT(xrp_eth.expectBalances(
4206 XRPAmount(10'026'208'900),
4207 STAmount{ETH, UINT64_C(10'073'65779244494), -11},
4208 xrp_eth.tokens()));
4209 BEAST_EXPECT(eth_eur.expectBalances(
4210 STAmount{ETH, UINT64_C(10'926'34220755506), -11},
4211 STAmount{EUR, UINT64_C(10'973'54232078752), -11},
4212 eth_eur.tokens()));
4213 BEAST_EXPECT(eur_usd.expectBalances(
4214 STAmount{EUR, UINT64_C(10'126'45767921248), -11},
4215 STAmount{USD, UINT64_C(9'973'93151712086), -11},
4216 eur_usd.tokens()));
4217 // XRP-USD path
4218 // This path provides ~73.9USD/74.1XRP
4219 BEAST_EXPECT(xrp_usd.expectBalances(
4220 XRPAmount(10'224'106'246),
4221 STAmount{USD, UINT64_C(10'126'06848287914), -11},
4222 xrp_usd.tokens()));
4223 }
4224 else
4225 {
4226 BEAST_EXPECT(xrp_eth.expectBalances(
4227 XRPAmount(10'026'208'900),
4228 STAmount{ETH, UINT64_C(10'073'65779244461), -11},
4229 xrp_eth.tokens()));
4230 BEAST_EXPECT(eth_eur.expectBalances(
4231 STAmount{ETH, UINT64_C(10'926'34220755539), -11},
4232 STAmount{EUR, UINT64_C(10'973'5423207872), -10},
4233 eth_eur.tokens()));
4234 BEAST_EXPECT(eur_usd.expectBalances(
4235 STAmount{EUR, UINT64_C(10'126'4576792128), -10},
4236 STAmount{USD, UINT64_C(9'973'93151712057), -11},
4237 eur_usd.tokens()));
4238 // XRP-USD path
4239 // This path provides ~73.9USD/74.1XRP
4240 BEAST_EXPECT(xrp_usd.expectBalances(
4241 XRPAmount(10'224'106'246),
4242 STAmount{USD, UINT64_C(10'126'06848287943), -11},
4243 xrp_usd.tokens()));
4244 }
4245
4246 // XRP-EUR-BTC-USD
4247 // This path doesn't provide any liquidity due to how
4248 // offers are generated in multi-path. Analytical solution
4249 // shows a different distribution:
4250 // XRP-EUR-BTC-USD 11.6USD/11.64XRP, XRP-USD 60.7USD/60.8XRP,
4251 // XRP-ETH-EUR-USD 27.6USD/27.6XRP
4252 BEAST_EXPECT(xrp_eur.expectBalances(
4253 XRP(10'100), EUR(10'000), xrp_eur.tokens()));
4254 BEAST_EXPECT(eur_btc.expectBalances(
4255 EUR(10'000), BTC(10'200), eur_btc.tokens()));
4256 BEAST_EXPECT(btc_usd.expectBalances(
4257 BTC(10'100), USD(10'000), btc_usd.tokens()));
4258
4259 BEAST_EXPECT(expectLine(env, carol, USD(300)));
4260 }
4261
4262 // Dependent AMM
4263 {
4264 Env env(*this, features);
4265 auto const ETH = gw["ETH"];
4266 fund(
4267 env,
4268 gw,
4269 {alice},
4270 XRP(40'000),
4271 {EUR(50'000), BTC(50'000), ETH(50'000), USD(50'000)});
4272 fund(env, gw, {carol, bob}, XRP(1000), {USD(200)}, Fund::Acct);
4273 AMM xrp_eur(env, alice, XRP(10'100), EUR(10'000));
4274 AMM eur_btc(env, alice, EUR(10'000), BTC(10'200));
4275 AMM btc_usd(env, alice, BTC(10'100), USD(10'000));
4276 AMM xrp_eth(env, alice, XRP(10'000), ETH(10'100));
4277 AMM eth_eur(env, alice, ETH(10'900), EUR(11'000));
4278 env(pay(bob, carol, USD(100)),
4279 path(~EUR, ~BTC, ~USD),
4280 path(~ETH, ~EUR, ~BTC, ~USD),
4281 sendmax(XRP(200)));
4282 if (!features[fixAMMv1_1])
4283 {
4284 // XRP-EUR-BTC-USD path provides ~17.8USD/~18.7XRP
4285 // XRP-ETH-EUR-BTC-USD path provides ~82.2USD/82.4XRP
4286 BEAST_EXPECT(xrp_eur.expectBalances(
4287 XRPAmount(10'118'738'472),
4288 STAmount{EUR, UINT64_C(9'981'544436337968), -12},
4289 xrp_eur.tokens()));
4290 BEAST_EXPECT(eur_btc.expectBalances(
4291 STAmount{EUR, UINT64_C(10'101'16096785173), -11},
4292 STAmount{BTC, UINT64_C(10'097'91426968066), -11},
4293 eur_btc.tokens()));
4294 BEAST_EXPECT(btc_usd.expectBalances(
4295 STAmount{BTC, UINT64_C(10'202'08573031934), -11},
4296 USD(9'900),
4297 btc_usd.tokens()));
4298 BEAST_EXPECT(xrp_eth.expectBalances(
4299 XRPAmount(10'082'446'397),
4300 STAmount{ETH, UINT64_C(10'017'41072778012), -11},
4301 xrp_eth.tokens()));
4302 BEAST_EXPECT(eth_eur.expectBalances(
4303 STAmount{ETH, UINT64_C(10'982'58927221988), -11},
4304 STAmount{EUR, UINT64_C(10'917'2945958103), -10},
4305 eth_eur.tokens()));
4306 }
4307 else
4308 {
4309 BEAST_EXPECT(xrp_eur.expectBalances(
4310 XRPAmount(10'118'738'472),
4311 STAmount{EUR, UINT64_C(9'981'544436337923), -12},
4312 xrp_eur.tokens()));
4313 BEAST_EXPECT(eur_btc.expectBalances(
4314 STAmount{EUR, UINT64_C(10'101'16096785188), -11},
4315 STAmount{BTC, UINT64_C(10'097'91426968059), -11},
4316 eur_btc.tokens()));
4317 BEAST_EXPECT(btc_usd.expectBalances(
4318 STAmount{BTC, UINT64_C(10'202'08573031941), -11},
4319 USD(9'900),
4320 btc_usd.tokens()));
4321 BEAST_EXPECT(xrp_eth.expectBalances(
4322 XRPAmount(10'082'446'397),
4323 STAmount{ETH, UINT64_C(10'017'41072777996), -11},
4324 xrp_eth.tokens()));
4325 BEAST_EXPECT(eth_eur.expectBalances(
4326 STAmount{ETH, UINT64_C(10'982'58927222004), -11},
4327 STAmount{EUR, UINT64_C(10'917'2945958102), -10},
4328 eth_eur.tokens()));
4329 }
4330 BEAST_EXPECT(expectLine(env, carol, USD(300)));
4331 }
4332
4333 // AMM offers limit
4334 // Consuming 30 CLOB offers, results in hitting 30 AMM offers limit.
4335 testAMM(
4336 [&](AMM& ammAlice, Env& env) {
4337 env.fund(XRP(1'000), bob);
4338 fund(env, gw, {bob}, {EUR(400)}, Fund::IOUOnly);
4339 env(trust(alice, EUR(200)));
4340 for (int i = 0; i < 30; ++i)
4341 env(offer(alice, EUR(1.0 + 0.01 * i), XRP(1)));
4342 // This is worse quality offer than 30 offers above.
4343 // It will not be consumed because of AMM offers limit.
4344 env(offer(alice, EUR(140), XRP(100)));
4345 env(pay(bob, carol, USD(100)),
4346 path(~XRP, ~USD),
4347 sendmax(EUR(400)),
4349 if (!features[fixAMMv1_1])
4350 {
4351 // Carol gets ~29.91USD because of the AMM offers limit
4352 BEAST_EXPECT(ammAlice.expectBalances(
4353 XRP(10'030),
4354 STAmount{USD, UINT64_C(9'970'089730807577), -12},
4355 ammAlice.tokens()));
4356 BEAST_EXPECT(expectLine(
4357 env,
4358 carol,
4359 STAmount{USD, UINT64_C(30'029'91026919241), -11}));
4360 }
4361 else
4362 {
4363 BEAST_EXPECT(ammAlice.expectBalances(
4364 XRP(10'030),
4365 STAmount{USD, UINT64_C(9'970'089730807827), -12},
4366 ammAlice.tokens()));
4367 BEAST_EXPECT(expectLine(
4368 env,
4369 carol,
4370 STAmount{USD, UINT64_C(30'029'91026919217), -11}));
4371 }
4372 BEAST_EXPECT(
4373 expectOffers(env, alice, 1, {{{EUR(140), XRP(100)}}}));
4374 },
4375 std::nullopt,
4376 0,
4377 std::nullopt,
4378 {features});
4379 // This payment is fulfilled
4380 testAMM(
4381 [&](AMM& ammAlice, Env& env) {
4382 env.fund(XRP(1'000), bob);
4383 fund(env, gw, {bob}, {EUR(400)}, Fund::IOUOnly);
4384 env(trust(alice, EUR(200)));
4385 for (int i = 0; i < 29; ++i)
4386 env(offer(alice, EUR(1.0 + 0.01 * i), XRP(1)));
4387 // This is worse quality offer than 30 offers above.
4388 // It will not be consumed because of AMM offers limit.
4389 env(offer(alice, EUR(140), XRP(100)));
4390 env(pay(bob, carol, USD(100)),
4391 path(~XRP, ~USD),
4392 sendmax(EUR(400)),
4394 BEAST_EXPECT(ammAlice.expectBalances(
4395 XRPAmount{10'101'010'102}, USD(9'900), ammAlice.tokens()));
4396 if (!features[fixAMMv1_1])
4397 {
4398 // Carol gets ~100USD
4399 BEAST_EXPECT(expectLine(
4400 env,
4401 carol,
4402 STAmount{USD, UINT64_C(30'099'99999999999), -11}));
4403 }
4404 else
4405 {
4406 BEAST_EXPECT(expectLine(env, carol, USD(30'100)));
4407 }
4408 BEAST_EXPECT(expectOffers(
4409 env,
4410 alice,
4411 1,
4412 {{{STAmount{EUR, UINT64_C(39'1858572), -7},
4413 XRPAmount{27'989'898}}}}));
4414 },
4415 std::nullopt,
4416 0,
4417 std::nullopt,
4418 {features});
4419
4420 // Offer crossing with AMM and another offer. AMM has a better
4421 // quality and is consumed first.
4422 {
4423 Env env(*this, features);
4424 fund(env, gw, {alice, carol, bob}, XRP(30'000), {USD(30'000)});
4425 env(offer(bob, XRP(100), USD(100.001)));
4426 AMM ammAlice(env, alice, XRP(10'000), USD(10'100));
4427 env(offer(carol, USD(100), XRP(100)));
4428 if (!features[fixAMMv1_1])
4429 {
4430 BEAST_EXPECT(ammAlice.expectBalances(
4431 XRPAmount{10'049'825'373},
4432 STAmount{USD, UINT64_C(10'049'92586949302), -11},
4433 ammAlice.tokens()));
4434 BEAST_EXPECT(expectOffers(
4435 env,
4436 bob,
4437 1,
4438 {{{XRPAmount{50'074'629},
4439 STAmount{USD, UINT64_C(50'07513050698), -11}}}}));
4440 }
4441 else
4442 {
4443 BEAST_EXPECT(ammAlice.expectBalances(
4444 XRPAmount{10'049'825'372},
4445 STAmount{USD, UINT64_C(10'049'92587049303), -11},
4446 ammAlice.tokens()));
4447 BEAST_EXPECT(expectOffers(
4448 env,
4449 bob,
4450 1,
4451 {{{XRPAmount{50'074'628},
4452 STAmount{USD, UINT64_C(50'07512950697), -11}}}}));
4453 BEAST_EXPECT(expectLine(env, carol, USD(30'100)));
4454 }
4455 }
4456
4457 // Individually frozen account
4458 testAMM(
4459 [&](AMM& ammAlice, Env& env) {
4460 env(trust(gw, carol["USD"](0), tfSetFreeze));
4461 env(trust(gw, alice["USD"](0), tfSetFreeze));
4462 env.close();
4463 env(pay(alice, carol, USD(1)),
4464 path(~USD),
4465 sendmax(XRP(10)),
4467 ter(tesSUCCESS));
4468 },
4469 std::nullopt,
4470 0,
4471 std::nullopt,
4472 {features});
4473 }
4474
4475 void
4477 {
4478 testcase("AMM Tokens");
4479 using namespace jtx;
4480
4481 // Offer crossing with AMM LPTokens and XRP.
4482 testAMM([&](AMM& ammAlice, Env& env) {
4483 auto const token1 = ammAlice.lptIssue();
4484 auto priceXRP = withdrawByTokens(
4485 STAmount{XRPAmount{10'000'000'000}},
4486 STAmount{token1, 10'000'000},
4487 STAmount{token1, 5'000'000},
4488 0);
4489 // Carol places an order to buy LPTokens
4490 env(offer(carol, STAmount{token1, 5'000'000}, priceXRP));
4491 // Alice places an order to sell LPTokens
4492 env(offer(alice, priceXRP, STAmount{token1, 5'000'000}));
4493 // Pool's LPTokens balance doesn't change
4494 BEAST_EXPECT(ammAlice.expectBalances(
4495 XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
4496 // Carol is Liquidity Provider
4497 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{5'000'000}));
4498 BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{5'000'000}));
4499 // Carol votes
4500 ammAlice.vote(carol, 1'000);
4501 BEAST_EXPECT(ammAlice.expectTradingFee(500));
4502 ammAlice.vote(carol, 0);
4503 BEAST_EXPECT(ammAlice.expectTradingFee(0));
4504 // Carol bids
4505 env(ammAlice.bid({.account = carol, .bidMin = 100}));
4506 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4'999'900}));
4507 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{100}));
4508 BEAST_EXPECT(accountBalance(env, carol) == "22499999960");
4509 priceXRP = withdrawByTokens(
4510 STAmount{XRPAmount{10'000'000'000}},
4511 STAmount{token1, 9'999'900},
4512 STAmount{token1, 4'999'900},
4513 0);
4514 // Carol withdraws
4515 ammAlice.withdrawAll(carol, XRP(0));
4516 BEAST_EXPECT(accountBalance(env, carol) == "29999949949");
4517 BEAST_EXPECT(ammAlice.expectBalances(
4518 XRPAmount{10'000'000'000} - priceXRP,
4519 USD(10'000),
4520 IOUAmount{5'000'000}));
4521 BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{5'000'000}));
4522 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
4523 });
4524
4525 // Offer crossing with two AMM LPTokens.
4526 testAMM([&](AMM& ammAlice, Env& env) {
4527 ammAlice.deposit(carol, 1'000'000);
4528 fund(env, gw, {alice, carol}, {EUR(10'000)}, Fund::IOUOnly);
4529 AMM ammAlice1(env, alice, XRP(10'000), EUR(10'000));
4530 ammAlice1.deposit(carol, 1'000'000);
4531 auto const token1 = ammAlice.lptIssue();
4532 auto const token2 = ammAlice1.lptIssue();
4533 env(offer(alice, STAmount{token1, 100}, STAmount{token2, 100}),
4535 env.close();
4536 BEAST_EXPECT(expectOffers(env, alice, 1));
4537 env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100}));
4538 env.close();
4539 BEAST_EXPECT(
4540 expectLine(env, alice, STAmount{token1, 10'000'100}) &&
4541 expectLine(env, alice, STAmount{token2, 9'999'900}));
4542 BEAST_EXPECT(
4543 expectLine(env, carol, STAmount{token2, 1'000'100}) &&
4544 expectLine(env, carol, STAmount{token1, 999'900}));
4545 BEAST_EXPECT(
4546 expectOffers(env, alice, 0) && expectOffers(env, carol, 0));
4547 });
4548
4549 // LPs pay LPTokens directly. Must trust set because the trust line
4550 // is checked for the limit, which is 0 in the AMM auto-created
4551 // trust line.
4552 testAMM([&](AMM& ammAlice, Env& env) {
4553 auto const token1 = ammAlice.lptIssue();
4554 env.trust(STAmount{token1, 2'000'000}, carol);
4555 env.close();
4556 ammAlice.deposit(carol, 1'000'000);
4557 BEAST_EXPECT(
4558 ammAlice.expectLPTokens(alice, IOUAmount{10'000'000, 0}) &&
4559 ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
4560 // Pool balance doesn't change, only tokens moved from
4561 // one line to another.
4562 env(pay(alice, carol, STAmount{token1, 100}));
4563 env.close();
4564 BEAST_EXPECT(
4565 // Alice initial token1 10,000,000 - 100
4566 ammAlice.expectLPTokens(alice, IOUAmount{9'999'900, 0}) &&
4567 // Carol initial token1 1,000,000 + 100
4568 ammAlice.expectLPTokens(carol, IOUAmount{1'000'100, 0}));
4569
4570 env.trust(STAmount{token1, 20'000'000}, alice);
4571 env.close();
4572 env(pay(carol, alice, STAmount{token1, 100}));
4573 env.close();
4574 // Back to the original balance
4575 BEAST_EXPECT(
4576 ammAlice.expectLPTokens(alice, IOUAmount{10'000'000, 0}) &&
4577 ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
4578 });
4579 }
4580
4581 void
4583 {
4584 testcase("Amendment");
4585 using namespace jtx;
4587 FeatureBitset const noAMM{all - featureAMM};
4588 FeatureBitset const noNumber{all - fixUniversalNumber};
4589 FeatureBitset const noAMMAndNumber{
4590 all - featureAMM - fixUniversalNumber};
4591
4592 for (auto const& feature : {noAMM, noNumber, noAMMAndNumber})
4593 {
4594 Env env{*this, feature};
4595 fund(env, gw, {alice}, {USD(1'000)}, Fund::All);
4596 AMM amm(env, alice, XRP(1'000), USD(1'000), ter(temDISABLED));
4597
4598 env(amm.bid({.bidMax = 1000}), ter(temMALFORMED));
4599 env(amm.bid({}), ter(temDISABLED));
4600 amm.vote(VoteArg{.tfee = 100, .err = ter(temDISABLED)});
4601 amm.withdraw(WithdrawArg{.tokens = 100, .err = ter(temMALFORMED)});
4602 amm.withdraw(WithdrawArg{.err = ter(temDISABLED)});
4603 amm.deposit(
4604 DepositArg{.asset1In = USD(100), .err = ter(temDISABLED)});
4605 amm.ammDelete(alice, ter(temDISABLED));
4606 }
4607 }
4608
4609 void
4611 {
4612 testcase("Flags");
4613 using namespace jtx;
4614
4615 testAMM([&](AMM& ammAlice, Env& env) {
4616 auto const info = env.rpc(
4617 "json",
4618 "account_info",
4620 "{\"account\": \"" + to_string(ammAlice.ammAccount()) +
4621 "\"}"));
4622 auto const flags =
4623 info[jss::result][jss::account_data][jss::Flags].asUInt();
4624 BEAST_EXPECT(
4625 flags ==
4627 });
4628 }
4629
4630 void
4632 {
4633 testcase("Rippling");
4634 using namespace jtx;
4635
4636 // Rippling via AMM fails because AMM trust line has 0 limit.
4637 // Set up two issuers, A and B. Have each issue a token called TST.
4638 // Have another account C hold TST from both issuers,
4639 // and create an AMM for this pair.
4640 // Have a fourth account, D, create a trust line to the AMM for TST.
4641 // Send a payment delivering TST.AMM from C to D, using SendMax in
4642 // TST.A (or B) and a path through the AMM account. By normal
4643 // rippling rules, this would have caused the AMM's balances
4644 // to shift at a 1:1 rate with no fee applied has it not been
4645 // for 0 limit.
4646 {
4647 Env env(*this);
4648 auto const A = Account("A");
4649 auto const B = Account("B");
4650 auto const TSTA = A["TST"];
4651 auto const TSTB = B["TST"];
4652 auto const C = Account("C");
4653 auto const D = Account("D");
4654
4655 env.fund(XRP(10'000), A);
4656 env.fund(XRP(10'000), B);
4657 env.fund(XRP(10'000), C);
4658 env.fund(XRP(10'000), D);
4659
4660 env.trust(TSTA(10'000), C);
4661 env.trust(TSTB(10'000), C);
4662 env(pay(A, C, TSTA(10'000)));
4663 env(pay(B, C, TSTB(10'000)));
4664 AMM amm(env, C, TSTA(5'000), TSTB(5'000));
4665 auto const ammIss = Issue(TSTA.currency, amm.ammAccount());
4666
4667 // Can SetTrust only for AMM LP tokens
4668 env(trust(D, STAmount{ammIss, 10'000}), ter(tecNO_PERMISSION));
4669 env.close();
4670
4671 // The payment would fail because of above, but check just in case
4672 env(pay(C, D, STAmount{ammIss, 10}),
4673 sendmax(TSTA(100)),
4674 path(amm.ammAccount()),
4676 ter(tecPATH_DRY));
4677 }
4678 }
4679
4680 void
4682 {
4683 testcase("AMMAndCLOB, offer quality change");
4684 using namespace jtx;
4685 auto const gw = Account("gw");
4686 auto const TST = gw["TST"];
4687 auto const LP1 = Account("LP1");
4688 auto const LP2 = Account("LP2");
4689
4690 auto prep = [&](auto const& offerCb, auto const& expectCb) {
4691 Env env(*this, features);
4692 env.fund(XRP(30'000'000'000), gw);
4693 env(offer(gw, XRP(11'500'000'000), TST(1'000'000'000)));
4694
4695 env.fund(XRP(10'000), LP1);
4696 env.fund(XRP(10'000), LP2);
4697 env(offer(LP1, TST(25), XRPAmount(287'500'000)));
4698
4699 // Either AMM or CLOB offer
4700 offerCb(env);
4701
4702 env(offer(LP2, TST(25), XRPAmount(287'500'000)));
4703
4704 expectCb(env);
4705 };
4706
4707 // If we replace AMM with an equivalent CLOB offer, which AMM generates
4708 // when it is consumed, then the result must be equivalent, too.
4709 std::string lp2TSTBalance;
4710 std::string lp2TakerGets;
4711 std::string lp2TakerPays;
4712 // Execute with AMM first
4713 prep(
4714 [&](Env& env) { AMM amm(env, LP1, TST(25), XRP(250)); },
4715 [&](Env& env) {
4716 lp2TSTBalance =
4717 getAccountLines(env, LP2, TST)["lines"][0u]["balance"]
4718 .asString();
4719 auto const offer = getAccountOffers(env, LP2)["offers"][0u];
4720 lp2TakerGets = offer["taker_gets"].asString();
4721 lp2TakerPays = offer["taker_pays"]["value"].asString();
4722 });
4723 // Execute with CLOB offer
4724 prep(
4725 [&](Env& env) {
4726 if (!features[fixAMMv1_1])
4727 env(offer(
4728 LP1,
4729 XRPAmount{18'095'133},
4730 STAmount{TST, UINT64_C(1'68737984885388), -14}),
4732 else
4733 env(offer(
4734 LP1,
4735 XRPAmount{18'095'132},
4736 STAmount{TST, UINT64_C(1'68737976189735), -14}),
4738 },
4739 [&](Env& env) {
4740 BEAST_EXPECT(
4741 lp2TSTBalance ==
4742 getAccountLines(env, LP2, TST)["lines"][0u]["balance"]
4743 .asString());
4744 auto const offer = getAccountOffers(env, LP2)["offers"][0u];
4745 BEAST_EXPECT(lp2TakerGets == offer["taker_gets"].asString());
4746 BEAST_EXPECT(
4747 lp2TakerPays == offer["taker_pays"]["value"].asString());
4748 });
4749 }
4750
4751 void
4753 {
4754 testcase("Trading Fee");
4755 using namespace jtx;
4756
4757 // Single Deposit, 1% fee
4758 testAMM(
4759 [&](AMM& ammAlice, Env& env) {
4760 // No fee
4761 ammAlice.deposit(carol, USD(3'000));
4762 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000}));
4763 ammAlice.withdrawAll(carol, USD(3'000));
4764 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
4765 BEAST_EXPECT(expectLine(env, carol, USD(30'000)));
4766 // Set fee to 1%
4767 ammAlice.vote(alice, 1'000);
4768 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
4769 // Carol gets fewer LPToken ~994, because of the single deposit
4770 // fee
4771 ammAlice.deposit(carol, USD(3'000));
4772 BEAST_EXPECT(ammAlice.expectLPTokens(
4773 carol, IOUAmount{994'981155689671, -12}));
4774 BEAST_EXPECT(expectLine(env, carol, USD(27'000)));
4775 // Set fee to 0
4776 ammAlice.vote(alice, 0);
4777 ammAlice.withdrawAll(carol, USD(0));
4778 // Carol gets back less than the original deposit
4779 BEAST_EXPECT(expectLine(
4780 env,
4781 carol,
4782 STAmount{USD, UINT64_C(29'994'96220068281), -11}));
4783 },
4784 {{USD(1'000), EUR(1'000)}},
4785 0,
4786 std::nullopt,
4787 {features});
4788
4789 // Single deposit with EP not exceeding specified:
4790 // 100USD with EP not to exceed 0.1 (AssetIn/TokensOut). 1% fee.
4791 testAMM(
4792 [&](AMM& ammAlice, Env& env) {
4793 auto const balance = env.balance(carol, USD);
4794 auto tokensFee = ammAlice.deposit(
4795 carol, USD(1'000), std::nullopt, STAmount{USD, 1, -1});
4796 auto const deposit = balance - env.balance(carol, USD);
4797 ammAlice.withdrawAll(carol, USD(0));
4798 ammAlice.vote(alice, 0);
4799 BEAST_EXPECT(ammAlice.expectTradingFee(0));
4800 auto const tokensNoFee = ammAlice.deposit(carol, deposit);
4801 // carol pays ~2008 LPTokens in fees or ~0.5% of the no-fee
4802 // LPTokens
4803 BEAST_EXPECT(tokensFee == IOUAmount(485'636'0611129, -7));
4804 BEAST_EXPECT(tokensNoFee == IOUAmount(487'644'85901109, -8));
4805 },
4806 std::nullopt,
4807 1'000,
4808 std::nullopt,
4809 {features});
4810
4811 // Single deposit with EP not exceeding specified:
4812 // 200USD with EP not to exceed 0.002020 (AssetIn/TokensOut). 1% fee
4813 testAMM(
4814 [&](AMM& ammAlice, Env& env) {
4815 auto const balance = env.balance(carol, USD);
4816 auto const tokensFee = ammAlice.deposit(
4817 carol, USD(200), std::nullopt, STAmount{USD, 2020, -6});
4818 auto const deposit = balance - env.balance(carol, USD);
4819 ammAlice.withdrawAll(carol, USD(0));
4820 ammAlice.vote(alice, 0);
4821 BEAST_EXPECT(ammAlice.expectTradingFee(0));
4822 auto const tokensNoFee = ammAlice.deposit(carol, deposit);
4823 // carol pays ~475 LPTokens in fees or ~0.5% of the no-fee
4824 // LPTokens
4825 BEAST_EXPECT(tokensFee == IOUAmount(98'000'00000002, -8));
4826 BEAST_EXPECT(tokensNoFee == IOUAmount(98'475'81871545, -8));
4827 },
4828 std::nullopt,
4829 1'000,
4830 std::nullopt,
4831 {features});
4832
4833 // Single Withdrawal, 1% fee
4834 testAMM(
4835 [&](AMM& ammAlice, Env& env) {
4836 // No fee
4837 ammAlice.deposit(carol, USD(3'000));
4838
4839 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000}));
4840 BEAST_EXPECT(expectLine(env, carol, USD(27'000)));
4841 // Set fee to 1%
4842 ammAlice.vote(alice, 1'000);
4843 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
4844 // Single withdrawal. Carol gets ~5USD less than deposited.
4845 ammAlice.withdrawAll(carol, USD(0));
4846 BEAST_EXPECT(expectLine(
4847 env,
4848 carol,
4849 STAmount{USD, UINT64_C(29'994'97487437186), -11}));
4850 },
4851 {{USD(1'000), EUR(1'000)}},
4852 0,
4853 std::nullopt,
4854 {features});
4855
4856 // Withdraw with EPrice limit, 1% fee.
4857 testAMM(
4858 [&](AMM& ammAlice, Env& env) {
4859 ammAlice.deposit(carol, 1'000'000);
4860 auto const tokensFee = ammAlice.withdraw(
4861 carol, USD(100), std::nullopt, IOUAmount{520, 0});
4862 // carol withdraws ~1,443.44USD
4863 auto const balanceAfterWithdraw = [&]() {
4864 if (!features[fixAMMv1_1])
4865 return STAmount(USD, UINT64_C(30'443'43891402715), -11);
4866 return STAmount(USD, UINT64_C(30'443'43891402714), -11);
4867 }();
4868 BEAST_EXPECT(env.balance(carol, USD) == balanceAfterWithdraw);
4869 // Set to original pool size
4870 auto const deposit = balanceAfterWithdraw - USD(29'000);
4871 ammAlice.deposit(carol, deposit);
4872 // fee 0%
4873 ammAlice.vote(alice, 0);
4874 BEAST_EXPECT(ammAlice.expectTradingFee(0));
4875 auto const tokensNoFee = ammAlice.withdraw(carol, deposit);
4876 if (!features[fixAMMv1_1])
4877 BEAST_EXPECT(
4878 env.balance(carol, USD) ==
4879 STAmount(USD, UINT64_C(30'443'43891402717), -11));
4880 else
4881 BEAST_EXPECT(
4882 env.balance(carol, USD) ==
4883 STAmount(USD, UINT64_C(30'443'43891402716), -11));
4884 // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee
4885 // LPTokens
4886 if (!features[fixAMMv1_1])
4887 BEAST_EXPECT(
4888 tokensNoFee == IOUAmount(746'579'80779913, -8));
4889 else
4890 BEAST_EXPECT(
4891 tokensNoFee == IOUAmount(746'579'80779912, -8));
4892 BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8));
4893 },
4894 std::nullopt,
4895 1'000,
4896 std::nullopt,
4897 {features});
4898
4899 // Payment, 1% fee
4900 testAMM(
4901 [&](AMM& ammAlice, Env& env) {
4902 fund(
4903 env,
4904 gw,
4905 {bob},
4906 XRP(1'000),
4907 {USD(1'000), EUR(1'000)},
4908 Fund::Acct);
4909 // Alice contributed 1010EUR and 1000USD to the pool
4910 BEAST_EXPECT(expectLine(env, alice, EUR(28'990)));
4911 BEAST_EXPECT(expectLine(env, alice, USD(29'000)));
4912 BEAST_EXPECT(expectLine(env, carol, USD(30'000)));
4913 // Carol pays to Alice with no fee
4914 env(pay(carol, alice, EUR(10)),
4915 path(~EUR),
4916 sendmax(USD(10)),
4918 env.close();
4919 // Alice has 10EUR more and Carol has 10USD less
4920 BEAST_EXPECT(expectLine(env, alice, EUR(29'000)));
4921 BEAST_EXPECT(expectLine(env, alice, USD(29'000)));
4922 BEAST_EXPECT(expectLine(env, carol, USD(29'990)));
4923
4924 // Set fee to 1%
4925 ammAlice.vote(alice, 1'000);
4926 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
4927 // Bob pays to Carol with 1% fee
4928 env(pay(bob, carol, USD(10)),
4929 path(~USD),
4930 sendmax(EUR(15)),
4932 env.close();
4933 // Bob sends 10.1~EUR to pay 10USD
4934 BEAST_EXPECT(expectLine(
4935 env, bob, STAmount{EUR, UINT64_C(989'8989898989899), -13}));
4936 // Carol got 10USD
4937 BEAST_EXPECT(expectLine(env, carol, USD(30'000)));
4938 BEAST_EXPECT(ammAlice.expectBalances(
4939 USD(1'000),
4940 STAmount{EUR, UINT64_C(1'010'10101010101), -11},
4941 ammAlice.tokens()));
4942 },
4943 {{USD(1'000), EUR(1'010)}},
4944 0,
4945 std::nullopt,
4946 {features});
4947
4948 // Offer crossing, 0.5% fee
4949 testAMM(
4950 [&](AMM& ammAlice, Env& env) {
4951 // No fee
4952 env(offer(carol, EUR(10), USD(10)));
4953 env.close();
4954 BEAST_EXPECT(expectLine(env, carol, USD(29'990)));
4955 BEAST_EXPECT(expectLine(env, carol, EUR(30'010)));
4956 // Change pool composition back
4957 env(offer(carol, USD(10), EUR(10)));
4958 env.close();
4959 // Set fee to 0.5%
4960 ammAlice.vote(alice, 500);
4961 BEAST_EXPECT(ammAlice.expectTradingFee(500));
4962 env(offer(carol, EUR(10), USD(10)));
4963 env.close();
4964 // Alice gets fewer ~4.97EUR for ~5.02USD, the difference goes
4965 // to the pool
4966 BEAST_EXPECT(expectLine(
4967 env,
4968 carol,
4969 STAmount{USD, UINT64_C(29'995'02512562814), -11}));
4970 BEAST_EXPECT(expectLine(
4971 env,
4972 carol,
4973 STAmount{EUR, UINT64_C(30'004'97487437186), -11}));
4974 BEAST_EXPECT(expectOffers(
4975 env,
4976 carol,
4977 1,
4978 {{Amounts{
4979 STAmount{EUR, UINT64_C(5'025125628140703), -15},
4980 STAmount{USD, UINT64_C(5'025125628140703), -15}}}}));
4981 if (!features[fixAMMv1_1])
4982 {
4983 BEAST_EXPECT(ammAlice.expectBalances(
4984 STAmount{USD, UINT64_C(1'004'974874371859), -12},
4985 STAmount{EUR, UINT64_C(1'005'025125628141), -12},
4986 ammAlice.tokens()));
4987 }
4988 else
4989 {
4990 BEAST_EXPECT(ammAlice.expectBalances(
4991 STAmount{USD, UINT64_C(1'004'97487437186), -11},
4992 STAmount{EUR, UINT64_C(1'005'025125628141), -12},
4993 ammAlice.tokens()));
4994 }
4995 },
4996 {{USD(1'000), EUR(1'010)}},
4997 0,
4998 std::nullopt,
4999 {features});
5000
5001 // Payment with AMM and CLOB offer, 0 fee
5002 // AMM liquidity is consumed first up to CLOB offer quality
5003 // CLOB offer is fully consumed next
5004 // Remaining amount is consumed via AMM liquidity
5005 {
5006 Env env(*this, features);
5007 Account const ed("ed");
5008 fund(
5009 env,
5010 gw,
5011 {alice, bob, carol, ed},
5012 XRP(1'000),
5013 {USD(2'000), EUR(2'000)});
5014 env(offer(carol, EUR(5), USD(5)));
5015 AMM ammAlice(env, alice, USD(1'005), EUR(1'000));
5016 env(pay(bob, ed, USD(10)),
5017 path(~USD),
5018 sendmax(EUR(15)),
5020 BEAST_EXPECT(expectLine(env, ed, USD(2'010)));
5021 if (!features[fixAMMv1_1])
5022 {
5023 BEAST_EXPECT(expectLine(env, bob, EUR(1'990)));
5024 BEAST_EXPECT(ammAlice.expectBalances(
5025 USD(1'000), EUR(1'005), ammAlice.tokens()));
5026 }
5027 else
5028 {
5029 BEAST_EXPECT(expectLine(
5030 env, bob, STAmount(EUR, UINT64_C(1989'999999999999), -12)));
5031 BEAST_EXPECT(ammAlice.expectBalances(
5032 USD(1'000),
5033 STAmount(EUR, UINT64_C(1005'000000000001), -12),
5034 ammAlice.tokens()));
5035 }
5036 BEAST_EXPECT(expectOffers(env, carol, 0));
5037 }
5038
5039 // Payment with AMM and CLOB offer. Same as above but with 0.25%
5040 // fee.
5041 {
5042 Env env(*this, features);
5043 Account const ed("ed");
5044 fund(
5045 env,
5046 gw,
5047 {alice, bob, carol, ed},
5048 XRP(1'000),
5049 {USD(2'000), EUR(2'000)});
5050 env(offer(carol, EUR(5), USD(5)));
5051 // Set 0.25% fee
5052 AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 250);
5053 env(pay(bob, ed, USD(10)),
5054 path(~USD),
5055 sendmax(EUR(15)),
5057 BEAST_EXPECT(expectLine(env, ed, USD(2'010)));
5058 if (!features[fixAMMv1_1])
5059 {
5060 BEAST_EXPECT(expectLine(
5061 env,
5062 bob,
5063 STAmount{EUR, UINT64_C(1'989'987453007618), -12}));
5064 BEAST_EXPECT(ammAlice.expectBalances(
5065 USD(1'000),
5066 STAmount{EUR, UINT64_C(1'005'012546992382), -12},
5067 ammAlice.tokens()));
5068 }
5069 else
5070 {
5071 BEAST_EXPECT(expectLine(
5072 env,
5073 bob,
5074 STAmount{EUR, UINT64_C(1'989'987453007628), -12}));
5075 BEAST_EXPECT(ammAlice.expectBalances(
5076 USD(1'000),
5077 STAmount{EUR, UINT64_C(1'005'012546992372), -12},
5078 ammAlice.tokens()));
5079 }
5080 BEAST_EXPECT(expectOffers(env, carol, 0));
5081 }
5082
5083 // Payment with AMM and CLOB offer. AMM has a better
5084 // spot price quality, but 1% fee offsets that. As the result
5085 // the entire trade is executed via LOB.
5086 {
5087 Env env(*this, features);
5088 Account const ed("ed");
5089 fund(
5090 env,
5091 gw,
5092 {alice, bob, carol, ed},
5093 XRP(1'000),
5094 {USD(2'000), EUR(2'000)});
5095 env(offer(carol, EUR(10), USD(10)));
5096 // Set 1% fee
5097 AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 1'000);
5098 env(pay(bob, ed, USD(10)),
5099 path(~USD),
5100 sendmax(EUR(15)),
5102 BEAST_EXPECT(expectLine(env, ed, USD(2'010)));
5103 BEAST_EXPECT(expectLine(env, bob, EUR(1'990)));
5104 BEAST_EXPECT(ammAlice.expectBalances(
5105 USD(1'005), EUR(1'000), ammAlice.tokens()));
5106 BEAST_EXPECT(expectOffers(env, carol, 0));
5107 }
5108
5109 // Payment with AMM and CLOB offer. AMM has a better
5110 // spot price quality, but 1% fee offsets that.
5111 // The CLOB offer is consumed first and the remaining
5112 // amount is consumed via AMM liquidity.
5113 {
5114 Env env(*this, features);
5115 Account const ed("ed");
5116 fund(
5117 env,
5118 gw,
5119 {alice, bob, carol, ed},
5120 XRP(1'000),
5121 {USD(2'000), EUR(2'000)});
5122 env(offer(carol, EUR(9), USD(9)));
5123 // Set 1% fee
5124 AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 1'000);
5125 env(pay(bob, ed, USD(10)),
5126 path(~USD),
5127 sendmax(EUR(15)),
5129 BEAST_EXPECT(expectLine(env, ed, USD(2'010)));
5130 BEAST_EXPECT(expectLine(
5131 env, bob, STAmount{EUR, UINT64_C(1'989'993923296712), -12}));
5132 BEAST_EXPECT(ammAlice.expectBalances(
5133 USD(1'004),
5134 STAmount{EUR, UINT64_C(1'001'006076703288), -12},
5135 ammAlice.tokens()));
5136 BEAST_EXPECT(expectOffers(env, carol, 0));
5137 }
5138 }
5139
5140 void
5142 {
5143 testcase("Adjusted Deposit/Withdraw Tokens");
5144
5145 using namespace jtx;
5146
5147 // Deposit/Withdraw in USD
5148 testAMM(
5149 [&](AMM& ammAlice, Env& env) {
5150 Account const bob("bob");
5151 Account const ed("ed");
5152 Account const paul("paul");
5153 Account const dan("dan");
5154 Account const chris("chris");
5155 Account const simon("simon");
5156 Account const ben("ben");
5157 Account const nataly("nataly");
5158 fund(
5159 env,
5160 gw,
5161 {bob, ed, paul, dan, chris, simon, ben, nataly},
5162 {USD(1'500'000)},
5163 Fund::Acct);
5164 for (int i = 0; i < 10; ++i)
5165 {
5166 ammAlice.deposit(ben, STAmount{USD, 1, -10});
5167 ammAlice.withdrawAll(ben, USD(0));
5168 ammAlice.deposit(simon, USD(0.1));
5169 ammAlice.withdrawAll(simon, USD(0));
5170 ammAlice.deposit(chris, USD(1));
5171 ammAlice.withdrawAll(chris, USD(0));
5172 ammAlice.deposit(dan, USD(10));
5173 ammAlice.withdrawAll(dan, USD(0));
5174 ammAlice.deposit(bob, USD(100));
5175 ammAlice.withdrawAll(bob, USD(0));
5176 ammAlice.deposit(carol, USD(1'000));
5177 ammAlice.withdrawAll(carol, USD(0));
5178 ammAlice.deposit(ed, USD(10'000));
5179 ammAlice.withdrawAll(ed, USD(0));
5180 ammAlice.deposit(paul, USD(100'000));
5181 ammAlice.withdrawAll(paul, USD(0));
5182 ammAlice.deposit(nataly, USD(1'000'000));
5183 ammAlice.withdrawAll(nataly, USD(0));
5184 }
5185 // Due to round off some accounts have a tiny gain, while
5186 // other have a tiny loss. The last account to withdraw
5187 // gets everything in the pool.
5188 if (!features[fixAMMv1_1])
5189 BEAST_EXPECT(ammAlice.expectBalances(
5190 XRP(10'000),
5191 STAmount{USD, UINT64_C(10'000'0000000013), -10},
5192 IOUAmount{10'000'000}));
5193 else
5194 BEAST_EXPECT(ammAlice.expectBalances(
5195 XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
5196 BEAST_EXPECT(expectLine(env, ben, USD(1'500'000)));
5197 BEAST_EXPECT(expectLine(env, simon, USD(1'500'000)));
5198 BEAST_EXPECT(expectLine(env, chris, USD(1'500'000)));
5199 BEAST_EXPECT(expectLine(env, dan, USD(1'500'000)));
5200 if (!features[fixAMMv1_1])
5201 BEAST_EXPECT(expectLine(
5202 env,
5203 carol,
5204 STAmount{USD, UINT64_C(30'000'00000000001), -11}));
5205 else
5206 BEAST_EXPECT(expectLine(env, carol, USD(30'000)));
5207 BEAST_EXPECT(expectLine(env, ed, USD(1'500'000)));
5208 BEAST_EXPECT(expectLine(env, paul, USD(1'500'000)));
5209 if (!features[fixAMMv1_1])
5210 BEAST_EXPECT(expectLine(
5211 env,
5212 nataly,
5213 STAmount{USD, UINT64_C(1'500'000'000000002), -9}));
5214 else
5215 BEAST_EXPECT(expectLine(
5216 env,
5217 nataly,
5218 STAmount{USD, UINT64_C(1'500'000'000000005), -9}));
5219 ammAlice.withdrawAll(alice);
5220 BEAST_EXPECT(!ammAlice.ammExists());
5221 if (!features[fixAMMv1_1])
5222 BEAST_EXPECT(expectLine(
5223 env,
5224 alice,
5225 STAmount{USD, UINT64_C(30'000'0000000013), -10}));
5226 else
5227 BEAST_EXPECT(expectLine(env, alice, USD(30'000)));
5228 // alice XRP balance is 30,000initial - 50 ammcreate fee -
5229 // 10drops fee
5230 BEAST_EXPECT(accountBalance(env, alice) == "29949999990");
5231 },
5232 std::nullopt,
5233 0,
5234 std::nullopt,
5235 {features});
5236
5237 // Same as above but deposit/withdraw in XRP
5238 testAMM([&](AMM& ammAlice, Env& env) {
5239 Account const bob("bob");
5240 Account const ed("ed");
5241 Account const paul("paul");
5242 Account const dan("dan");
5243 Account const chris("chris");
5244 Account const simon("simon");
5245 Account const ben("ben");
5246 Account const nataly("nataly");
5247 fund(
5248 env,
5249 gw,
5250 {bob, ed, paul, dan, chris, simon, ben, nataly},
5251 XRP(2'000'000),
5252 {},
5253 Fund::Acct);
5254 for (int i = 0; i < 10; ++i)
5255 {
5256 ammAlice.deposit(ben, XRPAmount{1});
5257 ammAlice.withdrawAll(ben, XRP(0));
5258 ammAlice.deposit(simon, XRPAmount(1'000));
5259 ammAlice.withdrawAll(simon, XRP(0));
5260 ammAlice.deposit(chris, XRP(1));
5261 ammAlice.withdrawAll(chris, XRP(0));
5262 ammAlice.deposit(dan, XRP(10));
5263 ammAlice.withdrawAll(dan, XRP(0));
5264 ammAlice.deposit(bob, XRP(100));
5265 ammAlice.withdrawAll(bob, XRP(0));
5266 ammAlice.deposit(carol, XRP(1'000));
5267 ammAlice.withdrawAll(carol, XRP(0));
5268 ammAlice.deposit(ed, XRP(10'000));
5269 ammAlice.withdrawAll(ed, XRP(0));
5270 ammAlice.deposit(paul, XRP(100'000));
5271 ammAlice.withdrawAll(paul, XRP(0));
5272 ammAlice.deposit(nataly, XRP(1'000'000));
5273 ammAlice.withdrawAll(nataly, XRP(0));
5274 }
5275 // No round off with XRP in this test
5276 BEAST_EXPECT(ammAlice.expectBalances(
5277 XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
5278 ammAlice.withdrawAll(alice);
5279 BEAST_EXPECT(!ammAlice.ammExists());
5280 // 20,000 initial - (deposit+withdraw) * 10
5281 auto const xrpBalance = (XRP(2'000'000) - txfee(env, 20)).getText();
5282 BEAST_EXPECT(accountBalance(env, ben) == xrpBalance);
5283 BEAST_EXPECT(accountBalance(env, simon) == xrpBalance);
5284 BEAST_EXPECT(accountBalance(env, chris) == xrpBalance);
5285 BEAST_EXPECT(accountBalance(env, dan) == xrpBalance);
5286 // 30,000 initial - (deposit+withdraw) * 10
5287 BEAST_EXPECT(accountBalance(env, carol) == "29999999800");
5288 BEAST_EXPECT(accountBalance(env, ed) == xrpBalance);
5289 BEAST_EXPECT(accountBalance(env, paul) == xrpBalance);
5290 BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance);
5291 // 30,000 initial - 50 ammcreate fee - 10drops withdraw fee
5292 BEAST_EXPECT(accountBalance(env, alice) == "29949999990");
5293 });
5294 }
5295
5296 void
5298 {
5299 testcase("Auto Delete");
5300
5301 using namespace jtx;
5303
5304 {
5305 Env env(
5306 *this,
5308 cfg->FEES.reference_fee = XRPAmount(1);
5309 return cfg;
5310 }),
5311 all);
5312 fund(env, gw, {alice}, XRP(20'000), {USD(10'000)});
5313 AMM amm(env, gw, XRP(10'000), USD(10'000));
5314 for (auto i = 0; i < maxDeletableAMMTrustLines + 10; ++i)
5315 {
5316 Account const a{std::to_string(i)};
5317 env.fund(XRP(1'000), a);
5318 env(trust(a, STAmount{amm.lptIssue(), 10'000}));
5319 env.close();
5320 }
5321 // The trustlines are partially deleted,
5322 // AMM is set to an empty state.
5323 amm.withdrawAll(gw);
5324 BEAST_EXPECT(amm.ammExists());
5325
5326 // Bid,Vote,Deposit,Withdraw,SetTrust failing with
5327 // tecAMM_EMPTY. Deposit succeeds with tfTwoAssetIfEmpty option.
5328 env(amm.bid({
5329 .account = alice,
5330 .bidMin = 1000,
5331 }),
5332 ter(tecAMM_EMPTY));
5333 amm.vote(
5334 std::nullopt,
5335 100,
5336 std::nullopt,
5337 std::nullopt,
5338 std::nullopt,
5339 ter(tecAMM_EMPTY));
5340 amm.withdraw(
5341 alice, 100, std::nullopt, std::nullopt, ter(tecAMM_EMPTY));
5342 amm.deposit(
5343 alice,
5344 USD(100),
5345 std::nullopt,
5346 std::nullopt,
5347 std::nullopt,
5348 ter(tecAMM_EMPTY));
5349 env(trust(alice, STAmount{amm.lptIssue(), 10'000}),
5350 ter(tecAMM_EMPTY));
5351
5352 // Can deposit with tfTwoAssetIfEmpty option
5353 amm.deposit(
5354 alice,
5355 std::nullopt,
5356 XRP(10'000),
5357 USD(10'000),
5358 std::nullopt,
5360 std::nullopt,
5361 std::nullopt,
5362 1'000);
5363 BEAST_EXPECT(
5364 amm.expectBalances(XRP(10'000), USD(10'000), amm.tokens()));
5365 BEAST_EXPECT(amm.expectTradingFee(1'000));
5366 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
5367
5368 // Withdrawing all tokens deletes AMM since the number
5369 // of remaining trustlines is less than max
5370 amm.withdrawAll(alice);
5371 BEAST_EXPECT(!amm.ammExists());
5372 BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
5373 }
5374
5375 {
5376 Env env(
5377 *this,
5379 cfg->FEES.reference_fee = XRPAmount(1);
5380 return cfg;
5381 }),
5382 all);
5383 fund(env, gw, {alice}, XRP(20'000), {USD(10'000)});
5384 AMM amm(env, gw, XRP(10'000), USD(10'000));
5385 for (auto i = 0; i < maxDeletableAMMTrustLines * 2 + 10; ++i)
5386 {
5387 Account const a{std::to_string(i)};
5388 env.fund(XRP(1'000), a);
5389 env(trust(a, STAmount{amm.lptIssue(), 10'000}));
5390 env.close();
5391 }
5392 // The trustlines are partially deleted.
5393 amm.withdrawAll(gw);
5394 BEAST_EXPECT(amm.ammExists());
5395
5396 // AMMDelete has to be called twice to delete AMM.
5397 amm.ammDelete(alice, ter(tecINCOMPLETE));
5398 BEAST_EXPECT(amm.ammExists());
5399 // Deletes remaining trustlines and deletes AMM.
5400 amm.ammDelete(alice);
5401 BEAST_EXPECT(!amm.ammExists());
5402 BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
5403
5404 // Try redundant delete
5405 amm.ammDelete(alice, ter(terNO_AMM));
5406 }
5407 }
5408
5409 void
5411 {
5412 testcase("Clawback");
5413 using namespace jtx;
5414 Env env(*this);
5415 env.fund(XRP(2'000), gw);
5416 env.fund(XRP(2'000), alice);
5417 AMM amm(env, gw, XRP(1'000), USD(1'000));
5419 }
5420
5421 void
5423 {
5424 testcase("AMMID");
5425 using namespace jtx;
5426 testAMM([&](AMM& amm, Env& env) {
5427 amm.setClose(false);
5428 auto const info = env.rpc(
5429 "json",
5430 "account_info",
5432 "{\"account\": \"" + to_string(amm.ammAccount()) + "\"}"));
5433 try
5434 {
5435 BEAST_EXPECT(
5436 info[jss::result][jss::account_data][jss::AMMID]
5437 .asString() == to_string(amm.ammID()));
5438 }
5439 catch (...)
5440 {
5441 fail();
5442 }
5443 amm.deposit(carol, 1'000);
5444 auto affected = env.meta()->getJson(
5445 JsonOptions::none)[sfAffectedNodes.fieldName];
5446 try
5447 {
5448 bool found = false;
5449 for (auto const& node : affected)
5450 {
5451 if (node.isMember(sfModifiedNode.fieldName) &&
5452 node[sfModifiedNode.fieldName]
5453 [sfLedgerEntryType.fieldName]
5454 .asString() == "AccountRoot" &&
5455 node[sfModifiedNode.fieldName][sfFinalFields.fieldName]
5456 [jss::Account]
5457 .asString() == to_string(amm.ammAccount()))
5458 {
5459 found = node[sfModifiedNode.fieldName]
5460 [sfFinalFields.fieldName][jss::AMMID]
5461 .asString() == to_string(amm.ammID());
5462 break;
5463 }
5464 }
5465 BEAST_EXPECT(found);
5466 }
5467 catch (...)
5468 {
5469 fail();
5470 }
5471 });
5472 }
5473
5474 void
5476 {
5477 testcase("Offer/Strand Selection");
5478 using namespace jtx;
5479 Account const ed("ed");
5480 Account const gw1("gw1");
5481 auto const ETH = gw1["ETH"];
5482 auto const CAN = gw1["CAN"];
5483
5484 // These tests are expected to fail if the OwnerPaysFee feature
5485 // is ever supported. Updates will need to be made to AMM handling
5486 // in the payment engine, and these tests will need to be updated.
5487
5488 auto prep = [&](Env& env, auto gwRate, auto gw1Rate) {
5489 fund(env, gw, {alice, carol, bob, ed}, XRP(2'000), {USD(2'000)});
5490 env.fund(XRP(2'000), gw1);
5491 fund(
5492 env,
5493 gw1,
5494 {alice, carol, bob, ed},
5495 {ETH(2'000), CAN(2'000)},
5496 Fund::IOUOnly);
5497 env(rate(gw, gwRate));
5498 env(rate(gw1, gw1Rate));
5499 env.close();
5500 };
5501
5502 for (auto const& rates :
5503 {std::make_pair(1.5, 1.9), std::make_pair(1.9, 1.5)})
5504 {
5505 // Offer Selection
5506
5507 // Cross-currency payment: AMM has the same spot price quality
5508 // as CLOB's offer and can't generate a better quality offer.
5509 // The transfer fee in this case doesn't change the CLOB quality
5510 // because trIn is ignored on adjustment and trOut on payment is
5511 // also ignored because ownerPaysTransferFee is false in this
5512 // case. Run test for 0) offer, 1) AMM, 2) offer and AMM to
5513 // verify that the quality is better in the first case, and CLOB
5514 // is selected in the second case.
5515 {
5517 for (auto i = 0; i < 3; ++i)
5518 {
5519 Env env(*this, features);
5520 prep(env, rates.first, rates.second);
5522 if (i == 0 || i == 2)
5523 {
5524 env(offer(ed, ETH(400), USD(400)), txflags(tfPassive));
5525 env.close();
5526 }
5527 if (i > 0)
5528 amm.emplace(env, ed, USD(1'000), ETH(1'000));
5529 env(pay(carol, bob, USD(100)),
5530 path(~USD),
5531 sendmax(ETH(500)));
5532 env.close();
5533 // CLOB and AMM, AMM is not selected
5534 if (i == 2)
5535 {
5536 BEAST_EXPECT(amm->expectBalances(
5537 USD(1'000), ETH(1'000), amm->tokens()));
5538 }
5539 BEAST_EXPECT(expectLine(env, bob, USD(2'100)));
5540 q[i] = Quality(Amounts{
5541 ETH(2'000) - env.balance(carol, ETH),
5542 env.balance(bob, USD) - USD(2'000)});
5543 }
5544 // CLOB is better quality than AMM
5545 BEAST_EXPECT(q[0] > q[1]);
5546 // AMM is not selected with CLOB
5547 BEAST_EXPECT(q[0] == q[2]);
5548 }
5549 // Offer crossing: AMM has the same spot price quality
5550 // as CLOB's offer and can't generate a better quality offer.
5551 // The transfer fee in this case doesn't change the CLOB quality
5552 // because the quality adjustment is ignored for the offer
5553 // crossing.
5554 for (auto i = 0; i < 3; ++i)
5555 {
5556 Env env(*this, features);
5557 prep(env, rates.first, rates.second);
5559 if (i == 0 || i == 2)
5560 {
5561 env(offer(ed, ETH(400), USD(400)), txflags(tfPassive));
5562 env.close();
5563 }
5564 if (i > 0)
5565 amm.emplace(env, ed, USD(1'000), ETH(1'000));
5566 env(offer(alice, USD(400), ETH(400)));
5567 env.close();
5568 // AMM is not selected
5569 if (i > 0)
5570 {
5571 BEAST_EXPECT(amm->expectBalances(
5572 USD(1'000), ETH(1'000), amm->tokens()));
5573 }
5574 if (i == 0 || i == 2)
5575 {
5576 // Fully crosses
5577 BEAST_EXPECT(expectOffers(env, alice, 0));
5578 }
5579 // Fails to cross because AMM is not selected
5580 else
5581 {
5582 BEAST_EXPECT(expectOffers(
5583 env, alice, 1, {Amounts{USD(400), ETH(400)}}));
5584 }
5585 BEAST_EXPECT(expectOffers(env, ed, 0));
5586 }
5587
5588 // Show that the CLOB quality reduction
5589 // results in AMM offer selection.
5590
5591 // Same as the payment but reduced offer quality
5592 {
5594 for (auto i = 0; i < 3; ++i)
5595 {
5596 Env env(*this, features);
5597 prep(env, rates.first, rates.second);
5599 if (i == 0 || i == 2)
5600 {
5601 env(offer(ed, ETH(400), USD(300)), txflags(tfPassive));
5602 env.close();
5603 }
5604 if (i > 0)
5605 amm.emplace(env, ed, USD(1'000), ETH(1'000));
5606 env(pay(carol, bob, USD(100)),
5607 path(~USD),
5608 sendmax(ETH(500)));
5609 env.close();
5610 // AMM and CLOB are selected
5611 if (i > 0)
5612 {
5613 BEAST_EXPECT(!amm->expectBalances(
5614 USD(1'000), ETH(1'000), amm->tokens()));
5615 }
5616 if (i == 2 && !features[fixAMMv1_1])
5617 {
5618 if (rates.first == 1.5)
5619 {
5620 if (!features[fixAMMv1_1])
5621 BEAST_EXPECT(expectOffers(
5622 env,
5623 ed,
5624 1,
5625 {{Amounts{
5626 STAmount{
5627 ETH,
5628 UINT64_C(378'6327949540823),
5629 -13},
5630 STAmount{
5631 USD,
5632 UINT64_C(283'9745962155617),
5633 -13}}}}));
5634 else
5635 BEAST_EXPECT(expectOffers(
5636 env,
5637 ed,
5638 1,
5639 {{Amounts{
5640 STAmount{
5641 ETH,
5642 UINT64_C(378'6327949540813),
5643 -13},
5644 STAmount{
5645 USD,
5646 UINT64_C(283'974596215561),
5647 -12}}}}));
5648 }
5649 else
5650 {
5651 if (!features[fixAMMv1_1])
5652 BEAST_EXPECT(expectOffers(
5653 env,
5654 ed,
5655 1,
5656 {{Amounts{
5657 STAmount{
5658 ETH,
5659 UINT64_C(325'299461620749),
5660 -12},
5661 STAmount{
5662 USD,
5663 UINT64_C(243'9745962155617),
5664 -13}}}}));
5665 else
5666 BEAST_EXPECT(expectOffers(
5667 env,
5668 ed,
5669 1,
5670 {{Amounts{
5671 STAmount{
5672 ETH,
5673 UINT64_C(325'299461620748),
5674 -12},
5675 STAmount{
5676 USD,
5677 UINT64_C(243'974596215561),
5678 -12}}}}));
5679 }
5680 }
5681 else if (i == 2)
5682 {
5683 if (rates.first == 1.5)
5684 {
5685 BEAST_EXPECT(expectOffers(
5686 env,
5687 ed,
5688 1,
5689 {{Amounts{
5690 STAmount{
5691 ETH, UINT64_C(378'6327949540812), -13},
5692 STAmount{
5693 USD,
5694 UINT64_C(283'9745962155609),
5695 -13}}}}));
5696 }
5697 else
5698 {
5699 BEAST_EXPECT(expectOffers(
5700 env,
5701 ed,
5702 1,
5703 {{Amounts{
5704 STAmount{
5705 ETH, UINT64_C(325'2994616207479), -13},
5706 STAmount{
5707 USD,
5708 UINT64_C(243'9745962155609),
5709 -13}}}}));
5710 }
5711 }
5712 BEAST_EXPECT(expectLine(env, bob, USD(2'100)));
5713 q[i] = Quality(Amounts{
5714 ETH(2'000) - env.balance(carol, ETH),
5715 env.balance(bob, USD) - USD(2'000)});
5716 }
5717 // AMM is better quality
5718 BEAST_EXPECT(q[1] > q[0]);
5719 // AMM and CLOB produce better quality
5720 BEAST_EXPECT(q[2] > q[1]);
5721 }
5722
5723 // Same as the offer-crossing but reduced offer quality
5724 for (auto i = 0; i < 3; ++i)
5725 {
5726 Env env(*this, features);
5727 prep(env, rates.first, rates.second);
5729 if (i == 0 || i == 2)
5730 {
5731 env(offer(ed, ETH(400), USD(250)), txflags(tfPassive));
5732 env.close();
5733 }
5734 if (i > 0)
5735 amm.emplace(env, ed, USD(1'000), ETH(1'000));
5736 env(offer(alice, USD(250), ETH(400)));
5737 env.close();
5738 // AMM is selected in both cases
5739 if (i > 0)
5740 {
5741 BEAST_EXPECT(!amm->expectBalances(
5742 USD(1'000), ETH(1'000), amm->tokens()));
5743 }
5744 // Partially crosses, AMM is selected, CLOB fails
5745 // limitQuality
5746 if (i == 2)
5747 {
5748 if (rates.first == 1.5)
5749 {
5750 if (!features[fixAMMv1_1])
5751 {
5752 BEAST_EXPECT(expectOffers(
5753 env, ed, 1, {{Amounts{ETH(400), USD(250)}}}));
5754 BEAST_EXPECT(expectOffers(
5755 env,
5756 alice,
5757 1,
5758 {{Amounts{
5759 STAmount{
5760 USD, UINT64_C(40'5694150420947), -13},
5761 STAmount{
5762 ETH, UINT64_C(64'91106406735152), -14},
5763 }}}));
5764 }
5765 else
5766 {
5767 // Ed offer is partially crossed.
5768 // The updated rounding makes limitQuality
5769 // work if both amendments are enabled
5770 BEAST_EXPECT(expectOffers(
5771 env,
5772 ed,
5773 1,
5774 {{Amounts{
5775 STAmount{
5776 ETH, UINT64_C(335'0889359326475), -13},
5777 STAmount{
5778 USD, UINT64_C(209'4305849579047), -13},
5779 }}}));
5780 BEAST_EXPECT(expectOffers(env, alice, 0));
5781 }
5782 }
5783 else
5784 {
5785 if (!features[fixAMMv1_1])
5786 {
5787 // Ed offer is partially crossed.
5788 BEAST_EXPECT(expectOffers(
5789 env,
5790 ed,
5791 1,
5792 {{Amounts{
5793 STAmount{
5794 ETH, UINT64_C(335'0889359326485), -13},
5795 STAmount{
5796 USD, UINT64_C(209'4305849579053), -13},
5797 }}}));
5798 BEAST_EXPECT(expectOffers(env, alice, 0));
5799 }
5800 else
5801 {
5802 // Ed offer is partially crossed.
5803 BEAST_EXPECT(expectOffers(
5804 env,
5805 ed,
5806 1,
5807 {{Amounts{
5808 STAmount{
5809 ETH, UINT64_C(335'0889359326475), -13},
5810 STAmount{
5811 USD, UINT64_C(209'4305849579047), -13},
5812 }}}));
5813 BEAST_EXPECT(expectOffers(env, alice, 0));
5814 }
5815 }
5816 }
5817 }
5818
5819 // Strand selection
5820
5821 // Two book steps strand quality is 1.
5822 // AMM strand's best quality is equal to AMM's spot price
5823 // quality, which is 1. Both strands (steps) are adjusted
5824 // for the transfer fee in qualityUpperBound. In case
5825 // of two strands, AMM offers have better quality and are
5826 // consumed first, remaining liquidity is generated by CLOB
5827 // offers. Liquidity from two strands is better in this case
5828 // than in case of one strand with two book steps. Liquidity
5829 // from one strand with AMM has better quality than either one
5830 // strand with two book steps or two strands. It may appear
5831 // unintuitive, but one strand with AMM is optimized and
5832 // generates one AMM offer, while in case of two strands,
5833 // multiple AMM offers are generated, which results in slightly
5834 // worse overall quality.
5835 {
5837 for (auto i = 0; i < 3; ++i)
5838 {
5839 Env env(*this, features);
5840 prep(env, rates.first, rates.second);
5842
5843 if (i == 0 || i == 2)
5844 {
5845 env(offer(ed, ETH(400), CAN(400)), txflags(tfPassive));
5846 env(offer(ed, CAN(400), USD(400))), txflags(tfPassive);
5847 env.close();
5848 }
5849
5850 if (i > 0)
5851 amm.emplace(env, ed, ETH(1'000), USD(1'000));
5852
5853 env(pay(carol, bob, USD(100)),
5854 path(~USD),
5855 path(~CAN, ~USD),
5856 sendmax(ETH(600)));
5857 env.close();
5858
5859 BEAST_EXPECT(expectLine(env, bob, USD(2'100)));
5860
5861 if (i == 2 && !features[fixAMMv1_1])
5862 {
5863 if (rates.first == 1.5)
5864 {
5865 // Liquidity is consumed from AMM strand only
5866 BEAST_EXPECT(amm->expectBalances(
5867 STAmount{ETH, UINT64_C(1'176'66038955758), -11},
5868 USD(850),
5869 amm->tokens()));
5870 }
5871 else
5872 {
5873 BEAST_EXPECT(amm->expectBalances(
5874 STAmount{
5875 ETH, UINT64_C(1'179'540094339627), -12},
5876 STAmount{USD, UINT64_C(847'7880529867501), -13},
5877 amm->tokens()));
5878 BEAST_EXPECT(expectOffers(
5879 env,
5880 ed,
5881 2,
5882 {{Amounts{
5883 STAmount{
5884 ETH,
5885 UINT64_C(343'3179205198749),
5886 -13},
5887 STAmount{
5888 CAN,
5889 UINT64_C(343'3179205198749),
5890 -13},
5891 },
5892 Amounts{
5893 STAmount{
5894 CAN,
5895 UINT64_C(362'2119470132499),
5896 -13},
5897 STAmount{
5898 USD,
5899 UINT64_C(362'2119470132499),
5900 -13},
5901 }}}));
5902 }
5903 }
5904 else if (i == 2)
5905 {
5906 if (rates.first == 1.5)
5907 {
5908 // Liquidity is consumed from AMM strand only
5909 BEAST_EXPECT(amm->expectBalances(
5910 STAmount{
5911 ETH, UINT64_C(1'176'660389557593), -12},
5912 USD(850),
5913 amm->tokens()));
5914 }
5915 else
5916 {
5917 BEAST_EXPECT(amm->expectBalances(
5918 STAmount{ETH, UINT64_C(1'179'54009433964), -11},
5919 STAmount{USD, UINT64_C(847'7880529867501), -13},
5920 amm->tokens()));
5921 BEAST_EXPECT(expectOffers(
5922 env,
5923 ed,
5924 2,
5925 {{Amounts{
5926 STAmount{
5927 ETH,
5928 UINT64_C(343'3179205198749),
5929 -13},
5930 STAmount{
5931 CAN,
5932 UINT64_C(343'3179205198749),
5933 -13},
5934 },
5935 Amounts{
5936 STAmount{
5937 CAN,
5938 UINT64_C(362'2119470132499),
5939 -13},
5940 STAmount{
5941 USD,
5942 UINT64_C(362'2119470132499),
5943 -13},
5944 }}}));
5945 }
5946 }
5947 q[i] = Quality(Amounts{
5948 ETH(2'000) - env.balance(carol, ETH),
5949 env.balance(bob, USD) - USD(2'000)});
5950 }
5951 BEAST_EXPECT(q[1] > q[0]);
5952 BEAST_EXPECT(q[2] > q[0] && q[2] < q[1]);
5953 }
5954 }
5955 }
5956
5957 void
5959 {
5960 testcase("Fix Default Inner Object");
5961 using namespace jtx;
5963
5964 auto test = [&](FeatureBitset features,
5965 TER const& err1,
5966 TER const& err2,
5967 TER const& err3,
5968 TER const& err4,
5969 std::uint16_t tfee,
5970 bool closeLedger,
5971 std::optional<std::uint16_t> extra = std::nullopt) {
5972 Env env(*this, features);
5973 fund(env, gw, {alice}, XRP(1'000), {USD(10)});
5974 AMM amm(
5975 env,
5976 gw,
5977 XRP(10),
5978 USD(10),
5979 {.tfee = tfee, .close = closeLedger});
5980 amm.deposit(alice, USD(10), XRP(10));
5981 amm.vote(VoteArg{.account = alice, .tfee = tfee, .err = ter(err1)});
5982 amm.withdraw(WithdrawArg{
5983 .account = gw, .asset1Out = USD(1), .err = ter(err2)});
5984 // with the amendment disabled and ledger not closed,
5985 // second vote succeeds if the first vote sets the trading fee
5986 // to non-zero; if the first vote sets the trading fee to >0 &&
5987 // <9 then the second withdraw succeeds if the second vote sets
5988 // the trading fee so that the discounted fee is non-zero
5989 amm.vote(VoteArg{.account = alice, .tfee = 20, .err = ter(err3)});
5990 amm.withdraw(WithdrawArg{
5991 .account = gw, .asset1Out = USD(2), .err = ter(err4)});
5992 };
5993
5994 // ledger is closed after each transaction, vote/withdraw don't fail
5995 // regardless whether the amendment is enabled or not
5996 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true);
5997 test(
5998 all - fixInnerObjTemplate,
5999 tesSUCCESS,
6000 tesSUCCESS,
6001 tesSUCCESS,
6002 tesSUCCESS,
6003 0,
6004 true);
6005 // ledger is not closed after each transaction
6006 // vote/withdraw don't fail if the amendment is enabled
6007 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, false);
6008 // vote/withdraw fail if the amendment is not enabled
6009 // second vote/withdraw still fail: second vote fails because
6010 // the initial trading fee is 0, consequently second withdraw fails
6011 // because the second vote fails
6012 test(
6013 all - fixInnerObjTemplate,
6018 0,
6019 false);
6020 // if non-zero trading/discounted fee then vote/withdraw
6021 // don't fail whether the ledger is closed or not and
6022 // the amendment is enabled or not
6023 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true);
6024 test(
6025 all - fixInnerObjTemplate,
6026 tesSUCCESS,
6027 tesSUCCESS,
6028 tesSUCCESS,
6029 tesSUCCESS,
6030 10,
6031 true);
6032 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false);
6033 test(
6034 all - fixInnerObjTemplate,
6035 tesSUCCESS,
6036 tesSUCCESS,
6037 tesSUCCESS,
6038 tesSUCCESS,
6039 10,
6040 false);
6041 // non-zero trading fee but discounted fee is 0, vote doesn't fail
6042 // but withdraw fails
6043 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 9, false);
6044 // second vote sets the trading fee to non-zero, consequently
6045 // second withdraw doesn't fail even if the amendment is not
6046 // enabled and the ledger is not closed
6047 test(
6048 all - fixInnerObjTemplate,
6049 tesSUCCESS,
6051 tesSUCCESS,
6052 tesSUCCESS,
6053 9,
6054 false);
6055 }
6056
6057 void
6059 {
6060 testcase("Fix changeSpotPriceQuality");
6061 using namespace jtx;
6062
6063 enum class Status {
6064 SucceedShouldSucceedResize, // Succeed in pre-fix because
6065 // error allowance, succeed post-fix
6066 // because of offer resizing
6067 FailShouldSucceed, // Fail in pre-fix due to rounding,
6068 // succeed after fix because of XRP
6069 // side is generated first
6070 SucceedShouldFail, // Succeed in pre-fix, fail after fix
6071 // due to small quality difference
6072 Fail, // Both fail because the quality can't be matched
6073 Succeed // Both succeed
6074 };
6075 using enum Status;
6076 auto const xrpIouAmounts10_100 =
6077 TAmounts{XRPAmount{10}, IOUAmount{100}};
6078 auto const iouXrpAmounts10_100 =
6079 TAmounts{IOUAmount{10}, XRPAmount{100}};
6080 // clang-format off
6082 //Pool In , Pool Out, Quality , Fee, Status
6083 {"0.001519763260828713", "1558701", Quality{5414253689393440221}, 1000, FailShouldSucceed},
6084 {"0.01099814367603737", "1892611", Quality{5482264816516900274}, 1000, FailShouldSucceed},
6085 {"0.78", "796599", Quality{5630392334958379008}, 1000, FailShouldSucceed},
6086 {"105439.2955578965", "49398693", Quality{5910869983721805038}, 400, FailShouldSucceed},
6087 {"12408293.23445213", "4340810521", Quality{5911611095910090752}, 997, FailShouldSucceed},
6088 {"1892611", "0.01099814367603737", Quality{6703103457950430139}, 1000, FailShouldSucceed},
6089 {"423028.8508101858", "3392804520", Quality{5837920340654162816}, 600, FailShouldSucceed},
6090 {"44565388.41001027", "73890647", Quality{6058976634606450001}, 1000, FailShouldSucceed},
6091 {"66831.68494832662", "16", Quality{6346111134641742975}, 0, FailShouldSucceed},
6092 {"675.9287302203422", "1242632304", Quality{5625960929244093294}, 300, FailShouldSucceed},
6093 {"7047.112186735699", "1649845866", Quality{5696855348026306945}, 504, FailShouldSucceed},
6094 {"840236.4402981238", "47419053", Quality{5982561601648018688}, 499, FailShouldSucceed},
6095 {"992715.618909774", "189445631733", Quality{5697835648288106944}, 815, SucceedShouldSucceedResize},
6096 {"504636667521", "185545883.9506651", Quality{6343802275337659280}, 503, SucceedShouldSucceedResize},
6097 {"992706.7218636649", "189447316000", Quality{5697835648288106944}, 797, SucceedShouldSucceedResize},
6098 {"1.068737911388205", "127860278877", Quality{5268604356368739396}, 293, SucceedShouldSucceedResize},
6099 {"17932506.56880419", "189308.6043676173", Quality{6206460598195440068}, 311, SucceedShouldSucceedResize},
6100 {"1.066379294658174", "128042251493", Quality{5268559341368739328}, 270, SucceedShouldSucceedResize},
6101 {"350131413924", "1576879.110907892", Quality{6487411636539049449}, 650, Fail},
6102 {"422093460", "2.731797662057464", Quality{6702911108534394924}, 1000, Fail},
6103 {"76128132223", "367172.7148422662", Quality{6487263463413514240}, 548, Fail},
6104 {"132701839250", "280703770.7695443", Quality{6273750681188885075}, 562, Fail},
6105 {"994165.7604612011", "189551302411", Quality{5697835592690668727}, 815, Fail},
6106 {"45053.33303227917", "86612695359", Quality{5625695218943638190}, 500, Fail},
6107 {"199649.077043865", "14017933007", Quality{5766034667318524880}, 324, Fail},
6108 {"27751824831.70903", "78896950", Quality{6272538159621630432}, 500, Fail},
6109 {"225.3731275781907", "156431793648", Quality{5477818047604078924}, 989, Fail},
6110 {"199649.077043865", "14017933007", Quality{5766036094462806309}, 324, Fail},
6111 {"3.590272027140361", "20677643641", Quality{5406056147042156356}, 808, Fail},
6112 {"1.070884664490231", "127604712776", Quality{5268620608623825741}, 293, Fail},
6113 {"3272.448829820197", "6275124076", Quality{5625710328924117902}, 81, Fail},
6114 {"0.009059512633902926", "7994028", Quality{5477511954775533172}, 1000, Fail},
6115 {"1", "1.0", Quality{0}, 100, Fail},
6116 {"1.0", "1", Quality{0}, 100, Fail},
6117 {"10", "10.0", Quality{xrpIouAmounts10_100}, 100, Fail},
6118 {"10.0", "10", Quality{iouXrpAmounts10_100}, 100, Fail},
6119 {"69864389131", "287631.4543025075", Quality{6487623473313516078}, 451, Succeed},
6120 {"4328342973", "12453825.99247381", Quality{6272522264364865181}, 997, Succeed},
6121 {"32347017", "7003.93031579449", Quality{6347261126087916670}, 1000, Succeed},
6122 {"61697206161", "36631.4583206413", Quality{6558965195382476659}, 500, Succeed},
6123 {"1654524979", "7028.659825511603", Quality{6487551345110052981}, 504, Succeed},
6124 {"88621.22277293179", "5128418948", Quality{5766347291552869205}, 380, Succeed},
6125 {"1892611", "0.01099814367603737", Quality{6703102780512015436}, 1000, Succeed},
6126 {"4542.639373338766", "24554809", Quality{5838994982188783710}, 0, Succeed},
6127 {"5132932546", "88542.99750172683", Quality{6419203342950054537}, 380, Succeed},
6128 {"78929964.1549083", "1506494795", Quality{5986890029845558688}, 589, Succeed},
6129 {"10096561906", "44727.72453735605", Quality{6487455290284644551}, 250, Succeed},
6130 {"5092.219565514988", "8768257694", Quality{5626349534958379008}, 503, Succeed},
6131 {"1819778294", "8305.084302902864", Quality{6487429398998540860}, 415, Succeed},
6132 {"6970462.633911943", "57359281", Quality{6054087899185946624}, 850, Succeed},
6133 {"3983448845", "2347.543644281467", Quality{6558965195382476659}, 856, Succeed},
6134 // This is a tiny offer 12drops/19321952e-15 it succeeds pre-amendment because of the error allowance.
6135 // Post amendment it is resized to 11drops/17711789e-15 but the quality is still less than
6136 // the target quality and the offer fails.
6137 {"771493171", "1.243473020567508", Quality{6707566798038544272}, 100, SucceedShouldFail},
6138 };
6139 // clang-format on
6140
6141 boost::regex rx("^\\d+$");
6142 boost::smatch match;
6143 // tests that succeed should have the same amounts pre-fix and post-fix
6145 Env env(*this, features);
6146 auto rules = env.current()->rules();
6148 for (auto const& t : tests)
6149 {
6150 auto getPool = [&](std::string const& v, bool isXRP) {
6151 if (isXRP)
6152 return amountFromString(xrpIssue(), v);
6153 return amountFromString(noIssue(), v);
6154 };
6155 auto const& quality = std::get<Quality>(t);
6156 auto const tfee = std::get<std::uint16_t>(t);
6157 auto const status = std::get<Status>(t);
6158 auto const poolInIsXRP =
6159 boost::regex_search(std::get<0>(t), match, rx);
6160 auto const poolOutIsXRP =
6161 boost::regex_search(std::get<1>(t), match, rx);
6162 assert(!(poolInIsXRP && poolOutIsXRP));
6163 auto const poolIn = getPool(std::get<0>(t), poolInIsXRP);
6164 auto const poolOut = getPool(std::get<1>(t), poolOutIsXRP);
6165 try
6166 {
6167 auto const amounts = changeSpotPriceQuality(
6168 Amounts{poolIn, poolOut},
6169 quality,
6170 tfee,
6171 env.current()->rules(),
6172 env.journal);
6173 if (amounts)
6174 {
6175 if (status == SucceedShouldSucceedResize)
6176 {
6177 if (!features[fixAMMv1_1])
6178 BEAST_EXPECT(Quality{*amounts} < quality);
6179 else
6180 BEAST_EXPECT(Quality{*amounts} >= quality);
6181 }
6182 else if (status == Succeed)
6183 {
6184 if (!features[fixAMMv1_1])
6185 BEAST_EXPECT(
6186 Quality{*amounts} >= quality ||
6188 Quality{*amounts}, quality, Number{1, -7}));
6189 else
6190 BEAST_EXPECT(Quality{*amounts} >= quality);
6191 }
6192 else if (status == FailShouldSucceed)
6193 {
6194 BEAST_EXPECT(
6195 features[fixAMMv1_1] &&
6196 Quality{*amounts} >= quality);
6197 }
6198 else if (status == SucceedShouldFail)
6199 {
6200 BEAST_EXPECT(
6201 !features[fixAMMv1_1] &&
6202 Quality{*amounts} < quality &&
6204 Quality{*amounts}, quality, Number{1, -7}));
6205 }
6206 }
6207 else
6208 {
6209 // Fails pre- and post-amendment because the quality can't
6210 // be matched. Verify by generating a tiny offer, which
6211 // doesn't match the quality. Exclude zero quality since
6212 // no offer is generated in this case.
6213 if (status == Fail && quality != Quality{0})
6214 {
6215 auto tinyOffer = [&]() {
6216 if (isXRP(poolIn))
6217 {
6218 auto const takerPays = STAmount{xrpIssue(), 1};
6219 return Amounts{
6220 takerPays,
6222 Amounts{poolIn, poolOut},
6223 takerPays,
6224 tfee)};
6225 }
6226 else if (isXRP(poolOut))
6227 {
6228 auto const takerGets = STAmount{xrpIssue(), 1};
6229 return Amounts{
6231 Amounts{poolIn, poolOut},
6232 takerGets,
6233 tfee),
6234 takerGets};
6235 }
6236 auto const takerPays = toAmount<STAmount>(
6237 getIssue(poolIn), Number{1, -10} * poolIn);
6238 return Amounts{
6239 takerPays,
6241 Amounts{poolIn, poolOut}, takerPays, tfee)};
6242 }();
6243 BEAST_EXPECT(Quality(tinyOffer) < quality);
6244 }
6245 else if (status == FailShouldSucceed)
6246 {
6247 BEAST_EXPECT(!features[fixAMMv1_1]);
6248 }
6249 else if (status == SucceedShouldFail)
6250 {
6251 BEAST_EXPECT(features[fixAMMv1_1]);
6252 }
6253 }
6254 }
6255 catch (std::runtime_error const& e)
6256 {
6257 BEAST_EXPECT(
6258 !strcmp(e.what(), "changeSpotPriceQuality failed"));
6259 BEAST_EXPECT(
6260 !features[fixAMMv1_1] && status == FailShouldSucceed);
6261 }
6262 }
6263
6264 // Test negative discriminant
6265 {
6266 // b**2 - 4 * a * c -> 1 * 1 - 4 * 1 * 1 = -3
6267 auto const res =
6269 BEAST_EXPECT(!res.has_value());
6270 }
6271 }
6272
6273 void
6275 {
6276 using namespace jtx;
6277
6278 testAMM([&](AMM& ammAlice, Env& env) {
6279 WithdrawArg args{
6281 .err = ter(temMALFORMED),
6282 };
6283 ammAlice.withdraw(args);
6284 });
6285
6286 testAMM([&](AMM& ammAlice, Env& env) {
6287 WithdrawArg args{
6289 .err = ter(temMALFORMED),
6290 };
6291 ammAlice.withdraw(args);
6292 });
6293
6294 testAMM([&](AMM& ammAlice, Env& env) {
6295 WithdrawArg args{
6297 .err = ter(temMALFORMED),
6298 };
6299 ammAlice.withdraw(args);
6300 });
6301
6302 testAMM([&](AMM& ammAlice, Env& env) {
6303 WithdrawArg args{
6304 .asset1Out = XRP(100),
6305 .asset2Out = XRP(100),
6306 .err = ter(temBAD_AMM_TOKENS),
6307 };
6308 ammAlice.withdraw(args);
6309 });
6310
6311 testAMM([&](AMM& ammAlice, Env& env) {
6312 WithdrawArg args{
6313 .asset1Out = XRP(100),
6314 .asset2Out = BAD(100),
6315 .err = ter(temBAD_CURRENCY),
6316 };
6317 ammAlice.withdraw(args);
6318 });
6319
6320 testAMM([&](AMM& ammAlice, Env& env) {
6321 Json::Value jv;
6322 jv[jss::TransactionType] = jss::AMMWithdraw;
6323 jv[jss::Flags] = tfLimitLPToken;
6324 jv[jss::Account] = alice.human();
6325 ammAlice.setTokens(jv);
6326 XRP(100).value().setJson(jv[jss::Amount]);
6327 USD(100).value().setJson(jv[jss::EPrice]);
6328 env(jv, ter(temBAD_AMM_TOKENS));
6329 });
6330 }
6331
6332 void
6334 {
6335 using namespace jtx;
6336 using namespace std::chrono;
6337 FeatureBitset const all{features};
6338
6339 Account const gatehub{"gatehub"};
6340 Account const bitstamp{"bitstamp"};
6341 Account const trader{"trader"};
6342 auto const usdGH = gatehub["USD"];
6343 auto const btcGH = gatehub["BTC"];
6344 auto const usdBIT = bitstamp["USD"];
6345
6346 struct InputSet
6347 {
6348 char const* testCase;
6349 double const poolUsdBIT;
6350 double const poolUsdGH;
6351 sendmax const sendMaxUsdBIT;
6352 STAmount const sendUsdGH;
6353 STAmount const failUsdGH;
6354 STAmount const failUsdGHr;
6355 STAmount const failUsdBIT;
6356 STAmount const failUsdBITr;
6357 STAmount const goodUsdGH;
6358 STAmount const goodUsdGHr;
6359 STAmount const goodUsdBIT;
6360 STAmount const goodUsdBITr;
6361 IOUAmount const lpTokenBalance;
6362 double const offer1BtcGH = 0.1;
6363 double const offer2BtcGH = 0.1;
6364 double const offer2UsdGH = 1;
6365 double const rateBIT = 0.0;
6366 double const rateGH = 0.0;
6367 };
6368
6369 using uint64_t = std::uint64_t;
6370
6371 for (auto const& input : {
6372 InputSet{
6373 .testCase = "Test Fix Overflow Offer", //
6374 .poolUsdBIT = 3, //
6375 .poolUsdGH = 273, //
6376 .sendMaxUsdBIT{usdBIT(50)}, //
6377 .sendUsdGH{usdGH, uint64_t(272'455089820359), -12}, //
6378 .failUsdGH = STAmount{0}, //
6379 .failUsdGHr = STAmount{0}, //
6380 .failUsdBIT{usdBIT, uint64_t(46'47826086956522), -14}, //
6381 .failUsdBITr{usdBIT, uint64_t(46'47826086956521), -14}, //
6382 .goodUsdGH{usdGH, uint64_t(96'7543114220382), -13}, //
6383 .goodUsdGHr{usdGH, uint64_t(96'7543114222965), -13}, //
6384 .goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15}, //
6385 .goodUsdBITr{usdBIT, uint64_t(8'464739069098152), -15}, //
6386 .lpTokenBalance = {28'61817604250837, -14}, //
6387 .offer1BtcGH = 0.1, //
6388 .offer2BtcGH = 0.1, //
6389 .offer2UsdGH = 1, //
6390 .rateBIT = 1.15, //
6391 .rateGH = 1.2, //
6392 },
6393 InputSet{
6394 .testCase = "Overflow test {1, 100, 0.111}", //
6395 .poolUsdBIT = 1, //
6396 .poolUsdGH = 100, //
6397 .sendMaxUsdBIT{usdBIT(0.111)}, //
6398 .sendUsdGH{usdGH, 100}, //
6399 .failUsdGH = STAmount{0}, //
6400 .failUsdGHr = STAmount{0}, //
6401 .failUsdBIT{usdBIT, uint64_t(1'111), -3}, //
6402 .failUsdBITr{usdBIT, uint64_t(1'111), -3}, //
6403 .goodUsdGH{usdGH, uint64_t(90'04347888284115), -14}, //
6404 .goodUsdGHr{usdGH, uint64_t(90'04347888284201), -14}, //
6405 .goodUsdBIT{usdBIT, uint64_t(1'111), -3}, //
6406 .goodUsdBITr{usdBIT, uint64_t(1'111), -3}, //
6407 .lpTokenBalance{10, 0}, //
6408 .offer1BtcGH = 1e-5, //
6409 .offer2BtcGH = 1, //
6410 .offer2UsdGH = 1e-5, //
6411 .rateBIT = 0, //
6412 .rateGH = 0, //
6413 },
6414 InputSet{
6415 .testCase = "Overflow test {1, 100, 1.00}", //
6416 .poolUsdBIT = 1, //
6417 .poolUsdGH = 100, //
6418 .sendMaxUsdBIT{usdBIT(1.00)}, //
6419 .sendUsdGH{usdGH, 100}, //
6420 .failUsdGH = STAmount{0}, //
6421 .failUsdGHr = STAmount{0}, //
6422 .failUsdBIT{usdBIT, uint64_t(2), 0}, //
6423 .failUsdBITr{usdBIT, uint64_t(2), 0}, //
6424 .goodUsdGH{usdGH, uint64_t(52'94379354424079), -14}, //
6425 .goodUsdGHr{usdGH, uint64_t(52'94379354424135), -14}, //
6426 .goodUsdBIT{usdBIT, uint64_t(2), 0}, //
6427 .goodUsdBITr{usdBIT, uint64_t(2), 0}, //
6428 .lpTokenBalance{10, 0}, //
6429 .offer1BtcGH = 1e-5, //
6430 .offer2BtcGH = 1, //
6431 .offer2UsdGH = 1e-5, //
6432 .rateBIT = 0, //
6433 .rateGH = 0, //
6434 },
6435 InputSet{
6436 .testCase = "Overflow test {1, 100, 4.6432}", //
6437 .poolUsdBIT = 1, //
6438 .poolUsdGH = 100, //
6439 .sendMaxUsdBIT{usdBIT(4.6432)}, //
6440 .sendUsdGH{usdGH, 100}, //
6441 .failUsdGH = STAmount{0}, //
6442 .failUsdGHr = STAmount{0}, //
6443 .failUsdBIT{usdBIT, uint64_t(5'6432), -4}, //
6444 .failUsdBITr{usdBIT, uint64_t(5'6432), -4}, //
6445 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
6446 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
6447 .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15}, //
6448 .goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15}, //
6449 .lpTokenBalance{10, 0}, //
6450 .offer1BtcGH = 1e-5, //
6451 .offer2BtcGH = 1, //
6452 .offer2UsdGH = 1e-5, //
6453 .rateBIT = 0, //
6454 .rateGH = 0, //
6455 },
6456 InputSet{
6457 .testCase = "Overflow test {1, 100, 10}", //
6458 .poolUsdBIT = 1, //
6459 .poolUsdGH = 100, //
6460 .sendMaxUsdBIT{usdBIT(10)}, //
6461 .sendUsdGH{usdGH, 100}, //
6462 .failUsdGH = STAmount{0}, //
6463 .failUsdGHr = STAmount{0}, //
6464 .failUsdBIT{usdBIT, uint64_t(11), 0}, //
6465 .failUsdBITr{usdBIT, uint64_t(11), 0}, //
6466 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
6467 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
6468 .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15}, //
6469 .goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15}, //
6470 .lpTokenBalance{10, 0}, //
6471 .offer1BtcGH = 1e-5, //
6472 .offer2BtcGH = 1, //
6473 .offer2UsdGH = 1e-5, //
6474 .rateBIT = 0, //
6475 .rateGH = 0, //
6476 },
6477 InputSet{
6478 .testCase = "Overflow test {50, 100, 5.55}", //
6479 .poolUsdBIT = 50, //
6480 .poolUsdGH = 100, //
6481 .sendMaxUsdBIT{usdBIT(5.55)}, //
6482 .sendUsdGH{usdGH, 100}, //
6483 .failUsdGH = STAmount{0}, //
6484 .failUsdGHr = STAmount{0}, //
6485 .failUsdBIT{usdBIT, uint64_t(55'55), -2}, //
6486 .failUsdBITr{usdBIT, uint64_t(55'55), -2}, //
6487 .goodUsdGH{usdGH, uint64_t(90'04347888284113), -14}, //
6488 .goodUsdGHr{usdGH, uint64_t(90'0434788828413), -13}, //
6489 .goodUsdBIT{usdBIT, uint64_t(55'55), -2}, //
6490 .goodUsdBITr{usdBIT, uint64_t(55'55), -2}, //
6491 .lpTokenBalance{uint64_t(70'71067811865475), -14}, //
6492 .offer1BtcGH = 1e-5, //
6493 .offer2BtcGH = 1, //
6494 .offer2UsdGH = 1e-5, //
6495 .rateBIT = 0, //
6496 .rateGH = 0, //
6497 },
6498 InputSet{
6499 .testCase = "Overflow test {50, 100, 50.00}", //
6500 .poolUsdBIT = 50, //
6501 .poolUsdGH = 100, //
6502 .sendMaxUsdBIT{usdBIT(50.00)}, //
6503 .sendUsdGH{usdGH, 100}, //
6504 .failUsdGH{usdGH, uint64_t(52'94379354424081), -14}, //
6505 .failUsdGHr{usdGH, uint64_t(52'94379354424092), -14}, //
6506 .failUsdBIT{usdBIT, uint64_t(100), 0}, //
6507 .failUsdBITr{usdBIT, uint64_t(100), 0}, //
6508 .goodUsdGH{usdGH, uint64_t(52'94379354424081), -14}, //
6509 .goodUsdGHr{usdGH, uint64_t(52'94379354424092), -14}, //
6510 .goodUsdBIT{usdBIT, uint64_t(100), 0}, //
6511 .goodUsdBITr{usdBIT, uint64_t(100), 0}, //
6512 .lpTokenBalance{uint64_t(70'71067811865475), -14}, //
6513 .offer1BtcGH = 1e-5, //
6514 .offer2BtcGH = 1, //
6515 .offer2UsdGH = 1e-5, //
6516 .rateBIT = 0, //
6517 .rateGH = 0, //
6518 },
6519 InputSet{
6520 .testCase = "Overflow test {50, 100, 232.16}", //
6521 .poolUsdBIT = 50, //
6522 .poolUsdGH = 100, //
6523 .sendMaxUsdBIT{usdBIT(232.16)}, //
6524 .sendUsdGH{usdGH, 100}, //
6525 .failUsdGH = STAmount{0}, //
6526 .failUsdGHr = STAmount{0}, //
6527 .failUsdBIT{usdBIT, uint64_t(282'16), -2}, //
6528 .failUsdBITr{usdBIT, uint64_t(282'16), -2}, //
6529 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
6530 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
6531 .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13}, //
6532 .goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13}, //
6533 .lpTokenBalance{70'71067811865475, -14}, //
6534 .offer1BtcGH = 1e-5, //
6535 .offer2BtcGH = 1, //
6536 .offer2UsdGH = 1e-5, //
6537 .rateBIT = 0, //
6538 .rateGH = 0, //
6539 },
6540 InputSet{
6541 .testCase = "Overflow test {50, 100, 500}", //
6542 .poolUsdBIT = 50, //
6543 .poolUsdGH = 100, //
6544 .sendMaxUsdBIT{usdBIT(500)}, //
6545 .sendUsdGH{usdGH, 100}, //
6546 .failUsdGH = STAmount{0}, //
6547 .failUsdGHr = STAmount{0}, //
6548 .failUsdBIT{usdBIT, uint64_t(550), 0}, //
6549 .failUsdBITr{usdBIT, uint64_t(550), 0}, //
6550 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
6551 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
6552 .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13}, //
6553 .goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13}, //
6554 .lpTokenBalance{70'71067811865475, -14}, //
6555 .offer1BtcGH = 1e-5, //
6556 .offer2BtcGH = 1, //
6557 .offer2UsdGH = 1e-5, //
6558 .rateBIT = 0, //
6559 .rateGH = 0, //
6560 },
6561 })
6562 {
6563 testcase(input.testCase);
6564 for (auto const& features :
6565 {all - fixAMMOverflowOffer, all | fixAMMOverflowOffer})
6566 {
6567 Env env(*this, features);
6568
6569 env.fund(XRP(5'000), gatehub, bitstamp, trader);
6570 env.close();
6571
6572 if (input.rateGH != 0.0)
6573 env(rate(gatehub, input.rateGH));
6574 if (input.rateBIT != 0.0)
6575 env(rate(bitstamp, input.rateBIT));
6576
6577 env(trust(trader, usdGH(10'000'000)));
6578 env(trust(trader, usdBIT(10'000'000)));
6579 env(trust(trader, btcGH(10'000'000)));
6580 env.close();
6581
6582 env(pay(gatehub, trader, usdGH(100'000)));
6583 env(pay(gatehub, trader, btcGH(100'000)));
6584 env(pay(bitstamp, trader, usdBIT(100'000)));
6585 env.close();
6586
6587 AMM amm{
6588 env,
6589 trader,
6590 usdGH(input.poolUsdGH),
6591 usdBIT(input.poolUsdBIT)};
6592 env.close();
6593
6594 IOUAmount const preSwapLPTokenBalance =
6595 amm.getLPTokensBalance();
6596
6597 env(offer(trader, usdBIT(1), btcGH(input.offer1BtcGH)));
6598 env(offer(
6599 trader,
6600 btcGH(input.offer2BtcGH),
6601 usdGH(input.offer2UsdGH)));
6602 env.close();
6603
6604 env(pay(trader, trader, input.sendUsdGH),
6605 path(~usdGH),
6606 path(~btcGH, ~usdGH),
6607 sendmax(input.sendMaxUsdBIT),
6609 env.close();
6610
6611 auto const failUsdGH =
6612 features[fixAMMv1_1] ? input.failUsdGHr : input.failUsdGH;
6613 auto const failUsdBIT =
6614 features[fixAMMv1_1] ? input.failUsdBITr : input.failUsdBIT;
6615 auto const goodUsdGH =
6616 features[fixAMMv1_1] ? input.goodUsdGHr : input.goodUsdGH;
6617 auto const goodUsdBIT =
6618 features[fixAMMv1_1] ? input.goodUsdBITr : input.goodUsdBIT;
6619 if (!features[fixAMMOverflowOffer])
6620 {
6621 BEAST_EXPECT(amm.expectBalances(
6622 failUsdGH, failUsdBIT, input.lpTokenBalance));
6623 }
6624 else
6625 {
6626 BEAST_EXPECT(amm.expectBalances(
6627 goodUsdGH, goodUsdBIT, input.lpTokenBalance));
6628
6629 // Invariant: LPToken balance must not change in a
6630 // payment or a swap transaction
6631 BEAST_EXPECT(
6632 amm.getLPTokensBalance() == preSwapLPTokenBalance);
6633
6634 // Invariant: The square root of (product of the pool
6635 // balances) must be at least the LPTokenBalance
6636 Number const sqrtPoolProduct =
6637 root2(goodUsdGH * goodUsdBIT);
6638
6639 // Include a tiny tolerance for the test cases using
6640 // .goodUsdGH{usdGH, uint64_t(35'44113971506987),
6641 // -14}, .goodUsdBIT{usdBIT,
6642 // uint64_t(2'821579689703915), -15},
6643 // These two values multiply
6644 // to 99.99999999999994227040383754105 which gets
6645 // internally rounded to 100, due to representation
6646 // error.
6647 BEAST_EXPECT(
6648 (sqrtPoolProduct + Number{1, -14} >=
6649 input.lpTokenBalance));
6650 }
6651 }
6652 }
6653 }
6654
6655 void
6657 {
6658 testcase("swapRounding");
6659 using namespace jtx;
6660
6661 const STAmount xrpPool{XRP, UINT64_C(51600'000981)};
6662 const STAmount iouPool{USD, UINT64_C(803040'9987141784), -10};
6663
6664 const STAmount xrpBob{XRP, UINT64_C(1092'878933)};
6665 const STAmount iouBob{
6666 USD, UINT64_C(3'988035892323031), -28}; // 3.9...e-13
6667
6668 testAMM(
6669 [&](AMM& amm, Env& env) {
6670 // Check our AMM starting conditions.
6671 auto [xrpBegin, iouBegin, lptBegin] = amm.balances(XRP, USD);
6672
6673 // Set Bob's starting conditions.
6674 env.fund(xrpBob, bob);
6675 env.trust(USD(1'000'000), bob);
6676 env(pay(gw, bob, iouBob));
6677 env.close();
6678
6679 env(offer(bob, XRP(6300), USD(100'000)));
6680 env.close();
6681
6682 // Assert that AMM is unchanged.
6683 BEAST_EXPECT(
6684 amm.expectBalances(xrpBegin, iouBegin, amm.tokens()));
6685 },
6686 {{xrpPool, iouPool}},
6687 889,
6688 std::nullopt,
6689 {jtx::supported_amendments() | fixAMMv1_1});
6690 }
6691
6692 void
6694 {
6695 testcase("AMM Offer Blocked By LOB");
6696 using namespace jtx;
6697
6698 // Low quality LOB offer blocks AMM liquidity
6699
6700 // USD/XRP crosses AMM
6701 {
6702 Env env(*this, features);
6703
6704 fund(env, gw, {alice, carol}, XRP(1'000'000), {USD(1'000'000)});
6705 // This offer blocks AMM offer in pre-amendment
6706 env(offer(alice, XRP(1), USD(0.01)));
6707 env.close();
6708
6709 AMM amm(env, gw, XRP(200'000), USD(100'000));
6710
6711 // The offer doesn't cross AMM in pre-amendment code
6712 // It crosses AMM in post-amendment code
6713 env(offer(carol, USD(0.49), XRP(1)));
6714 env.close();
6715
6716 if (!features[fixAMMv1_1])
6717 {
6718 BEAST_EXPECT(amm.expectBalances(
6719 XRP(200'000), USD(100'000), amm.tokens()));
6720 BEAST_EXPECT(expectOffers(
6721 env, alice, 1, {{Amounts{XRP(1), USD(0.01)}}}));
6722 // Carol's offer is blocked by alice's offer
6723 BEAST_EXPECT(expectOffers(
6724 env, carol, 1, {{Amounts{USD(0.49), XRP(1)}}}));
6725 }
6726 else
6727 {
6728 BEAST_EXPECT(amm.expectBalances(
6729 XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens()));
6730 BEAST_EXPECT(expectOffers(
6731 env, alice, 1, {{Amounts{XRP(1), USD(0.01)}}}));
6732 // Carol's offer crosses AMM
6733 BEAST_EXPECT(expectOffers(env, carol, 0));
6734 }
6735 }
6736
6737 // There is no blocking offer, the same AMM liquidity is consumed
6738 // pre- and post-amendment.
6739 {
6740 Env env(*this, features);
6741
6742 fund(env, gw, {alice, carol}, XRP(1'000'000), {USD(1'000'000)});
6743 // There is no blocking offer
6744 // env(offer(alice, XRP(1), USD(0.01)));
6745
6746 AMM amm(env, gw, XRP(200'000), USD(100'000));
6747
6748 // The offer crosses AMM
6749 env(offer(carol, USD(0.49), XRP(1)));
6750 env.close();
6751
6752 // The same result as with the blocking offer
6753 BEAST_EXPECT(amm.expectBalances(
6754 XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens()));
6755 // Carol's offer crosses AMM
6756 BEAST_EXPECT(expectOffers(env, carol, 0));
6757 }
6758
6759 // XRP/USD crosses AMM
6760 {
6761 Env env(*this, features);
6762 fund(env, gw, {alice, carol, bob}, XRP(10'000), {USD(1'000)});
6763
6764 // This offer blocks AMM offer in pre-amendment
6765 // It crosses AMM in post-amendment code
6766 env(offer(bob, USD(1), XRPAmount(500)));
6767 env.close();
6768 AMM amm(env, alice, XRP(1'000), USD(500));
6769 env(offer(carol, XRP(100), USD(55)));
6770 env.close();
6771 if (!features[fixAMMv1_1])
6772 {
6773 BEAST_EXPECT(
6774 amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
6775 BEAST_EXPECT(expectOffers(
6776 env, bob, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
6777 BEAST_EXPECT(expectOffers(
6778 env, carol, 1, {{Amounts{XRP(100), USD(55)}}}));
6779 }
6780 else
6781 {
6782 BEAST_EXPECT(amm.expectBalances(
6783 XRPAmount(909'090'909),
6784 STAmount{USD, UINT64_C(550'000000055), -9},
6785 amm.tokens()));
6786 BEAST_EXPECT(expectOffers(
6787 env,
6788 carol,
6789 1,
6790 {{Amounts{
6791 XRPAmount{9'090'909},
6792 STAmount{USD, 4'99999995, -8}}}}));
6793 BEAST_EXPECT(expectOffers(
6794 env, bob, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
6795 }
6796 }
6797
6798 // There is no blocking offer, the same AMM liquidity is consumed
6799 // pre- and post-amendment.
6800 {
6801 Env env(*this, features);
6802 fund(env, gw, {alice, carol, bob}, XRP(10'000), {USD(1'000)});
6803
6804 AMM amm(env, alice, XRP(1'000), USD(500));
6805 env(offer(carol, XRP(100), USD(55)));
6806 env.close();
6807 BEAST_EXPECT(amm.expectBalances(
6808 XRPAmount(909'090'909),
6809 STAmount{USD, UINT64_C(550'000000055), -9},
6810 amm.tokens()));
6811 BEAST_EXPECT(expectOffers(
6812 env,
6813 carol,
6814 1,
6815 {{Amounts{
6816 XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
6817 }
6818 }
6819
6820 void
6822 {
6823 using namespace jtx;
6824
6825 // Last Liquidity Provider is the issuer of one token
6826 {
6827 Env env(*this, features);
6828 fund(
6829 env,
6830 gw,
6831 {alice, carol},
6832 XRP(1'000'000'000),
6833 {USD(1'000'000'000)});
6834 AMM amm(env, gw, XRP(2), USD(1));
6835 amm.deposit(alice, IOUAmount{1'876123487565916, -15});
6836 amm.deposit(carol, IOUAmount{1'000'000});
6837 amm.withdrawAll(alice);
6838 amm.withdrawAll(carol);
6839 auto const lpToken = getAccountLines(
6840 env, gw, amm.lptIssue())[jss::lines][0u][jss::balance];
6841 auto const lpTokenBalance =
6842 amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value];
6843 BEAST_EXPECT(
6844 lpToken == "1414.213562373095" &&
6845 lpTokenBalance == "1414.213562373");
6846 if (!features[fixAMMv1_1])
6847 {
6848 amm.withdrawAll(gw, std::nullopt, ter(tecAMM_BALANCE));
6849 BEAST_EXPECT(amm.ammExists());
6850 }
6851 else
6852 {
6853 amm.withdrawAll(gw);
6854 BEAST_EXPECT(!amm.ammExists());
6855 }
6856 }
6857
6858 // Last Liquidity Provider is the issuer of two tokens, or not
6859 // the issuer
6860 for (auto const& lp : {gw, bob})
6861 {
6862 Env env(*this, features);
6863 auto const ABC = gw["ABC"];
6864 fund(
6865 env,
6866 gw,
6867 {alice, carol, bob},
6868 XRP(1'000),
6869 {USD(1'000'000'000), ABC(1'000'000'000'000)});
6870 AMM amm(env, lp, ABC(2'000'000), USD(1));
6871 amm.deposit(alice, IOUAmount{1'876123487565916, -15});
6872 amm.deposit(carol, IOUAmount{1'000'000});
6873 amm.withdrawAll(alice);
6874 amm.withdrawAll(carol);
6875 auto const lpToken = getAccountLines(
6876 env, lp, amm.lptIssue())[jss::lines][0u][jss::balance];
6877 auto const lpTokenBalance =
6878 amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value];
6879 BEAST_EXPECT(
6880 lpToken == "1414.213562373095" &&
6881 lpTokenBalance == "1414.213562373");
6882 if (!features[fixAMMv1_1])
6883 {
6884 amm.withdrawAll(lp, std::nullopt, ter(tecAMM_BALANCE));
6885 BEAST_EXPECT(amm.ammExists());
6886 }
6887 else
6888 {
6889 amm.withdrawAll(lp);
6890 BEAST_EXPECT(!amm.ammExists());
6891 }
6892 }
6893
6894 // More than one Liquidity Provider
6895 // XRP/IOU
6896 {
6897 Env env(*this, features);
6898 fund(env, gw, {alice}, XRP(1'000), {USD(1'000)});
6899 AMM amm(env, gw, XRP(10), USD(10));
6900 amm.deposit(alice, 1'000);
6901 auto res =
6902 isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
6903 BEAST_EXPECT(res && !res.value());
6904 res =
6905 isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
6906 BEAST_EXPECT(res && !res.value());
6907 }
6908 // IOU/IOU, issuer of both IOU
6909 {
6910 Env env(*this, features);
6911 fund(env, gw, {alice}, XRP(1'000), {USD(1'000), EUR(1'000)});
6912 AMM amm(env, gw, EUR(10), USD(10));
6913 amm.deposit(alice, 1'000);
6914 auto res =
6915 isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
6916 BEAST_EXPECT(res && !res.value());
6917 res =
6918 isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
6919 BEAST_EXPECT(res && !res.value());
6920 }
6921 // IOU/IOU, issuer of one IOU
6922 {
6923 Env env(*this, features);
6924 Account const gw1("gw1");
6925 auto const YAN = gw1["YAN"];
6926 fund(env, gw, {gw1}, XRP(1'000), {USD(1'000)});
6927 fund(env, gw1, {gw}, XRP(1'000), {YAN(1'000)}, Fund::IOUOnly);
6928 AMM amm(env, gw1, YAN(10), USD(10));
6929 amm.deposit(gw, 1'000);
6930 auto res =
6931 isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
6932 BEAST_EXPECT(res && !res.value());
6933 res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw1);
6934 BEAST_EXPECT(res && !res.value());
6935 }
6936 }
6937
6938 void
6940 {
6941 testcase("test clawback from AMM account");
6942 using namespace jtx;
6943
6944 // Issuer has clawback enabled
6945 Env env(*this, features);
6946 env.fund(XRP(1'000), gw);
6948 fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct);
6949 env.close();
6950
6951 // If featureAMMClawback is not enabled, AMMCreate is not allowed for
6952 // clawback-enabled issuer
6953 if (!features[featureAMMClawback])
6954 {
6955 AMM amm(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION));
6956 AMM amm1(env, alice, USD(100), XRP(100), ter(tecNO_PERMISSION));
6958 env.close();
6959 // Can't be cleared
6960 AMM amm2(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION));
6961 }
6962 // If featureAMMClawback is enabled, AMMCreate is allowed for
6963 // clawback-enabled issuer. Clawback from the AMM Account is not
6964 // allowed, which will return tecAMM_ACCOUNT. We can only use
6965 // AMMClawback transaction to claw back from AMM Account.
6966 else
6967 {
6968 AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS));
6969 AMM amm1(env, alice, USD(100), XRP(200), ter(tecDUPLICATE));
6970
6971 // Construct the amount being clawed back using AMM account.
6972 // By doing this, we make the clawback transaction's Amount field's
6973 // subfield `issuer` to be the AMM account, which means
6974 // we are clawing back from an AMM account. This should return an
6975 // tecAMM_ACCOUNT error because regular Clawback transaction is not
6976 // allowed for clawing back from an AMM account. Please notice the
6977 // `issuer` subfield represents the account being clawed back, which
6978 // is confusing.
6979 Issue usd(USD.issue().currency, amm.ammAccount());
6980 auto amount = amountFromString(usd, "10");
6981 env(claw(gw, amount), ter(tecAMM_ACCOUNT));
6982 }
6983 }
6984
6985 void
6987 {
6988 testcase("test AMMDeposit with frozen assets");
6989 using namespace jtx;
6990
6991 // This lambda function is used to create trustlines
6992 // between gw and alice, and create an AMM account.
6993 // And also test the callback function.
6994 auto testAMMDeposit = [&](Env& env, std::function<void(AMM & amm)> cb) {
6995 env.fund(XRP(1'000), gw);
6996 fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct);
6997 env.close();
6998 AMM amm(env, alice, XRP(100), USD(100), ter(tesSUCCESS));
6999 env(trust(gw, alice["USD"](0), tfSetFreeze));
7000 cb(amm);
7001 };
7002
7003 // Deposit two assets, one of which is frozen,
7004 // then we should get tecFROZEN error.
7005 {
7006 Env env(*this, features);
7007 testAMMDeposit(env, [&](AMM& amm) {
7008 amm.deposit(
7009 alice,
7010 USD(100),
7011 XRP(100),
7012 std::nullopt,
7013 tfTwoAsset,
7014 ter(tecFROZEN));
7015 });
7016 }
7017
7018 // Deposit one asset, which is the frozen token,
7019 // then we should get tecFROZEN error.
7020 {
7021 Env env(*this, features);
7022 testAMMDeposit(env, [&](AMM& amm) {
7023 amm.deposit(
7024 alice,
7025 USD(100),
7026 std::nullopt,
7027 std::nullopt,
7029 ter(tecFROZEN));
7030 });
7031 }
7032
7033 if (features[featureAMMClawback])
7034 {
7035 // Deposit one asset which is not the frozen token,
7036 // but the other asset is frozen. We should get tecFROZEN error
7037 // when feature AMMClawback is enabled.
7038 Env env(*this, features);
7039 testAMMDeposit(env, [&](AMM& amm) {
7040 amm.deposit(
7041 alice,
7042 XRP(100),
7043 std::nullopt,
7044 std::nullopt,
7046 ter(tecFROZEN));
7047 });
7048 }
7049 else
7050 {
7051 // Deposit one asset which is not the frozen token,
7052 // but the other asset is frozen. We will get tecSUCCESS
7053 // when feature AMMClawback is not enabled.
7054 Env env(*this, features);
7055 testAMMDeposit(env, [&](AMM& amm) {
7056 amm.deposit(
7057 alice,
7058 XRP(100),
7059 std::nullopt,
7060 std::nullopt,
7062 ter(tesSUCCESS));
7063 });
7064 }
7065 }
7066
7067 void
7069 {
7070 testcase("Fix Reserve Check On Withdrawal");
7071 using namespace jtx;
7072
7073 auto const err = features[fixAMMv1_2] ? ter(tecINSUFFICIENT_RESERVE)
7074 : ter(tesSUCCESS);
7075
7076 auto test = [&](auto&& cb) {
7077 Env env(*this, features);
7078 auto const starting_xrp =
7079 reserve(env, 2) + env.current()->fees().base * 5;
7080 env.fund(starting_xrp, gw);
7081 env.fund(starting_xrp, alice);
7082 env.trust(USD(2'000), alice);
7083 env.close();
7084 env(pay(gw, alice, USD(2'000)));
7085 env.close();
7086 AMM amm(env, gw, EUR(1'000), USD(1'000));
7087 amm.deposit(alice, USD(1));
7088 cb(amm);
7089 };
7090
7091 // Equal withdraw
7092 test([&](AMM& amm) { amm.withdrawAll(alice, std::nullopt, err); });
7093
7094 // Equal withdraw with a limit
7095 test([&](AMM& amm) {
7096 amm.withdraw(WithdrawArg{
7097 .account = alice,
7098 .asset1Out = EUR(0.1),
7099 .asset2Out = USD(0.1),
7100 .err = err});
7101 amm.withdraw(WithdrawArg{
7102 .account = alice,
7103 .asset1Out = USD(0.1),
7104 .asset2Out = EUR(0.1),
7105 .err = err});
7106 });
7107
7108 // Single withdraw
7109 test([&](AMM& amm) {
7110 amm.withdraw(WithdrawArg{
7111 .account = alice, .asset1Out = EUR(0.1), .err = err});
7112 amm.withdraw(WithdrawArg{.account = alice, .asset1Out = USD(0.1)});
7113 });
7114 }
7115
7116 void
7117 run() override
7118 {
7119 FeatureBitset const all{jtx::supported_amendments()};
7120 testInvalidInstance();
7121 testInstanceCreate();
7122 testInvalidDeposit(all);
7123 testInvalidDeposit(all - featureAMMClawback);
7124 testDeposit();
7125 testInvalidWithdraw();
7126 testWithdraw();
7127 testInvalidFeeVote();
7128 testFeeVote();
7129 testInvalidBid();
7130 testBid(all);
7131 testBid(all - fixAMMv1_1);
7132 testInvalidAMMPayment();
7133 testBasicPaymentEngine(all);
7134 testBasicPaymentEngine(all - fixAMMv1_1);
7135 testBasicPaymentEngine(all - fixReducedOffersV2);
7136 testBasicPaymentEngine(all - fixAMMv1_1 - fixReducedOffersV2);
7137 testAMMTokens();
7138 testAmendment();
7139 testFlags();
7140 testRippling();
7141 testAMMAndCLOB(all);
7142 testAMMAndCLOB(all - fixAMMv1_1);
7143 testTradingFee(all);
7144 testTradingFee(all - fixAMMv1_1);
7145 testAdjustedTokens(all);
7146 testAdjustedTokens(all - fixAMMv1_1);
7147 testAutoDelete();
7148 testClawback();
7149 testAMMID();
7150 testSelection(all);
7151 testSelection(all - fixAMMv1_1);
7152 testFixDefaultInnerObj();
7153 testMalformed();
7154 testFixOverflowOffer(all);
7155 testFixOverflowOffer(all - fixAMMv1_1);
7156 testSwapRounding();
7157 testFixChangeSpotPriceQuality(all);
7158 testFixChangeSpotPriceQuality(all - fixAMMv1_1);
7159 testFixAMMOfferBlockedByLOB(all);
7160 testFixAMMOfferBlockedByLOB(all - fixAMMv1_1);
7161 testLPTokenBalance(all);
7162 testLPTokenBalance(all - fixAMMv1_1);
7163 testAMMClawback(all);
7164 testAMMClawback(all - featureAMMClawback);
7165 testAMMClawback(all - fixAMMv1_1 - featureAMMClawback);
7166 testAMMDepositWithFrozenAssets(all);
7167 testAMMDepositWithFrozenAssets(all - featureAMMClawback);
7168 testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback);
7169 testFixReserveCheckOnWithdrawal(all);
7170 testFixReserveCheckOnWithdrawal(all - fixAMMv1_2);
7171 }
7172};
7173
7174BEAST_DEFINE_TESTSUITE_PRIO(AMM, app, ripple, 1);
7175
7176} // namespace test
7177} // namespace ripple
Represents a JSON value.
Definition: json_value.h:147
std::string asString() const
Returns the unquoted string value.
Definition: json_value.cpp:469
testcase_t testcase
Memberspace for declaring test cases.
Definition: suite.h:153
RAII class to set and restore the current transaction rules.
Definition: Rules.h:108
Floating point representation of amounts with high dynamic range.
Definition: IOUAmount.h:45
std::int64_t mantissa() const noexcept
Definition: IOUAmount.h:171
A currency issued by an account.
Definition: Issue.h:36
Currency currency
Definition: Issue.h:38
constexpr int exponent() const noexcept
Definition: Number.h:218
constexpr rep mantissa() const noexcept
Definition: Number.h:212
Json::Value getJson(JsonOptions) const override
Definition: STIssue.cpp:98
jtx::Account const alice
Definition: AMMTest.h:66
jtx::Account const gw
Definition: AMMTest.h:64
jtx::Account const bob
Definition: AMMTest.h:67
jtx::Account const carol
Definition: AMMTest.h:65
void testAMM(std::function< void(jtx::AMM &, jtx::Env &)> &&cb, std::optional< std::pair< STAmount, STAmount > > const &pool=std::nullopt, std::uint16_t tfee=0, std::optional< jtx::ter > const &ter=std::nullopt, std::vector< FeatureBitset > const &features={supported_amendments()})
testAMM() funds 30,000XRP and 30,000IOU for each non-XRP asset to Alice and Carol
Definition: AMMTest.cpp:101
XRPAmount ammCrtFee(jtx::Env &env) const
Definition: AMMTest.cpp:159
XRPAmount reserve(jtx::Env &env, std::uint32_t count) const
Definition: AMMTest.cpp:153
Convenience class to test AMM functionality.
Definition: AMM.h:122
Json::Value ammRpcInfo(std::optional< AccountID > const &account=std::nullopt, std::optional< std::string > const &ledgerIndex=std::nullopt, std::optional< Issue > issue1=std::nullopt, std::optional< Issue > issue2=std::nullopt, std::optional< AccountID > const &ammAccount=std::nullopt, bool ignoreParams=false, unsigned apiVersion=RPC::apiInvalidVersion) const
Send amm_info RPC command.
Definition: AMM.cpp:160
void vote(std::optional< Account > const &account, std::uint32_t feeVal, std::optional< std::uint32_t > const &flags=std::nullopt, std::optional< jtx::seq > const &seq=std::nullopt, std::optional< std::pair< Issue, Issue > > const &assets=std::nullopt, std::optional< ter > const &ter=std::nullopt)
Definition: AMM.cpp:636
bool expectAuctionSlot(std::uint32_t fee, std::optional< std::uint8_t > timeSlot, IOUAmount expectedPrice) const
Definition: AMM.cpp:274
IOUAmount tokens() const
Definition: AMM.h:335
AccountID const & ammAccount() const
Definition: AMM.h:323
bool expectTradingFee(std::uint16_t fee) const
Definition: AMM.cpp:311
IOUAmount withdraw(std::optional< Account > const &account, std::optional< LPToken > const &tokens, std::optional< STAmount > const &asset1OutDetails=std::nullopt, std::optional< std::uint32_t > const &flags=std::nullopt, std::optional< ter > const &ter=std::nullopt)
Definition: AMM.cpp:536
bool expectBalances(STAmount const &asset1, STAmount const &asset2, IOUAmount const &lpt, std::optional< AccountID > const &account=std::nullopt) const
Verify the AMM balances.
Definition: AMM.cpp:231
Issue lptIssue() const
Definition: AMM.h:329
IOUAmount deposit(std::optional< Account > const &account, LPToken tokens, std::optional< STAmount > const &asset1InDetails=std::nullopt, std::optional< std::uint32_t > const &flags=std::nullopt, std::optional< ter > const &ter=std::nullopt)
Definition: AMM.cpp:410
IOUAmount getLPTokensBalance(std::optional< AccountID > const &account=std::nullopt) const
Definition: AMM.cpp:244
Json::Value bid(BidArg const &arg)
Definition: AMM.cpp:663
void setTokens(Json::Value &jv, std::optional< std::pair< Issue, Issue > > const &assets=std::nullopt)
Definition: AMM.cpp:370
bool ammExists() const
Definition: AMM.cpp:319
IOUAmount withdrawAll(std::optional< Account > const &account, std::optional< STAmount > const &asset1OutDetails=std::nullopt, std::optional< ter > const &ter=std::nullopt)
Definition: AMM.h:271
bool expectLPTokens(AccountID const &account, IOUAmount const &tokens) const
Definition: AMM.cpp:261
Immutable cryptographic account descriptor.
Definition: Account.h:38
AccountID id() const
Returns the Account ID.
Definition: Account.h:106
std::string const & human() const
Returns the human readable public key.
Definition: Account.h:113
A transaction testing environment.
Definition: Env.h:117
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition: Env.cpp:115
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition: Env.h:325
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition: Env.cpp:121
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition: Env.cpp:268
Account const & master
Definition: Env.h:121
NetClock::time_point now()
Returns the current network time.
Definition: Env.h:278
beast::Journal const journal
Definition: Env.h:158
Json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition: Env.h:764
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition: Env.cpp:237
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition: Env.cpp:451
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition: Env.cpp:183
void memoize(Account const &account)
Associate AccountID with account.
Definition: Env.cpp:156
std::shared_ptr< SLE const > le(Account const &account) const
Return an account root.
Definition: Env.cpp:225
A balance matches.
Definition: balance.h:39
Set the fee on a JTx.
Definition: fee.h:36
Match set account flags.
Definition: flags.h:112
Add a path.
Definition: paths.h:56
Sets the SendMax on a JTx.
Definition: sendmax.h:32
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition: ter.h:34
Set the flags on a JTx.
Definition: txflags.h:31
T make_pair(T... args)
@ objectValue
object value (collection of name/value pairs).
Definition: json_value.h:43
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition: Indexes.cpp:422
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition: Indexes.cpp:350
Json::Value pay(Account const &account, AccountID const &to, STAmount const &amount)
Definition: AMM.cpp:816
Json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
Definition: flags.h:40
Json::Value claw(Account const &account, STAmount const &amount, std::optional< Account > const &mptHolder)
Definition: trust.cpp:67
bool expectOffers(Env &env, AccountID const &account, std::uint16_t size, std::vector< Amounts > const &toMatch)
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
Json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition: trust.cpp:30
Json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition: flags.cpp:28
bool expectLine(Env &env, AccountID const &account, STAmount const &value, bool defaultLimits)
Json::Value getAccountLines(Env &env, AccountID const &acctId)
Definition: TestHelpers.cpp:40
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition: pay.cpp:29
void fund(jtx::Env &env, jtx::Account const &gw, std::vector< jtx::Account > const &accounts, std::vector< STAmount > const &amts, Fund how)
Definition: AMMTest.cpp:35
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition: envconfig.h:54
Json::Value accountBalance(Env &env, Account const &acct)
Json::Value rate(Account const &account, double multiplier)
Set a transfer rate.
Definition: rate.cpp:30
std::array< std::uint8_t, 39 > constexpr cb1
Definition: TestHelpers.h:255
Json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition: offer.cpp:28
bool expectLedgerEntryRoot(Env &env, Account const &acct, STAmount const &expectedValue)
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition: amount.cpp:104
XRPAmount txfee(Env const &env, std::uint16_t n)
Definition: TestHelpers.cpp:93
FeatureBitset supported_amendments()
Definition: Env.h:70
Json::Value getAccountOffers(Env &env, AccountID const &acct, bool current)
Definition: TestHelpers.cpp:32
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: algorithm.h:26
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition: Issue.h:118
STAmount amountFromString(Asset const &issue, std::string const &amount)
Definition: STAmount.cpp:834
constexpr std::uint32_t tfSingleAsset
Definition: TxFlags.h:204
constexpr std::uint32_t asfGlobalFreeze
Definition: TxFlags.h:82
constexpr std::uint32_t tfOneAssetWithdrawAll
Definition: TxFlags.h:203
std::uint32_t constexpr TOTAL_TIME_SLOT_SECS
Definition: AMMCore.h:34
bool isXRP(AccountID const &c)
Definition: AccountID.h:91
std::uint16_t constexpr AUCTION_SLOT_TIME_INTERVALS
Definition: AMMCore.h:35
std::optional< Number > solveQuadraticEqSmallest(Number const &a, Number const &b, Number const &c)
Solve quadratic equation to find takerGets or takerPays.
Definition: AMMHelpers.cpp:227
@ telINSUF_FEE_P
Definition: TER.h:57
@ Fail
Should not be retried in this ledger.
Issue getIssue(T const &amt)
TOut swapAssetIn(TAmounts< TIn, TOut > const &pool, TIn const &assetIn, std::uint16_t tfee)
AMM pool invariant - the product (A * B) after swap in/out has to remain at least the same: (A + in) ...
Definition: AMMHelpers.h:465
@ lsfDefaultRipple
@ lsfDisableMaster
@ lsfDepositAuth
constexpr std::uint32_t tfLimitLPToken
Definition: TxFlags.h:207
constexpr std::uint32_t const tfBurnable
Definition: TxFlags.h:131
constexpr std::uint32_t tfPassive
Definition: TxFlags.h:96
constexpr std::uint32_t tfOneAssetLPToken
Definition: TxFlags.h:206
Expected< bool, TER > isOnlyLiquidityProvider(ReadView const &view, Issue const &ammIssue, AccountID const &lpAccount)
Return true if the Liquidity Provider is the only AMM provider, false otherwise.
Definition: AMMUtils.cpp:353
@ tefEXCEPTION
Definition: TER.h:172
constexpr std::uint32_t tfTwoAsset
Definition: TxFlags.h:205
constexpr std::uint32_t tfPartialPayment
Definition: TxFlags.h:105
constexpr std::uint32_t tfWithdrawAll
Definition: TxFlags.h:202
base_uint< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition: UintTypes.h:56
std::optional< TAmounts< TIn, TOut > > changeSpotPriceQuality(TAmounts< TIn, TOut > const &pool, Quality const &quality, std::uint16_t tfee, Rules const &rules, beast::Journal j)
Generate AMM offer so that either updated Spot Price Quality (SPQ) is equal to LOB quality (in this c...
Definition: AMMHelpers.h:332
constexpr std::uint32_t tfSetfAuth
Definition: TxFlags.h:112
constexpr std::uint32_t asfDefaultRipple
Definition: TxFlags.h:83
constexpr std::uint32_t tfClearFreeze
Definition: TxFlags.h:116
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition: Issue.h:126
@ tecINSUF_RESERVE_LINE
Definition: TER.h:275
@ tecINCOMPLETE
Definition: TER.h:322
@ tecFROZEN
Definition: TER.h:290
@ tecAMM_EMPTY
Definition: TER.h:319
@ tecOWNERS
Definition: TER.h:285
@ tecDUPLICATE
Definition: TER.h:302
@ tecNO_PERMISSION
Definition: TER.h:292
@ tecAMM_NOT_EMPTY
Definition: TER.h:320
@ tecPATH_PARTIAL
Definition: TER.h:269
@ tecUNFUNDED_AMM
Definition: TER.h:315
@ tecAMM_ACCOUNT
Definition: TER.h:321
@ tecAMM_FAILED
Definition: TER.h:317
@ tecPATH_DRY
Definition: TER.h:281
@ tecAMM_INVALID_TOKENS
Definition: TER.h:318
@ tecAMM_BALANCE
Definition: TER.h:316
@ tecINSUFFICIENT_RESERVE
Definition: TER.h:294
@ tecNO_AUTH
Definition: TER.h:287
constexpr std::uint32_t tfLPToken
Definition: TxFlags.h:201
constexpr std::uint32_t tfNoRippleDirect
Definition: TxFlags.h:104
@ tesSUCCESS
Definition: TER.h:242
std::uint32_t constexpr AUCTION_SLOT_INTERVAL_DURATION
Definition: AMMCore.h:40
constexpr std::uint32_t tfLimitQuality
Definition: TxFlags.h:106
constexpr std::uint32_t tfTwoAssetIfEmpty
Definition: TxFlags.h:208
constexpr std::uint32_t asfAllowTrustLineClawback
Definition: TxFlags.h:93
std::uint16_t constexpr maxDeletableAMMTrustLines
The maximum number of trustlines to delete as part of AMM account deletion cleanup.
Definition: Protocol.h:130
constexpr std::uint32_t asfRequireAuth
Definition: TxFlags.h:77
@ terNO_ACCOUNT
Definition: TER.h:217
@ terNO_RIPPLE
Definition: TER.h:224
@ terNO_AMM
Definition: TER.h:227
constexpr std::uint32_t tfSetFreeze
Definition: TxFlags.h:115
STAmount withdrawByTokens(STAmount const &assetBalance, STAmount const &lptAMMBalance, STAmount const &lpTokens, std::uint16_t tfee)
Calculate asset withdrawal by tokens.
Definition: AMMHelpers.cpp:114
bool withinRelativeDistance(Quality const &calcQuality, Quality const &reqQuality, Number const &dist)
Check if the relative distance between the qualities is within the requested distance.
Definition: AMMHelpers.h:130
Number root2(Number f)
Definition: Number.cpp:695
@ temBAD_AMOUNT
Definition: TER.h:89
@ temBAD_FEE
Definition: TER.h:92
@ temBAD_CURRENCY
Definition: TER.h:90
@ temMALFORMED
Definition: TER.h:87
@ temBAD_AMM_TOKENS
Definition: TER.h:129
@ temINVALID_FLAG
Definition: TER.h:111
@ temDISABLED
Definition: TER.h:114
TIn swapAssetOut(TAmounts< TIn, TOut > const &pool, TOut const &assetOut, std::uint16_t tfee)
Swap assetOut out of the pool and swap in a proportional amount of the other asset.
Definition: AMMHelpers.h:538
T push_back(T... args)
Zero allows classes to offer efficient comparisons to zero.
Definition: Zero.h:43
Basic tests of AMM that do not use offers.
Definition: AMM_test.cpp:51
void testBid(FeatureBitset features)
Definition: AMM_test.cpp:2839
void testInvalidDeposit(FeatureBitset features)
Definition: AMM_test.cpp:422
void testSelection(FeatureBitset features)
Definition: AMM_test.cpp:5475
void testAMMClawback(FeatureBitset features)
Definition: AMM_test.cpp:6939
void testLPTokenBalance(FeatureBitset features)
Definition: AMM_test.cpp:6821
void run() override
Runs the suite.
Definition: AMM_test.cpp:7117
void testTradingFee(FeatureBitset features)
Definition: AMM_test.cpp:4752
void testFixOverflowOffer(FeatureBitset features)
Definition: AMM_test.cpp:6333
void testAMMAndCLOB(FeatureBitset features)
Definition: AMM_test.cpp:4681
void testBasicPaymentEngine(FeatureBitset features)
Definition: AMM_test.cpp:3510
void testFixChangeSpotPriceQuality(FeatureBitset features)
Definition: AMM_test.cpp:6058
void testFixReserveCheckOnWithdrawal(FeatureBitset features)
Definition: AMM_test.cpp:7068
void testAdjustedTokens(FeatureBitset features)
Definition: AMM_test.cpp:5141
void testAMMDepositWithFrozenAssets(FeatureBitset features)
Definition: AMM_test.cpp:6986
void testFixAMMOfferBlockedByLOB(FeatureBitset features)
Definition: AMM_test.cpp:6693
std::optional< Account > account
Definition: AMM.h:74
std::optional< STAmount > asset1In
Definition: AMM.h:76
std::optional< Account > account
Definition: AMM.h:101
std::uint32_t tfee
Definition: AMM.h:102
std::optional< Account > account
Definition: AMM.h:88
std::optional< std::uint32_t > flags
Definition: AMM.h:93
std::optional< STAmount > asset1Out
Definition: AMM.h:90
std::optional< LPToken > tokens
Definition: AMM.h:89
std::optional< ter > err
Definition: AMM.h:96
Set the "CancelAfter" time tag on a JTx.
Definition: TestHelpers.h:284
Set the "FinishAfter" time tag on a JTx.
Definition: TestHelpers.h:266
Set the sequence number on a JTx.
Definition: seq.h:34
T to_string(T... args)
T what(T... args)