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