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