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