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