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