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 xrpl {
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 succeeds 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 // Invalid 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()->header().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 xrpl::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, xrpl, 1);
7956
7957} // namespace test
7958} // namespace xrpl
Represents a JSON value.
Definition json_value.h:131
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 rep mantissa() const noexcept
Definition Number.h:209
constexpr int exponent() const noexcept
Definition Number.h:215
Issue const & issue() const
Definition STAmount.h:488
Json::Value getJson(JsonOptions) const override
Definition STIssue.cpp:83
jtx::Account const gw
Definition AMMTest.h:56
jtx::Account const bob
Definition AMMTest.h:59
jtx::Account const alice
Definition AMMTest.h:58
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
bool expectTradingFee(std::uint16_t fee) const
Definition AMM.cpp:298
IOUAmount getLPTokensBalance(std::optional< AccountID > const &account=std::nullopt) const
Definition AMM.cpp:231
Issue lptIssue() const
Definition AMM.h:318
bool ammExists() const
Definition AMM.cpp:306
void setTokens(Json::Value &jv, std::optional< std::pair< Issue, Issue > > const &assets=std::nullopt)
Definition AMM.cpp:357
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
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
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
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
AccountID const & ammAccount() const
Definition AMM.h:312
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
Json::Value bid(BidArg const &arg)
Definition AMM.cpp:650
bool expectLPTokens(AccountID const &account, IOUAmount const &tokens) const
Definition AMM.cpp:248
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
Immutable cryptographic account descriptor.
Definition Account.h:20
std::string const & human() const
Returns the human readable public key.
Definition Account.h:99
AccountID id() const
Returns the Account ID.
Definition Account.h:92
A transaction testing environment.
Definition Env.h:102
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:104
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:98
std::shared_ptr< SLE const > le(Account const &account) const
Return an account root.
Definition Env.cpp:260
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:272
Account const & master
Definition Env.h:106
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:166
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:303
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:774
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:488
bool enabled(uint256 feature) const
Definition Env.h:621
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:139
beast::Journal const journal
Definition Env.h:143
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:314
NetClock::time_point now()
Returns the current network time.
Definition Env.h:267
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:27
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:356
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:428
Json::Value pay(Account const &account, AccountID const &to, STAmount const &amount)
Definition AMM.cpp:803
bool expectLedgerEntryRoot(Env &env, Account const &acct, STAmount const &expectedValue)
Json::Value getAccountOffers(Env &env, AccountID const &acct, bool current)
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)
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
bool expectHolding(Env &env, AccountID const &account, STAmount const &value, bool defaultLimits)
Json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:13
Json::Value rate(Account const &account, double multiplier)
Set a transfer rate.
Definition rate.cpp:13
XRPAmount txfee(Env const &env, std::uint16_t n)
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:11
Json::Value accountBalance(Env &env, Account const &acct)
Json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
Definition flags.h:102
FeatureBitset testable_amendments()
Definition Env.h:55
auto const amount
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:35
Json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:10
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
Json::Value getAccountLines(Env &env, AccountID const &acctId)
Json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:10
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
constexpr std::uint32_t asfAllowTrustLineClawback
Definition TxFlags.h:75
std::uint32_t constexpr AUCTION_SLOT_INTERVAL_DURATION
Definition AMMCore.h:21
@ telINSUF_FEE_P
Definition TER.h:38
@ terNO_AMM
Definition TER.h:208
@ terNO_RIPPLE
Definition TER.h:205
@ terADDRESS_COLLISION
Definition TER.h:209
@ terNO_ACCOUNT
Definition TER.h:198
base_uint< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition UintTypes.h:37
constexpr std::uint32_t asfGlobalFreeze
Definition TxFlags.h:64
constexpr std::uint32_t tfPassive
Definition TxFlags.h:79
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:96
std::uint16_t constexpr AUCTION_SLOT_TIME_INTERVALS
Definition AMMCore.h:16
AccountID pseudoAccountAddress(ReadView const &view, uint256 const &pseudoOwnerKey)
Definition View.cpp:1175
constexpr std::uint32_t tfWithdrawAll
Definition TxFlags.h:226
bool isXRP(AccountID const &c)
Definition AccountID.h:71
constexpr std::uint32_t tfSingleAsset
Definition TxFlags.h:228
std::uint32_t constexpr TOTAL_TIME_SLOT_SECS
Definition AMMCore.h:15
constexpr std::uint32_t const tfBurnable
Definition TxFlags.h:120
@ tefEXCEPTION
Definition TER.h:153
std::uint16_t constexpr maxDeletableAMMTrustLines
The maximum number of trustlines to delete as part of AMM account deletion cleanup.
Definition Protocol.h:266
@ Fail
Should not be retried in this ledger.
constexpr std::uint32_t tfLimitQuality
Definition TxFlags.h:90
STAmount amountFromString(Asset const &asset, std::string const &amount)
Definition STAmount.cpp:977
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
constexpr std::uint32_t tfTwoAsset
Definition TxFlags.h:229
STAmount ammAssetOut(STAmount const &assetBalance, STAmount const &lptAMMBalance, STAmount const &lpTokens, std::uint16_t tfee)
Calculate asset withdrawal by tokens.
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:369
constexpr std::uint32_t tfOneAssetLPToken
Definition TxFlags.h:230
constexpr std::uint32_t asfDefaultRipple
Definition TxFlags.h:65
constexpr std::uint32_t tfOneAssetWithdrawAll
Definition TxFlags.h:227
constexpr std::uint32_t tfClearFreeze
Definition TxFlags.h:100
constexpr std::uint32_t tfTwoAssetIfEmpty
Definition TxFlags.h:232
std::optional< Number > solveQuadraticEqSmallest(Number const &a, Number const &b, Number const &c)
Solve quadratic equation to find takerGets or takerPays.
constexpr std::uint32_t tfNoRippleDirect
Definition TxFlags.h:88
Number root2(Number f)
Definition Number.cpp:709
constexpr std::uint32_t tfSetfAuth
Definition TxFlags.h:96
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 asfRequireAuth
Definition TxFlags.h:59
@ temBAD_CURRENCY
Definition TER.h:71
@ temBAD_FEE
Definition TER.h:73
@ temBAD_AMM_TOKENS
Definition TER.h:110
@ temINVALID_FLAG
Definition TER.h:92
@ temMALFORMED
Definition TER.h:68
@ temDISABLED
Definition TER.h:95
@ temBAD_AMOUNT
Definition TER.h:70
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
constexpr std::uint32_t tfLimitLPToken
Definition TxFlags.h:231
constexpr std::uint32_t tfLPToken
Definition TxFlags.h:225
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition Issue.h:104
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
@ tecPSEUDO_ACCOUNT
Definition TER.h:344
@ tecAMM_EMPTY
Definition TER.h:314
@ tecPATH_PARTIAL
Definition TER.h:264
@ tecAMM_INVALID_TOKENS
Definition TER.h:313
@ tecINSUF_RESERVE_LINE
Definition TER.h:270
@ tecAMM_FAILED
Definition TER.h:312
@ tecAMM_NOT_EMPTY
Definition TER.h:315
@ tecPATH_DRY
Definition TER.h:276
@ tecINCOMPLETE
Definition TER.h:317
@ tecUNFUNDED_AMM
Definition TER.h:310
@ tecNO_AUTH
Definition TER.h:282
@ tecAMM_BALANCE
Definition TER.h:311
@ tecINVARIANT_FAILED
Definition TER.h:295
@ tecFROZEN
Definition TER.h:285
@ tecAMM_ACCOUNT
Definition TER.h:316
@ tecOWNERS
Definition TER.h:280
@ tecINSUFFICIENT_RESERVE
Definition TER.h:289
@ tecNO_PERMISSION
Definition TER.h:287
@ tecDUPLICATE
Definition TER.h:297
@ lsfDepositAuth
@ lsfDefaultRipple
@ lsfDisableMaster
constexpr std::uint32_t tfPartialPayment
Definition TxFlags.h:89
constexpr std::uint32_t tfSetFreeze
Definition TxFlags.h:99
@ tesSUCCESS
Definition TER.h:226
Issue getIssue(T const &amt)
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 testAMMAndCLOB(FeatureBitset features)
void run() override
Runs the suite.
void testTradingFee(FeatureBitset features)
void testWithdrawRounding(FeatureBitset all)
void testFixAMMOfferBlockedByLOB(FeatureBitset features)
void testSelection(FeatureBitset features)
void testInvalidDeposit(FeatureBitset features)
Definition AMM_test.cpp:419
void invariant(jtx::AMM &amm, jtx::Env &env, std::string const &msg, bool shouldFail)
void testBid(FeatureBitset features)
void testFixChangeSpotPriceQuality(FeatureBitset features)
void testDepositRounding(FeatureBitset all)
void testFixOverflowOffer(FeatureBitset featuresInitial)
void testAMMClawback(FeatureBitset features)
void testLPTokenBalance(FeatureBitset features)
void testAMMDepositWithFrozenAssets(FeatureBitset features)
void testAdjustedTokens(FeatureBitset features)
void testBasicPaymentEngine(FeatureBitset features)
void testFixReserveCheckOnWithdrawal(FeatureBitset features)
void testDepositAndWithdrawRounding(FeatureBitset features)
std::uint16_t tfee
Definition AMM.h:46
std::optional< LPToken > tokens
Definition AMM.h:58
std::optional< Account > account
Definition AMM.h:57
std::optional< STAmount > asset1In
Definition AMM.h:59
std::uint32_t tfee
Definition AMM.h:85
std::optional< Account > account
Definition AMM.h:84
std::optional< ter > err
Definition AMM.h:79
std::optional< STAmount > asset1Out
Definition AMM.h:73
std::optional< std::uint32_t > flags
Definition AMM.h:76
std::optional< Account > account
Definition AMM.h:71
std::optional< LPToken > tokens
Definition AMM.h:72
Set the sequence number on a JTx.
Definition seq.h:15
T to_string(T... args)
T what(T... args)