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