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