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