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