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 - featureLendingProtocol;
43 }
44
45 void
47 {
48 testcase("Instance Create");
49
50 using namespace jtx;
51
52#if NUMBERTODO
53 // XRP to IOU, with featureSingleAssetVault
54 testAMM(
55 [&](AMM& ammAlice, Env&) {
56 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
57 },
58 {},
59 0,
60 {},
61 {testable_amendments() | featureSingleAssetVault});
62#endif
63
64 // XRP to IOU, without featureSingleAssetVault
65 testAMM(
66 [&](AMM& ammAlice, Env&) {
67 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
68 },
69 {},
70 0,
71 {},
72 {testable_amendments() - featureSingleAssetVault});
73
74 // IOU to IOU
75 testAMM(
76 [&](AMM& ammAlice, Env&) {
77 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
78 },
79 {{USD(20'000), BTC(0.5)}});
80
81 // IOU to IOU + transfer fee
82 {
83 Env env{*this};
84 fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
85 env(rate(gw, 1.25));
86 env.close();
87 // no transfer fee on create
88 AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
89 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
90 BEAST_EXPECT(expectHolding(env, alice, USD(0)));
91 BEAST_EXPECT(expectHolding(env, alice, BTC(0)));
92 }
93
94 // Require authorization is set, account is authorized
95 {
96 Env env{*this};
97 env.fund(XRP(30'000), gw, alice);
98 env.close();
99 env(fset(gw, asfRequireAuth));
100 env(trust(alice, gw["USD"](30'000), 0));
101 env(trust(gw, alice["USD"](0), tfSetfAuth));
102 env.close();
103 env(pay(gw, alice, USD(10'000)));
104 env.close();
105 AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
106 }
107
108 // Cleared global freeze
109 {
110 Env env{*this};
111 env.fund(XRP(30'000), gw, alice);
112 env.close();
113 env.trust(USD(30'000), alice);
114 env.close();
115 env(pay(gw, alice, USD(10'000)));
116 env.close();
117 env(fset(gw, asfGlobalFreeze));
118 env.close();
119 AMM ammAliceFail(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
121 env.close();
122 AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
123 }
124
125 // Trading fee
126 testAMM(
127 [&](AMM& amm, Env&) {
128 BEAST_EXPECT(amm.expectTradingFee(1'000));
129 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
130 },
132 1'000);
133
134 // Make sure asset comparison works.
135 BEAST_EXPECT(STIssue(sfAsset, STAmount(XRP(2'000)).issue()) == STIssue(sfAsset, STAmount(XRP(2'000)).issue()));
136 BEAST_EXPECT(STIssue(sfAsset, STAmount(XRP(2'000)).issue()) != STIssue(sfAsset, STAmount(USD(2'000)).issue()));
137 }
138
139 void
141 {
142 testcase("Invalid Instance");
143
144 using namespace jtx;
145
146 // Can't have both XRP tokens
147 {
148 Env env{*this};
149 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
150 AMM ammAlice(env, alice, XRP(10'000), XRP(10'000), ter(temBAD_AMM_TOKENS));
151 BEAST_EXPECT(!ammAlice.ammExists());
152 }
153
154 // Can't have both tokens the same IOU
155 {
156 Env env{*this};
157 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
158 AMM ammAlice(env, alice, USD(10'000), USD(10'000), ter(temBAD_AMM_TOKENS));
159 BEAST_EXPECT(!ammAlice.ammExists());
160 }
161
162 // Can't have zero or negative amounts
163 {
164 Env env{*this};
165 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
166 AMM ammAlice(env, alice, XRP(0), USD(10'000), ter(temBAD_AMOUNT));
167 BEAST_EXPECT(!ammAlice.ammExists());
168 AMM ammAlice1(env, alice, XRP(10'000), USD(0), ter(temBAD_AMOUNT));
169 BEAST_EXPECT(!ammAlice1.ammExists());
170 AMM ammAlice2(env, alice, XRP(10'000), USD(-10'000), ter(temBAD_AMOUNT));
171 BEAST_EXPECT(!ammAlice2.ammExists());
172 AMM ammAlice3(env, alice, XRP(-10'000), USD(10'000), ter(temBAD_AMOUNT));
173 BEAST_EXPECT(!ammAlice3.ammExists());
174 }
175
176 // Bad currency
177 {
178 Env env{*this};
179 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
180 AMM ammAlice(env, alice, XRP(10'000), BAD(10'000), ter(temBAD_CURRENCY));
181 BEAST_EXPECT(!ammAlice.ammExists());
182 }
183
184 // Insufficient IOU balance
185 {
186 Env env{*this};
187 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
188 AMM ammAlice(env, alice, XRP(10'000), USD(40'000), ter(tecUNFUNDED_AMM));
189 BEAST_EXPECT(!ammAlice.ammExists());
190 }
191
192 // Insufficient XRP balance
193 {
194 Env env{*this};
195 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
196 AMM ammAlice(env, alice, XRP(40'000), USD(10'000), ter(tecUNFUNDED_AMM));
197 BEAST_EXPECT(!ammAlice.ammExists());
198 }
199
200 // Invalid trading fee
201 {
202 Env env{*this};
203 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
204 AMM ammAlice(
205 env,
206 alice,
207 XRP(10'000),
208 USD(10'000),
209 false,
210 65'001,
211 10,
215 ter(temBAD_FEE));
216 BEAST_EXPECT(!ammAlice.ammExists());
217 }
218
219 // AMM already exists
220 testAMM(
221 [&](AMM& ammAlice, Env& env) { AMM ammCarol(env, carol, XRP(10'000), USD(10'000), ter(tecDUPLICATE)); });
222
223 // Invalid flags
224 {
225 Env env{*this};
226 fund(env, gw, {alice}, {USD(30'000)}, Fund::All);
227 AMM ammAlice(
228 env,
229 alice,
230 XRP(10'000),
231 USD(10'000),
232 false,
233 0,
234 10,
239 BEAST_EXPECT(!ammAlice.ammExists());
240 }
241
242 // Invalid Account
243 {
244 Env env{*this};
245 Account bad("bad");
246 env.memoize(bad);
247 AMM ammAlice(
248 env,
249 bad,
250 XRP(10'000),
251 USD(10'000),
252 false,
253 0,
254 10,
256 seq(1),
259 BEAST_EXPECT(!ammAlice.ammExists());
260 }
261
262 // Require authorization is set
263 {
264 Env env{*this};
265 env.fund(XRP(30'000), gw, alice);
266 env.close();
267 env(fset(gw, asfRequireAuth));
268 env.close();
269 env(trust(gw, alice["USD"](30'000)));
270 env.close();
271 AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecNO_AUTH));
272 BEAST_EXPECT(!ammAlice.ammExists());
273 }
274
275 // Globally frozen
276 {
277 Env env{*this};
278 env.fund(XRP(30'000), gw, alice);
279 env.close();
280 env(fset(gw, asfGlobalFreeze));
281 env.close();
282 env(trust(gw, alice["USD"](30'000)));
283 env.close();
284 AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
285 BEAST_EXPECT(!ammAlice.ammExists());
286 }
287
288 // Individually frozen
289 {
290 Env env{*this};
291 env.fund(XRP(30'000), gw, alice);
292 env.close();
293 env(trust(gw, alice["USD"](30'000)));
294 env.close();
295 env(trust(gw, alice["USD"](0), tfSetFreeze));
296 env.close();
297 AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(tecFROZEN));
298 BEAST_EXPECT(!ammAlice.ammExists());
299 }
300
301 // Insufficient reserve, XRP/IOU
302 {
303 Env env(*this);
304 auto const starting_xrp = XRP(1'000) + reserve(env, 3) + env.current()->fees().base * 4;
305 env.fund(starting_xrp, gw);
306 env.fund(starting_xrp, alice);
307 env.trust(USD(2'000), alice);
308 env.close();
309 env(pay(gw, alice, USD(2'000)));
310 env.close();
311 env(offer(alice, XRP(101), USD(100)));
312 env(offer(alice, XRP(102), USD(100)));
313 AMM ammAlice(env, alice, XRP(1'000), USD(1'000), ter(tecUNFUNDED_AMM));
314 }
315
316 // Insufficient reserve, IOU/IOU
317 {
318 Env env(*this);
319 auto const starting_xrp = reserve(env, 4) + env.current()->fees().base * 5;
320 env.fund(starting_xrp, gw);
321 env.fund(starting_xrp, alice);
322 env.trust(USD(2'000), alice);
323 env.trust(EUR(2'000), alice);
324 env.close();
325 env(pay(gw, alice, USD(2'000)));
326 env(pay(gw, alice, EUR(2'000)));
327 env.close();
328 env(offer(alice, EUR(101), USD(100)));
329 env(offer(alice, EUR(102), USD(100)));
330 AMM ammAlice(env, alice, EUR(1'000), USD(1'000), ter(tecINSUF_RESERVE_LINE));
331 }
332
333 // Insufficient fee
334 {
335 Env env(*this);
336 fund(env, gw, {alice}, XRP(2'000), {USD(2'000), EUR(2'000)});
337 AMM ammAlice(
338 env,
339 alice,
340 EUR(1'000),
341 USD(1'000),
342 false,
343 0,
344 ammCrtFee(env).drops() - 1,
349 }
350
351 // AMM with LPTokens
352
353 // AMM with one LPToken from another AMM.
354 testAMM([&](AMM& ammAlice, Env& env) {
355 fund(env, gw, {alice}, {EUR(10'000)}, Fund::IOUOnly);
356 AMM ammAMMToken(
357 env, alice, EUR(10'000), STAmount{ammAlice.lptIssue(), 1'000'000}, ter(tecAMM_INVALID_TOKENS));
358 AMM ammAMMToken1(
359 env, alice, STAmount{ammAlice.lptIssue(), 1'000'000}, EUR(10'000), ter(tecAMM_INVALID_TOKENS));
360 });
361
362 // AMM with two LPTokens from other AMMs.
363 testAMM([&](AMM& ammAlice, Env& env) {
364 fund(env, gw, {alice}, {EUR(10'000)}, Fund::IOUOnly);
365 AMM ammAlice1(env, alice, XRP(10'000), EUR(10'000));
366 auto const token1 = ammAlice.lptIssue();
367 auto const token2 = ammAlice1.lptIssue();
368 AMM ammAMMTokens(
369 env, alice, STAmount{token1, 1'000'000}, STAmount{token2, 1'000'000}, ter(tecAMM_INVALID_TOKENS));
370 });
371
372 // Issuer has DefaultRipple disabled
373 {
374 Env env(*this);
375 env.fund(XRP(30'000), gw);
377 AMM ammGw(env, gw, XRP(10'000), USD(10'000), ter(terNO_RIPPLE));
378 env.fund(XRP(30'000), alice);
379 env.trust(USD(30'000), alice);
380 env(pay(gw, alice, USD(30'000)));
381 AMM ammAlice(env, alice, XRP(10'000), USD(10'000), ter(terNO_RIPPLE));
382 Account const gw1("gw1");
383 env.fund(XRP(30'000), gw1);
384 env(fclear(gw1, asfDefaultRipple));
385 env.trust(USD(30'000), gw1);
386 env(pay(gw, gw1, USD(30'000)));
387 auto const USD1 = gw1["USD"];
388 AMM ammGwGw1(env, gw, USD(10'000), USD1(10'000), ter(terNO_RIPPLE));
389 env.trust(USD1(30'000), alice);
390 env(pay(gw1, alice, USD1(30'000)));
391 AMM ammAlice1(env, alice, USD(10'000), USD1(10'000), ter(terNO_RIPPLE));
392 }
393 }
394
395 void
397 {
398 testcase("Invalid Deposit");
399
400 using namespace jtx;
401
402 testAMM([&](AMM& ammAlice, Env& env) {
403 // Invalid flags
405
406 // Invalid options
414 invalidOptions = {
415 // flags, tokens, asset1In, asset2in, EPrice, tfee
420 {tfLPToken, 1'000, XRP(100), std::nullopt, STAmount{USD, 1, -1}, std::nullopt},
427 {tfTwoAsset, std::nullopt, XRP(100), USD(100), STAmount{USD, 1, -1}, std::nullopt},
429 {tfTwoAsset, std::nullopt, XRP(100), USD(100), std::nullopt, 1'000},
434 {tfOneAssetLPToken, 1'000, XRP(100), std::nullopt, std::nullopt, 1'000},
438 {tfLimitLPToken, std::nullopt, XRP(100), std::nullopt, STAmount{USD, 1, -1}, 1'000},
444 XRP(100),
445 USD(100),
446 STAmount{USD, 1, -1},
447 std::nullopt}};
448 for (auto const& it : invalidOptions)
449 {
450 ammAlice.deposit(
451 alice,
452 std::get<1>(it),
453 std::get<2>(it),
454 std::get<3>(it),
455 std::get<4>(it),
456 std::get<0>(it),
459 std::get<5>(it),
461 }
462
463 {
464 // bad preflight1
466 jv[jss::Account] = alice.human();
467 jv[jss::TransactionType] = jss::AMMDeposit;
468 jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::none);
469 jv[jss::Asset2] = STIssue(sfAsset, USD).getJson(JsonOptions::none);
470 jv[jss::Fee] = "-1";
471 env(jv, ter(temBAD_FEE));
472 }
473
474 // Invalid tokens
477
478 {
480 jv[jss::Account] = alice.human();
481 jv[jss::TransactionType] = jss::AMMDeposit;
482 jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::none);
483 jv[jss::Asset2] = STIssue(sfAsset, USD).getJson(JsonOptions::none);
484 jv[jss::LPTokenOut] = USD(100).value().getJson(JsonOptions::none);
485 jv[jss::Flags] = tfLPToken;
486 env(jv, ter(temBAD_AMM_TOKENS));
487 }
488
489 // Invalid trading fee
490 ammAlice.deposit(
491 carol,
493 XRP(200),
494 USD(200),
499 10'000,
500 ter(temBAD_FEE));
501
502 // Invalid tokens - bogus currency
503 {
504 auto const iss1 = Issue{Currency(0xabc), gw.id()};
505 auto const iss2 = Issue{Currency(0xdef), gw.id()};
506 ammAlice.deposit(
507 alice,
508 1'000,
513 {{iss1, iss2}},
516 ter(terNO_AMM));
517 }
518
519 // Depositing mismatched token, invalid Asset1In.issue
521
522 // Depositing mismatched token, invalid Asset2In.issue
524
525 // Depositing mismatched token, Asset1In.issue == Asset2In.issue
527
528 // Invalid amount value
532
533 // Bad currency
535
536 // Invalid Account
537 Account bad("bad");
538 env.memoize(bad);
539 ammAlice.deposit(
540 bad,
541 1'000'000,
547 seq(1),
550
551 // Invalid AMM
552 ammAlice.deposit(
553 alice,
554 1'000,
559 {{USD, GBP}},
562 ter(terNO_AMM));
563
564 // Single deposit: 100000 tokens worth of USD
565 // Amount to deposit exceeds Max
566 ammAlice.deposit(
567 carol,
568 100'000,
569 USD(200),
577
578 // Single deposit: 100000 tokens worth of XRP
579 // Amount to deposit exceeds Max
580 ammAlice.deposit(
581 carol,
582 100'000,
583 XRP(200),
591
592 // Deposit amount is invalid
593 // Calculated amount to deposit is 98,000,000
595 // Calculated amount is 0
596 ammAlice.deposit(alice, USD(0), std::nullopt, STAmount{USD, 2'000, -6}, std::nullopt, ter(tecAMM_FAILED));
597
598 // Deposit non-empty AMM
600 });
601
602 // Tiny deposit
603 testAMM(
604 [&](AMM& ammAlice, Env& env) {
605 auto const enabledV1_3 = env.current()->rules().enabled(fixAMMv1_3);
606 auto const err = !enabledV1_3 ? ter(temBAD_AMOUNT) : ter(tesSUCCESS);
607 // Pre-amendment XRP deposit side is rounded to 0
608 // and deposit fails.
609 // Post-amendment XRP deposit side is rounded to 1
610 // and deposit succeeds.
611 ammAlice.deposit(carol, IOUAmount{1, -4}, std::nullopt, std::nullopt, err);
612 // Pre/post-amendment LPTokens is rounded to 0 and deposit
613 // fails with tecAMM_INVALID_TOKENS.
614 ammAlice.deposit(
616 },
618 0,
620 {features, features - fixAMMv1_3});
621
622 // Invalid AMM
623 testAMM([&](AMM& ammAlice, Env& env) {
624 ammAlice.withdrawAll(alice);
625 ammAlice.deposit(alice, 10'000, std::nullopt, std::nullopt, ter(terNO_AMM));
626 });
627
628 // Globally frozen asset
629 testAMM(
630 [&](AMM& ammAlice, Env& env) {
631 env(fset(gw, asfGlobalFreeze));
632 if (!features[featureAMMClawback])
633 // If the issuer set global freeze, the holder still can
634 // deposit the other non-frozen token when AMMClawback is
635 // not enabled.
636 ammAlice.deposit(carol, XRP(100));
637 else
638 // If the issuer set global freeze, the holder cannot
639 // deposit the other non-frozen token when AMMClawback is
640 // enabled.
643 ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
644 ammAlice.deposit(carol, XRP(100), USD(100), std::nullopt, std::nullopt, ter(tecFROZEN));
645 },
647 0,
649 {features});
650
651 // Individually frozen (AMM) account
652 testAMM(
653 [&](AMM& ammAlice, Env& env) {
654 env(trust(gw, carol["USD"](0), tfSetFreeze));
655 env.close();
656 if (!features[featureAMMClawback])
657 // Can deposit non-frozen token if AMMClawback is not
658 // enabled
659 ammAlice.deposit(carol, XRP(100));
660 else
661 // Cannot deposit non-frozen token if the other token is
662 // frozen when AMMClawback is enabled
664
665 ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
667 env(trust(gw, carol["USD"](0), tfClearFreeze));
668 // Individually frozen AMM
669 env(trust(gw, STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
670 env.close();
671 // Can deposit non-frozen token
672 ammAlice.deposit(carol, XRP(100));
673 ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
675 },
677 0,
679 {features});
680
681 // Individually frozen (AMM) account with IOU/IOU AMM
682 testAMM(
683 [&](AMM& ammAlice, Env& env) {
684 env(trust(gw, carol["USD"](0), tfSetFreeze));
685 env(trust(gw, carol["BTC"](0), tfSetFreeze));
686 env.close();
687 ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
689 env(trust(gw, carol["USD"](0), tfClearFreeze));
690 // Individually frozen AMM
691 env(trust(gw, STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
692 env.close();
693 // Cannot deposit non-frozen token
694 ammAlice.deposit(carol, 1'000'000, std::nullopt, std::nullopt, ter(tecFROZEN));
695 ammAlice.deposit(carol, USD(100), BTC(0.01), std::nullopt, std::nullopt, ter(tecFROZEN));
696 },
697 {{USD(20'000), BTC(0.5)}});
698
699 // Deposit unauthorized token.
700 {
701 Env env(*this, features);
702 env.fund(XRP(1000), gw, alice, bob);
703 env(fset(gw, asfRequireAuth));
704 env.close();
705 env(trust(gw, alice["USD"](100)), txflags(tfSetfAuth));
706 env(trust(alice, gw["USD"](20)));
707 env.close();
708 env(pay(gw, alice, gw["USD"](10)));
709 env.close();
710 env(trust(gw, bob["USD"](100)));
711 env.close();
712
713 AMM amm(env, alice, XRP(10), gw["USD"](10), ter(tesSUCCESS));
714 env.close();
715
716 if (features[featureAMMClawback])
717 // if featureAMMClawback is enabled, bob can not deposit XRP
718 // because he's not authorized to hold the paired token
719 // gw["USD"].
721 else
723 }
724
725 // Insufficient XRP balance
726 testAMM([&](AMM& ammAlice, Env& env) {
727 env.fund(XRP(1'000), bob);
728 env.close();
729 // Adds LPT trustline
730 ammAlice.deposit(bob, XRP(10));
732 });
733
734 // Insufficient USD balance
735 testAMM([&](AMM& ammAlice, Env& env) {
736 fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
737 env.close();
739 });
740
741 // Insufficient USD balance by tokens
742 testAMM([&](AMM& ammAlice, Env& env) {
743 fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
744 env.close();
745 ammAlice.deposit(
746 bob,
747 10'000'000,
756 });
757
758 // Insufficient XRP balance by tokens
759 testAMM([&](AMM& ammAlice, Env& env) {
760 env.fund(XRP(1'000), bob);
761 env.trust(USD(100'000), bob);
762 env.close();
763 env(pay(gw, bob, USD(90'000)));
764 env.close();
765 ammAlice.deposit(
766 bob,
767 10'000'000,
776 });
777
778 // Insufficient reserve, XRP/IOU
779 {
780 Env env(*this);
781 auto const starting_xrp = reserve(env, 4) + env.current()->fees().base * 4;
782 env.fund(XRP(10'000), gw);
783 env.fund(XRP(10'000), alice);
784 env.fund(starting_xrp, carol);
785 env.trust(USD(2'000), alice);
786 env.trust(USD(2'000), carol);
787 env.close();
788 env(pay(gw, alice, USD(2'000)));
789 env(pay(gw, carol, USD(2'000)));
790 env.close();
791 env(offer(carol, XRP(100), USD(101)));
792 env(offer(carol, XRP(100), USD(102)));
793 AMM ammAlice(env, alice, XRP(1'000), USD(1'000));
795
796 env(offer(carol, XRP(100), USD(103)));
798 }
799
800 // Insufficient reserve, IOU/IOU
801 {
802 Env env(*this);
803 auto const starting_xrp = reserve(env, 4) + env.current()->fees().base * 4;
804 env.fund(XRP(10'000), gw);
805 env.fund(XRP(10'000), alice);
806 env.fund(starting_xrp, carol);
807 env.trust(USD(2'000), alice);
808 env.trust(EUR(2'000), alice);
809 env.trust(USD(2'000), carol);
810 env.trust(EUR(2'000), carol);
811 env.close();
812 env(pay(gw, alice, USD(2'000)));
813 env(pay(gw, alice, EUR(2'000)));
814 env(pay(gw, carol, USD(2'000)));
815 env(pay(gw, carol, EUR(2'000)));
816 env.close();
817 env(offer(carol, XRP(100), USD(101)));
818 env(offer(carol, XRP(100), USD(102)));
819 AMM ammAlice(env, alice, XRP(1'000), USD(1'000));
821 }
822
823 // Invalid min
824 testAMM([&](AMM& ammAlice, Env& env) {
825 // min tokens can't be <= zero
826 ammAlice.deposit(carol, 0, XRP(100), tfSingleAsset, ter(temBAD_AMM_TOKENS));
827 ammAlice.deposit(carol, -1, XRP(100), tfSingleAsset, ter(temBAD_AMM_TOKENS));
828 ammAlice.deposit(
829 carol,
830 0,
831 XRP(100),
832 USD(100),
839 // min amounts can't be <= zero
840 ammAlice.deposit(
841 carol,
842 1'000,
843 XRP(0),
844 USD(100),
851 ammAlice.deposit(
852 carol,
853 1'000,
854 XRP(100),
855 USD(-1),
862 // min amount bad currency
863 ammAlice.deposit(
864 carol,
865 1'000,
866 XRP(100),
867 BAD(100),
874 // min amount bad token pair
875 ammAlice.deposit(
876 carol,
877 1'000,
878 XRP(100),
879 XRP(100),
886 ammAlice.deposit(
887 carol,
888 1'000,
889 XRP(100),
890 GBP(100),
897 });
898
899 // Min deposit
900 testAMM([&](AMM& ammAlice, Env& env) {
901 // Equal deposit by tokens
902 ammAlice.deposit(
903 carol,
904 1'000'000,
905 XRP(1'000),
906 USD(1'001),
908 tfLPToken,
913 ammAlice.deposit(
914 carol,
915 1'000'000,
916 XRP(1'001),
917 USD(1'000),
919 tfLPToken,
924 // Equal deposit by asset
925 ammAlice.deposit(
926 carol,
927 100'001,
928 XRP(100),
929 USD(100),
936 // Single deposit by asset
937 ammAlice.deposit(
938 carol,
939 488'090,
940 XRP(1'000),
948 });
949
950 // Equal deposit, tokens rounded to 0
951 testAMM([&](AMM& amm, Env& env) {
952 amm.deposit(DepositArg{.tokens = IOUAmount{1, -12}, .err = ter(tecAMM_INVALID_TOKENS)});
953 });
954
955 // Equal deposit limit, tokens rounded to 0
956 testAMM(
957 [&](AMM& amm, Env& env) {
958 amm.deposit(DepositArg{
959 .asset1In = STAmount{USD, 1, -15}, .asset2In = XRPAmount{1}, .err = ter(tecAMM_INVALID_TOKENS)});
960 },
961 {.pool = {{USD(1'000'000), XRP(1'000'000)}}, .features = {features - fixAMMv1_3}});
962 testAMM([&](AMM& amm, Env& env) {
963 amm.deposit(DepositArg{
964 .asset1In = STAmount{USD, 1, -15}, .asset2In = XRPAmount{1}, .err = ter(tecAMM_INVALID_TOKENS)});
965 });
966
967 // Single deposit by asset, tokens rounded to 0
968 testAMM([&](AMM& amm, Env& env) {
969 amm.deposit(DepositArg{.asset1In = STAmount{USD, 1, -15}, .err = ter(tecAMM_INVALID_TOKENS)});
970 });
971
972 // Single deposit by tokens, tokens rounded to 0
973 testAMM([&](AMM& amm, Env& env) {
974 amm.deposit(DepositArg{
975 .tokens = IOUAmount{1, -10}, .asset1In = STAmount{USD, 1, -15}, .err = ter(tecAMM_INVALID_TOKENS)});
976 });
977
978 // Single deposit with eprice, tokens rounded to 0
979 testAMM([&](AMM& amm, Env& env) {
980 amm.deposit(DepositArg{
981 .asset1In = STAmount{USD, 1, -15}, .maxEP = STAmount{USD, 1, -1}, .err = ter(tecAMM_INVALID_TOKENS)});
982 });
983 }
984
985 void
987 {
988 testcase("Deposit");
989
990 auto const all = testable_amendments();
991 using namespace jtx;
992
993 // Equal deposit: 1000000 tokens, 10% of the current pool
994 testAMM([&](AMM& ammAlice, Env& env) {
995 auto const baseFee = env.current()->fees().base;
996 ammAlice.deposit(carol, 1'000'000);
997 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
998 // 30,000 less deposited 1,000
999 BEAST_EXPECT(expectHolding(env, carol, USD(29'000)));
1000 // 30,000 less deposited 1,000 and 10 drops tx fee
1001 BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRPAmount{29'000'000'000 - baseFee}));
1002 });
1003
1004 // equal asset deposit: unit test to exercise the rounding-down of
1005 // LPTokens in the AMMHelpers.cpp: adjustLPTokens calculations
1006 // The LPTokens need to have 16 significant digits and a fractional part
1007 for (Number const& deltaLPTokens :
1008 {Number{UINT64_C(100000'0000000009), -10}, Number{UINT64_C(100000'0000000001), -10}})
1009 {
1010 testAMM([&](AMM& ammAlice, Env& env) {
1011 // initial LPToken balance
1012 IOUAmount const initLPToken = ammAlice.getLPTokensBalance();
1013 IOUAmount const newLPTokens{deltaLPTokens};
1014
1015 // carol performs a two-asset deposit
1016 ammAlice.deposit(DepositArg{.account = carol, .tokens = newLPTokens});
1017
1018 IOUAmount const finalLPToken = ammAlice.getLPTokensBalance();
1019
1020 // Change in behavior due to rounding down of LPTokens:
1021 // there is a decrease in the observed return of LPTokens --
1022 // Inputs Number{UINT64_C(100000'0000000001), -10} and
1023 // Number{UINT64_C(100000'0000000009), -10} are both rounded
1024 // down to 1e5
1025 BEAST_EXPECT((finalLPToken - initLPToken == IOUAmount{1, 5}));
1026 BEAST_EXPECT(finalLPToken - initLPToken < deltaLPTokens);
1027
1028 // fraction of newLPTokens/(existing LPToken balance). The
1029 // existing LPToken balance is 1e7
1030 Number const fr = deltaLPTokens / 1e7;
1031
1032 // The below equations are based on Equation 1, 2 from XLS-30d
1033 // specification, Section: 2.3.1.2
1034 Number const deltaXRP = fr * 1e10;
1035 Number const deltaUSD = fr * 1e4;
1036
1037 STAmount const depositUSD = STAmount{USD, deltaUSD};
1038
1039 STAmount const depositXRP = STAmount{XRP, deltaXRP};
1040
1041 // initial LPTokens (1e7) + newLPTokens
1042 BEAST_EXPECT(ammAlice.expectBalances(
1043 XRP(10'000) + depositXRP, USD(10'000) + depositUSD, IOUAmount{1, 7} + newLPTokens));
1044
1045 // 30,000 less deposited depositUSD
1046 BEAST_EXPECT(expectHolding(env, carol, USD(30'000) - depositUSD));
1047 // 30,000 less deposited depositXRP and 10 drops tx fee
1048 BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRP(30'000) - depositXRP - txfee(env, 1)));
1049 });
1050 }
1051
1052 // Equal limit deposit: deposit USD100 and XRP proportionally
1053 // to the pool composition not to exceed 100XRP. If the amount
1054 // exceeds 100XRP then deposit 100XRP and USD proportionally
1055 // to the pool composition not to exceed 100USD. Fail if exceeded.
1056 // Deposit 100USD/100XRP
1057 testAMM([&](AMM& ammAlice, Env&) {
1058 ammAlice.deposit(carol, USD(100), XRP(100));
1059 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
1060 });
1061
1062 // Equal limit deposit.
1063 // Try to deposit 200USD/100XRP. Is truncated to 100USD/100XRP.
1064 testAMM([&](AMM& ammAlice, Env&) {
1065 ammAlice.deposit(carol, USD(200), XRP(100));
1066 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
1067 });
1068 // Try to deposit 100USD/200XRP. Is truncated to 100USD/100XRP.
1069 testAMM([&](AMM& ammAlice, Env&) {
1070 ammAlice.deposit(carol, USD(100), XRP(200));
1071 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'100), IOUAmount{10'100'000, 0}));
1072 });
1073
1074 // Single deposit: 1000 USD
1075 testAMM([&](AMM& ammAlice, Env&) {
1076 ammAlice.deposit(carol, USD(1'000));
1077 BEAST_EXPECT(ammAlice.expectBalances(
1078 XRP(10'000), STAmount{USD, UINT64_C(10'999'99999999999), -11}, IOUAmount{10'488'088'48170151, -8}));
1079 });
1080
1081 // Single deposit: 1000 XRP
1082 testAMM([&](AMM& ammAlice, Env&) {
1083 ammAlice.deposit(carol, XRP(1'000));
1084 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8}));
1085 });
1086
1087 // Single deposit: 100000 tokens worth of USD
1088 testAMM([&](AMM& ammAlice, Env&) {
1089 ammAlice.deposit(carol, 100000, USD(205));
1090 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'201), IOUAmount{10'100'000, 0}));
1091 });
1092
1093 // Single deposit: 100000 tokens worth of XRP
1094 testAMM([&](AMM& ammAlice, Env& env) {
1095 ammAlice.deposit(carol, 100'000, XRP(205));
1096 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'201), USD(10'000), IOUAmount{10'100'000, 0}));
1097 });
1098
1099 // Single deposit with EP not exceeding specified:
1100 // 100USD with EP not to exceed 0.1 (AssetIn/TokensOut)
1101 testAMM([&](AMM& ammAlice, Env&) {
1102 ammAlice.deposit(carol, USD(1'000), std::nullopt, STAmount{USD, 1, -1});
1103 BEAST_EXPECT(ammAlice.expectBalances(
1104 XRP(10'000), STAmount{USD, UINT64_C(10'999'99999999999), -11}, IOUAmount{10'488'088'48170151, -8}));
1105 });
1106
1107 // Single deposit with EP not exceeding specified:
1108 // 100USD with EP not to exceed 0.002004 (AssetIn/TokensOut)
1109 testAMM([&](AMM& ammAlice, Env&) {
1110 ammAlice.deposit(carol, USD(100), std::nullopt, STAmount{USD, 2004, -6});
1111 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), STAmount{USD, 10'080'16, -2}, IOUAmount{10'040'000, 0}));
1112 });
1113
1114 // Single deposit with EP not exceeding specified:
1115 // 0USD with EP not to exceed 0.002004 (AssetIn/TokensOut)
1116 testAMM([&](AMM& ammAlice, Env&) {
1117 ammAlice.deposit(carol, USD(0), std::nullopt, STAmount{USD, 2004, -6});
1118 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), STAmount{USD, 10'080'16, -2}, IOUAmount{10'040'000, 0}));
1119 });
1120
1121 // IOU to IOU + transfer fee
1122 {
1123 Env env{*this};
1124 fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
1125 env(rate(gw, 1.25));
1126 env.close();
1127 AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
1128 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
1129 BEAST_EXPECT(expectHolding(env, alice, USD(0)));
1130 BEAST_EXPECT(expectHolding(env, alice, BTC(0)));
1131 fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct);
1132 // no transfer fee on deposit
1133 ammAlice.deposit(carol, 10);
1134 BEAST_EXPECT(ammAlice.expectBalances(USD(22'000), BTC(0.55), IOUAmount{110, 0}));
1135 BEAST_EXPECT(expectHolding(env, carol, USD(0)));
1136 BEAST_EXPECT(expectHolding(env, carol, BTC(0)));
1137 }
1138
1139 // Tiny deposits
1140 testAMM([&](AMM& ammAlice, Env&) {
1141 ammAlice.deposit(carol, IOUAmount{1, -3});
1142 BEAST_EXPECT(ammAlice.expectBalances(
1143 XRPAmount{10'000'000'001}, STAmount{USD, UINT64_C(10'000'000001), -6}, IOUAmount{10'000'000'001, -3}));
1144 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1, -3}));
1145 });
1146 testAMM([&](AMM& ammAlice, Env&) {
1147 ammAlice.deposit(carol, XRPAmount{1});
1148 BEAST_EXPECT(
1149 ammAlice.expectBalances(XRPAmount{10'000'000'001}, USD(10'000), IOUAmount{1'000'000'000049999, -8}));
1150 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{49999, -8}));
1151 });
1152 testAMM([&](AMM& ammAlice, Env&) {
1153 ammAlice.deposit(carol, STAmount{USD, 1, -10});
1154 BEAST_EXPECT(ammAlice.expectBalances(
1155 XRP(10'000), STAmount{USD, UINT64_C(10'000'00000000008), -11}, IOUAmount{10'000'000'00000004, -8}));
1156 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4, -8}));
1157 });
1158
1159 // Issuer create/deposit
1160 for (auto const& feat : {all, all - fixAMMv1_3})
1161 {
1162 Env env(*this, feat);
1163 env.fund(XRP(30000), gw);
1164 AMM ammGw(env, gw, XRP(10'000), USD(10'000));
1165 BEAST_EXPECT(ammGw.expectBalances(XRP(10'000), USD(10'000), ammGw.tokens()));
1166 ammGw.deposit(gw, 1'000'000);
1167 BEAST_EXPECT(ammGw.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000}));
1168 ammGw.deposit(gw, USD(1'000));
1169 BEAST_EXPECT(ammGw.expectBalances(
1170 XRP(11'000), STAmount{USD, UINT64_C(11'999'99999999998), -11}, IOUAmount{11'489'125'29307605, -8}));
1171 }
1172
1173 // Issuer deposit
1174 testAMM([&](AMM& ammAlice, Env& env) {
1175 ammAlice.deposit(gw, 1'000'000);
1176 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000}));
1177 ammAlice.deposit(gw, USD(1'000));
1178 BEAST_EXPECT(ammAlice.expectBalances(
1179 XRP(11'000), STAmount{USD, UINT64_C(11'999'99999999998), -11}, IOUAmount{11'489'125'29307605, -8}));
1180 });
1181
1182 // Min deposit
1183 testAMM([&](AMM& ammAlice, Env& env) {
1184 // Equal deposit by tokens
1185 ammAlice.deposit(
1186 carol, 1'000'000, XRP(1'000), USD(1'000), std::nullopt, tfLPToken, std::nullopt, std::nullopt);
1187 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1188 });
1189 testAMM([&](AMM& ammAlice, Env& env) {
1190 // Equal deposit by asset
1191 ammAlice.deposit(
1192 carol, 1'000'000, XRP(1'000), USD(1'000), std::nullopt, tfTwoAsset, std::nullopt, std::nullopt);
1193 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1194 });
1195 testAMM([&](AMM& ammAlice, Env& env) {
1196 // Single deposit by asset
1197 ammAlice.deposit(
1199 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(10'000), IOUAmount{10'488'088'48170151, -8}));
1200 });
1201 testAMM([&](AMM& ammAlice, Env& env) {
1202 // Single deposit by asset
1203 ammAlice.deposit(
1205 BEAST_EXPECT(ammAlice.expectBalances(
1206 XRP(10'000), STAmount{USD, UINT64_C(10'999'99999999999), -11}, IOUAmount{10'488'088'48170151, -8}));
1207 });
1208 }
1209
1210 void
1212 {
1213 testcase("Invalid Withdraw");
1214
1215 auto const all = testable_amendments();
1216 using namespace jtx;
1217
1218 testAMM(
1219 [&](AMM& ammAlice, Env& env) {
1220 WithdrawArg args{
1221 .asset1Out = XRP(100),
1222 .err = ter(tecAMM_BALANCE),
1223 };
1224 ammAlice.withdraw(args);
1225 },
1226 {{XRP(99), USD(99)}});
1227
1228 testAMM(
1229 [&](AMM& ammAlice, Env& env) {
1230 WithdrawArg args{
1231 .asset1Out = USD(100),
1232 .err = ter(tecAMM_BALANCE),
1233 };
1234 ammAlice.withdraw(args);
1235 },
1236 {{XRP(99), USD(99)}});
1237
1238 {
1239 Env env{*this};
1240 env.fund(XRP(30'000), gw, alice, bob);
1241 env.close();
1242 env(fset(gw, asfRequireAuth));
1243 env.close();
1244 env(trust(alice, gw["USD"](30'000), 0));
1245 env(trust(gw, alice["USD"](0), tfSetfAuth));
1246 // Bob trusts Gateway to owe him USD...
1247 env(trust(bob, gw["USD"](30'000), 0));
1248 // ...but Gateway does not authorize Bob to hold its USD.
1249 env.close();
1250 env(pay(gw, alice, USD(10'000)));
1251 env.close();
1252 AMM ammAlice(env, alice, XRP(10'000), USD(10'000));
1253 WithdrawArg args{
1254 .account = bob,
1255 .asset1Out = USD(100),
1256 .err = ter(tecNO_AUTH),
1257 };
1258 ammAlice.withdraw(args);
1259 }
1260
1261 testAMM([&](AMM& ammAlice, Env& env) {
1262 // Invalid flags
1263 ammAlice.withdraw(
1264 alice,
1265 1'000'000,
1269 tfBurnable,
1273 ammAlice.withdraw(
1274 alice,
1275 1'000'000,
1283
1284 // Invalid options
1291 NotTEC>>
1292 invalidOptions = {
1293 // tokens, asset1Out, asset2Out, EPrice, flags, ter
1299 {std::nullopt,
1304 temMALFORMED},
1311 {std::nullopt, XRP(100), USD(100), IOUAmount{250, 0}, std::nullopt, temMALFORMED},
1312 {1'000, XRP(100), USD(100), std::nullopt, std::nullopt, temMALFORMED},
1314 for (auto const& it : invalidOptions)
1315 {
1316 ammAlice.withdraw(
1317 alice,
1318 std::get<0>(it),
1319 std::get<1>(it),
1320 std::get<2>(it),
1321 std::get<3>(it),
1322 std::get<4>(it),
1325 ter(std::get<5>(it)));
1326 }
1327
1328 // Invalid tokens
1331
1332 // Mismatched token, invalid Asset1Out issue
1334
1335 // Mismatched token, invalid Asset2Out issue
1336 ammAlice.withdraw(alice, USD(100), GBP(100), std::nullopt, ter(temBAD_AMM_TOKENS));
1337
1338 // Mismatched token, Asset1Out.issue == Asset2Out.issue
1339 ammAlice.withdraw(alice, USD(100), USD(100), std::nullopt, ter(temBAD_AMM_TOKENS));
1340
1341 // Invalid amount value
1344 ammAlice.withdraw(alice, USD(10), std::nullopt, IOUAmount{-1}, ter(temBAD_AMOUNT));
1345
1346 // Invalid amount/token value, withdraw all tokens from one side
1347 // of the pool.
1350 ammAlice.withdraw(
1351 alice,
1353 USD(0),
1360
1361 // Bad currency
1363
1364 // Invalid Account
1365 Account bad("bad");
1366 env.memoize(bad);
1367 ammAlice.withdraw(
1368 bad,
1369 1'000'000,
1375 seq(1),
1377
1378 // Invalid AMM
1379 ammAlice.withdraw(
1380 alice,
1381 1'000,
1386 {{USD, GBP}},
1388 ter(terNO_AMM));
1389
1390 // Carol is not a Liquidity Provider
1392
1393 // Withdrawing from one side.
1394 // XRP by tokens
1395 ammAlice.withdraw(alice, IOUAmount(9'999'999'9999, -4), XRP(0), std::nullopt, ter(tecAMM_BALANCE));
1396 // USD by tokens
1397 ammAlice.withdraw(alice, IOUAmount(9'999'999'9, -1), USD(0), std::nullopt, ter(tecAMM_BALANCE));
1398 // XRP
1400 // USD
1401 ammAlice.withdraw(
1402 alice,
1403 STAmount{USD, UINT64_C(9'999'9999999999999), -13},
1407 });
1408
1409 testAMM(
1410 [&](AMM& ammAlice, Env& env) {
1411 // Withdraw entire one side of the pool.
1412 // Pre-amendment:
1413 // Equal withdraw but due to XRP rounding
1414 // this results in full withdraw of XRP pool only,
1415 // while leaving a tiny amount in USD pool.
1416 // Post-amendment:
1417 // Most of the pool is withdrawn with remaining tiny amounts
1418 auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) : ter(tecAMM_BALANCE);
1419 ammAlice.withdraw(alice, IOUAmount{9'999'999'9999, -4}, std::nullopt, std::nullopt, err);
1420 if (env.enabled(fixAMMv1_3))
1421 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4}));
1422 },
1424 0,
1426 {all, all - fixAMMv1_3});
1427
1428 testAMM(
1429 [&](AMM& ammAlice, Env& env) {
1430 // Similar to above with even smaller remaining amount
1431 // is it ok that the pool is unbalanced?
1432 // Withdraw entire one side of the pool.
1433 // Equal withdraw but due to XRP precision limit,
1434 // this results in full withdraw of XRP pool only,
1435 // while leaving a tiny amount in USD pool.
1436 auto err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) : ter(tecAMM_BALANCE);
1437 ammAlice.withdraw(alice, IOUAmount{9'999'999'999999999, -9}, std::nullopt, std::nullopt, err);
1438 if (env.enabled(fixAMMv1_3))
1439 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8}));
1440 },
1442 0,
1444 {all, all - fixAMMv1_3});
1445
1446 // Invalid AMM
1447 testAMM([&](AMM& ammAlice, Env& env) {
1448 ammAlice.withdrawAll(alice);
1449 ammAlice.withdraw(alice, 10'000, std::nullopt, std::nullopt, ter(terNO_AMM));
1450 });
1451
1452 // Globally frozen asset
1453 testAMM([&](AMM& ammAlice, Env& env) {
1454 env(fset(gw, asfGlobalFreeze));
1455 env.close();
1456 // Can withdraw non-frozen token
1457 ammAlice.withdraw(alice, XRP(100));
1459 ammAlice.withdraw(alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
1460 });
1461
1462 // Individually frozen (AMM) account
1463 testAMM([&](AMM& ammAlice, Env& env) {
1464 env(trust(gw, alice["USD"](0), tfSetFreeze));
1465 env.close();
1466 // Can withdraw non-frozen token
1467 ammAlice.withdraw(alice, XRP(100));
1468 ammAlice.withdraw(alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
1470 env(trust(gw, alice["USD"](0), tfClearFreeze));
1471 // Individually frozen AMM
1472 env(trust(gw, STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
1473 // Can withdraw non-frozen token
1474 ammAlice.withdraw(alice, XRP(100));
1475 ammAlice.withdraw(alice, 1'000, std::nullopt, std::nullopt, ter(tecFROZEN));
1477 });
1478
1479 // Carol withdraws more than she owns
1480 testAMM([&](AMM& ammAlice, Env&) {
1481 // Single deposit of 100000 worth of tokens,
1482 // which is 10% of the pool. Carol is LP now.
1483 ammAlice.deposit(carol, 1'000'000);
1484 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1485
1487 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1488 });
1489
1490 // Withdraw with EPrice limit. Fails to withdraw, calculated tokens
1491 // to withdraw are 0.
1492 testAMM(
1493 [&](AMM& ammAlice, Env& env) {
1494 ammAlice.deposit(carol, 1'000'000);
1495 auto const err = env.enabled(fixAMMv1_3) ? ter(tecAMM_INVALID_TOKENS) : ter(tecAMM_FAILED);
1496 ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{500, 0}, err);
1497 },
1499 0,
1501 {all, all - fixAMMv1_3});
1502
1503 // Withdraw with EPrice limit. Fails to withdraw, calculated tokens
1504 // to withdraw are greater than the LP shares.
1505 testAMM([&](AMM& ammAlice, Env&) {
1506 ammAlice.deposit(carol, 1'000'000);
1507 ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{600, 0}, ter(tecAMM_INVALID_TOKENS));
1508 });
1509
1510 // Withdraw with EPrice limit. Fails to withdraw, amount1
1511 // to withdraw is less than 1700USD.
1512 testAMM([&](AMM& ammAlice, Env&) {
1513 ammAlice.deposit(carol, 1'000'000);
1514 ammAlice.withdraw(carol, USD(1'700), std::nullopt, IOUAmount{520, 0}, ter(tecAMM_FAILED));
1515 });
1516
1517 // Deposit/Withdraw the same amount with the trading fee
1518 testAMM(
1519 [&](AMM& ammAlice, Env&) {
1520 ammAlice.deposit(carol, USD(1'000));
1522 },
1524 1'000);
1525 testAMM(
1526 [&](AMM& ammAlice, Env&) {
1527 ammAlice.deposit(carol, XRP(1'000));
1529 },
1531 1'000);
1532
1533 // Deposit/Withdraw the same amount fails due to the tokens adjustment
1534 testAMM([&](AMM& ammAlice, Env&) {
1535 ammAlice.deposit(carol, STAmount{USD, 1, -6});
1537 });
1538
1539 // Withdraw close to one side of the pool. Account's LP tokens
1540 // are rounded to all LP tokens.
1541 testAMM(
1542 [&](AMM& ammAlice, Env& env) {
1543 auto const err = env.enabled(fixAMMv1_3) ? ter(tecINVARIANT_FAILED) : ter(tecAMM_BALANCE);
1544 ammAlice.withdraw(
1545 alice, STAmount{USD, UINT64_C(9'999'999999999999), -12}, std::nullopt, std::nullopt, err);
1546 },
1547 {.features = {all, all - fixAMMv1_3}, .noLog = true});
1548
1549 // Tiny withdraw
1550 testAMM([&](AMM& ammAlice, Env&) {
1551 // XRP amount to withdraw is 0
1553 // Calculated tokens to withdraw are 0
1555 ammAlice.deposit(carol, STAmount{USD, 1, -10});
1558 ammAlice.withdraw(WithdrawArg{.tokens = IOUAmount{1, -10}, .err = ter(tecAMM_INVALID_TOKENS)});
1559 ammAlice.withdraw(WithdrawArg{
1560 .asset1Out = STAmount{USD, 1, -15}, .asset2Out = XRPAmount{1}, .err = ter(tecAMM_INVALID_TOKENS)});
1561 ammAlice.withdraw(WithdrawArg{
1562 .tokens = IOUAmount{1, -10}, .asset1Out = STAmount{USD, 1, -15}, .err = ter(tecAMM_INVALID_TOKENS)});
1563 });
1564 }
1565
1566 void
1568 {
1569 testcase("Withdraw");
1570
1571 auto const all = testable_amendments();
1572 using namespace jtx;
1573
1574 // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current
1575 // pool
1576 testAMM([&](AMM& ammAlice, Env& env) {
1577 auto const baseFee = env.current()->fees().base.drops();
1578 // Single deposit of 100000 worth of tokens,
1579 // which is 10% of the pool. Carol is LP now.
1580 ammAlice.deposit(carol, 1'000'000);
1581 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{11'000'000, 0}));
1582 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
1583 // 30,000 less deposited 1,000
1584 BEAST_EXPECT(expectHolding(env, carol, USD(29'000)));
1585 // 30,000 less deposited 1,000 and 10 drops tx fee
1586 BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRPAmount{29'000'000'000 - baseFee}));
1587
1588 // Carol withdraws all tokens
1589 ammAlice.withdraw(carol, 1'000'000);
1590 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero())));
1591 BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
1592 BEAST_EXPECT(expectLedgerEntryRoot(env, carol, XRPAmount{30'000'000'000 - 2 * baseFee}));
1593 });
1594
1595 // Equal withdrawal by tokens 1000000, 10%
1596 // of the current pool
1597 testAMM([&](AMM& ammAlice, Env&) {
1598 ammAlice.withdraw(alice, 1'000'000);
1599 BEAST_EXPECT(ammAlice.expectBalances(XRP(9'000), USD(9'000), IOUAmount{9'000'000, 0}));
1600 });
1601
1602 // Equal withdrawal with a limit. Withdraw XRP200.
1603 // If proportional withdraw of USD is less than 100
1604 // then withdraw that amount, otherwise withdraw USD100
1605 // and proportionally withdraw XRP. It's the latter
1606 // in this case - XRP100/USD100.
1607 testAMM([&](AMM& ammAlice, Env&) {
1608 ammAlice.withdraw(alice, XRP(200), USD(100));
1609 BEAST_EXPECT(ammAlice.expectBalances(XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0}));
1610 });
1611
1612 // Equal withdrawal with a limit. XRP100/USD100.
1613 testAMM([&](AMM& ammAlice, Env&) {
1614 ammAlice.withdraw(alice, XRP(100), USD(200));
1615 BEAST_EXPECT(ammAlice.expectBalances(XRP(9'900), USD(9'900), IOUAmount{9'900'000, 0}));
1616 });
1617
1618 // Single withdrawal by amount XRP1000
1619 testAMM(
1620 [&](AMM& ammAlice, Env& env) {
1621 ammAlice.withdraw(alice, XRP(1'000));
1622 if (!env.enabled(fixAMMv1_3))
1623 BEAST_EXPECT(ammAlice.expectBalances(XRP(9'000), USD(10'000), IOUAmount{9'486'832'98050514, -8}));
1624 else
1625 BEAST_EXPECT(ammAlice.expectBalances(
1626 XRPAmount{9'000'000'001}, USD(10'000), IOUAmount{9'486'832'98050514, -8}));
1627 },
1629 0,
1631 {all, all - fixAMMv1_3});
1632
1633 // Single withdrawal by tokens 10000.
1634 testAMM([&](AMM& ammAlice, Env&) {
1635 ammAlice.withdraw(alice, 10'000, USD(0));
1636 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(9980.01), IOUAmount{9'990'000, 0}));
1637 });
1638
1639 // Withdraw all tokens.
1640 testAMM([&](AMM& ammAlice, Env& env) {
1641 env(trust(carol, STAmount{ammAlice.lptIssue(), 10'000}));
1642 // Can SetTrust only for AMM LP tokens
1643 env(trust(carol, STAmount{Issue{EUR.currency, ammAlice.ammAccount()}, 10'000}), ter(tecNO_PERMISSION));
1644 env.close();
1645 ammAlice.withdrawAll(alice);
1646 BEAST_EXPECT(!ammAlice.ammExists());
1647
1648 BEAST_EXPECT(!env.le(keylet::ownerDir(ammAlice.ammAccount())));
1649
1650 // Can create AMM for the XRP/USD pair
1651 AMM ammCarol(env, carol, XRP(10'000), USD(10'000));
1652 BEAST_EXPECT(ammCarol.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
1653 });
1654
1655 // Single deposit 1000USD, withdraw all tokens in USD
1656 testAMM([&](AMM& ammAlice, Env& env) {
1657 ammAlice.deposit(carol, USD(1'000));
1658 ammAlice.withdrawAll(carol, USD(0));
1659 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
1660 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount(beast::Zero())));
1661 });
1662
1663 // Single deposit 1000USD, withdraw all tokens in XRP
1664 testAMM([&](AMM& ammAlice, Env&) {
1665 ammAlice.deposit(carol, USD(1'000));
1666 ammAlice.withdrawAll(carol, XRP(0));
1667 BEAST_EXPECT(ammAlice.expectBalances(
1668 XRPAmount(9'090'909'091), STAmount{USD, UINT64_C(10'999'99999999999), -11}, IOUAmount{10'000'000, 0}));
1669 });
1670
1671 // Single deposit/withdraw by the same account
1672 testAMM(
1673 [&](AMM& ammAlice, Env& env) {
1674 // Since a smaller amount might be deposited due to
1675 // the lp tokens adjustment, withdrawing by tokens
1676 // is generally preferred to withdrawing by amount.
1677 auto lpTokens = ammAlice.deposit(carol, USD(1'000));
1678 ammAlice.withdraw(carol, lpTokens, USD(0));
1679 lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6));
1680 ammAlice.withdraw(carol, lpTokens, USD(0));
1681 lpTokens = ammAlice.deposit(carol, XRPAmount(1));
1682 ammAlice.withdraw(carol, lpTokens, XRPAmount(0));
1683 if (!env.enabled(fixAMMv1_3))
1684 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
1685 else
1686 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount(10'000'000'001), USD(10'000), ammAlice.tokens()));
1687 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
1688 },
1690 0,
1692 {all, all - fixAMMv1_3});
1693
1694 // Single deposit by different accounts and then withdraw
1695 // in reverse.
1696 testAMM([&](AMM& ammAlice, Env&) {
1697 auto const carolTokens = ammAlice.deposit(carol, USD(1'000));
1698 auto const aliceTokens = ammAlice.deposit(alice, USD(1'000));
1699 ammAlice.withdraw(alice, aliceTokens, USD(0));
1700 ammAlice.withdraw(carol, carolTokens, USD(0));
1701 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
1702 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
1703 BEAST_EXPECT(ammAlice.expectLPTokens(alice, ammAlice.tokens()));
1704 });
1705
1706 // Equal deposit 10%, withdraw all tokens
1707 testAMM([&](AMM& ammAlice, Env&) {
1708 ammAlice.deposit(carol, 1'000'000);
1709 ammAlice.withdrawAll(carol);
1710 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000, 0}));
1711 });
1712
1713 // Equal deposit 10%, withdraw all tokens in USD
1714 testAMM([&](AMM& ammAlice, Env&) {
1715 ammAlice.deposit(carol, 1'000'000);
1716 ammAlice.withdrawAll(carol, USD(0));
1717 BEAST_EXPECT(ammAlice.expectBalances(
1718 XRP(11'000), STAmount{USD, UINT64_C(9'090'909090909092), -12}, IOUAmount{10'000'000, 0}));
1719 });
1720
1721 // Equal deposit 10%, withdraw all tokens in XRP
1722 testAMM([&](AMM& ammAlice, Env&) {
1723 ammAlice.deposit(carol, 1'000'000);
1724 ammAlice.withdrawAll(carol, XRP(0));
1725 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount(9'090'909'091), USD(11'000), IOUAmount{10'000'000, 0}));
1726 });
1727
1728 // Withdraw with EPrice limit.
1729 testAMM(
1730 [&](AMM& ammAlice, Env& env) {
1731 ammAlice.deposit(carol, 1'000'000);
1732 ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{520, 0});
1733 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{153'846'15384616, -8}));
1734 if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
1735 BEAST_EXPECT(ammAlice.expectBalances(
1736 XRPAmount(11'000'000'000),
1737 STAmount{USD, UINT64_C(9'372'781065088757), -12},
1738 IOUAmount{10'153'846'15384616, -8}));
1739 else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
1740 BEAST_EXPECT(ammAlice.expectBalances(
1741 XRPAmount(11'000'000'000),
1742 STAmount{USD, UINT64_C(9'372'781065088769), -12},
1743 IOUAmount{10'153'846'15384616, -8}));
1744 else if (env.enabled(fixAMMv1_3))
1745 BEAST_EXPECT(ammAlice.expectBalances(
1746 XRPAmount(11'000'000'000),
1747 STAmount{USD, UINT64_C(9'372'78106508877), -11},
1748 IOUAmount{10'153'846'15384616, -8}));
1749 ammAlice.withdrawAll(carol);
1750 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
1751 },
1752 {.features = {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3}, .noLog = true});
1753
1754 // Withdraw with EPrice limit. AssetOut is 0.
1755 testAMM(
1756 [&](AMM& ammAlice, Env& env) {
1757 ammAlice.deposit(carol, 1'000'000);
1758 ammAlice.withdraw(carol, USD(0), std::nullopt, IOUAmount{520, 0});
1759 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{153'846'15384616, -8}));
1760 if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
1761 BEAST_EXPECT(ammAlice.expectBalances(
1762 XRP(11'000),
1763 STAmount{USD, UINT64_C(9'372'781065088757), -12},
1764 IOUAmount{10'153'846'15384616, -8}));
1765 else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
1766 BEAST_EXPECT(ammAlice.expectBalances(
1767 XRP(11'000),
1768 STAmount{USD, UINT64_C(9'372'781065088769), -12},
1769 IOUAmount{10'153'846'15384616, -8}));
1770 else if (env.enabled(fixAMMv1_3))
1771 BEAST_EXPECT(ammAlice.expectBalances(
1772 XRP(11'000),
1773 STAmount{USD, UINT64_C(9'372'78106508877), -11},
1774 IOUAmount{10'153'846'15384616, -8}));
1775 },
1777 0,
1779 {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3});
1780
1781 // IOU to IOU + transfer fee
1782 {
1783 Env env{*this};
1784 fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
1785 env(rate(gw, 1.25));
1786 env.close();
1787 // no transfer fee on create
1788 AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
1789 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
1790 BEAST_EXPECT(expectHolding(env, alice, USD(0)));
1791 BEAST_EXPECT(expectHolding(env, alice, BTC(0)));
1792 fund(env, gw, {carol}, {USD(2'000), BTC(0.05)}, Fund::Acct);
1793 // no transfer fee on deposit
1794 ammAlice.deposit(carol, 10);
1795 BEAST_EXPECT(ammAlice.expectBalances(USD(22'000), BTC(0.55), IOUAmount{110, 0}));
1796 BEAST_EXPECT(expectHolding(env, carol, USD(0)));
1797 BEAST_EXPECT(expectHolding(env, carol, BTC(0)));
1798 // no transfer fee on withdraw
1799 ammAlice.withdraw(carol, 10);
1800 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));
1801 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0, 0}));
1802 BEAST_EXPECT(expectHolding(env, carol, USD(2'000)));
1803 BEAST_EXPECT(expectHolding(env, carol, BTC(0.05)));
1804 }
1805
1806 // Tiny withdraw
1807 testAMM([&](AMM& ammAlice, Env&) {
1808 // By tokens
1809 ammAlice.withdraw(alice, IOUAmount{1, -3});
1810 BEAST_EXPECT(ammAlice.expectBalances(
1811 XRPAmount{9'999'999'999}, STAmount{USD, UINT64_C(9'999'999999), -6}, IOUAmount{9'999'999'999, -3}));
1812 });
1813 testAMM(
1814 [&](AMM& ammAlice, Env& env) {
1815 // Single XRP pool
1816 ammAlice.withdraw(alice, std::nullopt, XRPAmount{1});
1817 if (!env.enabled(fixAMMv1_3))
1818 BEAST_EXPECT(
1819 ammAlice.expectBalances(XRPAmount{9'999'999'999}, USD(10'000), IOUAmount{9'999'999'9995, -4}));
1820 else
1821 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{9'999'999'9995, -4}));
1822 },
1824 0,
1826 {all, all - fixAMMv1_3});
1827 testAMM([&](AMM& ammAlice, Env&) {
1828 // Single USD pool
1829 ammAlice.withdraw(alice, std::nullopt, STAmount{USD, 1, -10});
1830 BEAST_EXPECT(ammAlice.expectBalances(
1831 XRP(10'000), STAmount{USD, UINT64_C(9'999'9999999999), -10}, IOUAmount{9'999'999'99999995, -8}));
1832 });
1833
1834 // Withdraw close to entire pool
1835 // Equal by tokens
1836 testAMM([&](AMM& ammAlice, Env&) {
1837 ammAlice.withdraw(alice, IOUAmount{9'999'999'999, -3});
1838 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, STAmount{USD, 1, -6}, IOUAmount{1, -3}));
1839 });
1840 // USD by tokens
1841 testAMM([&](AMM& ammAlice, Env&) {
1842 ammAlice.withdraw(alice, IOUAmount{9'999'999}, USD(0));
1843 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), STAmount{USD, 1, -10}, IOUAmount{1}));
1844 });
1845 // XRP by tokens
1846 testAMM([&](AMM& ammAlice, Env&) {
1847 ammAlice.withdraw(alice, IOUAmount{9'999'900}, XRP(0));
1848 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, USD(10'000), IOUAmount{100}));
1849 });
1850 // USD
1851 testAMM([&](AMM& ammAlice, Env&) {
1852 ammAlice.withdraw(alice, STAmount{USD, UINT64_C(9'999'99999999999), -11});
1853 BEAST_EXPECT(ammAlice.expectBalances(XRP(10000), STAmount{USD, 1, -11}, IOUAmount{316227765, -9}));
1854 });
1855 // XRP
1856 testAMM([&](AMM& ammAlice, Env&) {
1857 ammAlice.withdraw(alice, XRPAmount{9'999'999'999});
1858 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{1}, USD(10'000), IOUAmount{100}));
1859 });
1860 }
1861
1862 void
1864 {
1865 testcase("Invalid Fee Vote");
1866 using namespace jtx;
1867
1868 testAMM([&](AMM& ammAlice, Env& env) {
1869 // Invalid flags
1871
1872 // Invalid fee.
1874 BEAST_EXPECT(ammAlice.expectTradingFee(0));
1875
1876 // Invalid Account
1877 Account bad("bad");
1878 env.memoize(bad);
1879 ammAlice.vote(bad, 1'000, std::nullopt, seq(1), std::nullopt, ter(terNO_ACCOUNT));
1880
1881 // Invalid AMM
1882 ammAlice.vote(alice, 1'000, std::nullopt, std::nullopt, {{USD, GBP}}, ter(terNO_AMM));
1883
1884 // Account is not LP
1886 });
1887
1888 // Invalid AMM
1889 testAMM([&](AMM& ammAlice, Env& env) {
1890 ammAlice.withdrawAll(alice);
1892 });
1893 }
1894
1895 void
1897 {
1898 testcase("Fee Vote");
1899 auto const all = testable_amendments();
1900 using namespace jtx;
1901
1902 // One vote sets fee to 1%.
1903 testAMM([&](AMM& ammAlice, Env& env) {
1904 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{0}));
1905 ammAlice.vote({}, 1'000);
1906 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
1907 // Discounted fee is 1/10 of trading fee.
1908 BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{0}));
1909 });
1910
1911 auto vote = [&](AMM& ammAlice,
1912 Env& env,
1913 int i,
1914 int fundUSD = 100'000,
1915 std::uint32_t tokens = 10'000'000,
1916 std::vector<Account>* accounts = nullptr) {
1917 Account a(std::to_string(i));
1918 // post-amendment the amount to deposit is slightly higher
1919 // in order to ensure AMM invariant sqrt(asset1 * asset2) >= tokens
1920 // fund just one USD higher in this case, which is enough for
1921 // deposit to succeed
1922 if (env.enabled(fixAMMv1_3))
1923 ++fundUSD;
1924 fund(env, gw, {a}, {USD(fundUSD)}, Fund::Acct);
1925 ammAlice.deposit(a, tokens);
1926 ammAlice.vote(a, 50 * (i + 1));
1927 if (accounts)
1928 accounts->push_back(std::move(a));
1929 };
1930
1931 // Eight votes fill all voting slots, set fee 0.175%.
1932 testAMM(
1933 [&](AMM& ammAlice, Env& env) {
1934 for (int i = 0; i < 7; ++i)
1935 vote(ammAlice, env, i, 10'000);
1936 BEAST_EXPECT(ammAlice.expectTradingFee(175));
1937 },
1939 0,
1941 {all});
1942
1943 // Eight votes fill all voting slots, set fee 0.175%.
1944 // New vote, same account, sets fee 0.225%
1945 testAMM([&](AMM& ammAlice, Env& env) {
1946 for (int i = 0; i < 7; ++i)
1947 vote(ammAlice, env, i);
1948 BEAST_EXPECT(ammAlice.expectTradingFee(175));
1949 Account const a("0");
1950 ammAlice.vote(a, 450);
1951 BEAST_EXPECT(ammAlice.expectTradingFee(225));
1952 });
1953
1954 // Eight votes fill all voting slots, set fee 0.175%.
1955 // New vote, new account, higher vote weight, set higher fee 0.244%
1956 testAMM([&](AMM& ammAlice, Env& env) {
1957 for (int i = 0; i < 7; ++i)
1958 vote(ammAlice, env, i);
1959 BEAST_EXPECT(ammAlice.expectTradingFee(175));
1960 vote(ammAlice, env, 7, 100'000, 20'000'000);
1961 BEAST_EXPECT(ammAlice.expectTradingFee(244));
1962 });
1963
1964 // Eight votes fill all voting slots, set fee 0.219%.
1965 // New vote, new account, higher vote weight, set smaller fee 0.206%
1966 testAMM([&](AMM& ammAlice, Env& env) {
1967 for (int i = 7; i > 0; --i)
1968 vote(ammAlice, env, i);
1969 BEAST_EXPECT(ammAlice.expectTradingFee(219));
1970 vote(ammAlice, env, 0, 100'000, 20'000'000);
1971 BEAST_EXPECT(ammAlice.expectTradingFee(206));
1972 });
1973
1974 // Eight votes fill all voting slots. The accounts then withdraw all
1975 // tokens. An account sets a new fee and the previous slots are
1976 // deleted.
1977 testAMM([&](AMM& ammAlice, Env& env) {
1978 std::vector<Account> accounts;
1979 for (int i = 0; i < 7; ++i)
1980 vote(ammAlice, env, i, 100'000, 10'000'000, &accounts);
1981 BEAST_EXPECT(ammAlice.expectTradingFee(175));
1982 for (int i = 0; i < 7; ++i)
1983 ammAlice.withdrawAll(accounts[i]);
1984 ammAlice.deposit(carol, 10'000'000);
1985 ammAlice.vote(carol, 1'000);
1986 // The initial LP set the fee to 1000. Carol gets 50% voting
1987 // power, and the new fee is 500.
1988 BEAST_EXPECT(ammAlice.expectTradingFee(500));
1989 });
1990
1991 // Eight votes fill all voting slots. The accounts then withdraw some
1992 // tokens. The new vote doesn't get the voting power but
1993 // the slots are refreshed and the fee is updated.
1994 testAMM([&](AMM& ammAlice, Env& env) {
1995 std::vector<Account> accounts;
1996 for (int i = 0; i < 7; ++i)
1997 vote(ammAlice, env, i, 100'000, 10'000'000, &accounts);
1998 BEAST_EXPECT(ammAlice.expectTradingFee(175));
1999 for (int i = 0; i < 7; ++i)
2000 ammAlice.withdraw(accounts[i], 9'000'000);
2001 ammAlice.deposit(carol, 1'000);
2002 // The vote is not added to the slots
2003 ammAlice.vote(carol, 1'000);
2004 auto const info = ammAlice.ammRpcInfo()[jss::amm][jss::vote_slots];
2005 for (std::uint16_t i = 0; i < info.size(); ++i)
2006 BEAST_EXPECT(info[i][jss::account] != carol.human());
2007 // But the slots are refreshed and the fee is changed
2008 BEAST_EXPECT(ammAlice.expectTradingFee(82));
2009 });
2010 }
2011
2012 void
2014 {
2015 testcase("Invalid Bid");
2016 using namespace jtx;
2017 using namespace std::chrono;
2018
2019 // burn all the LPTokens through a AMMBid transaction
2020 {
2021 Env env(*this);
2022 fund(env, gw, {alice}, XRP(2'000), {USD(2'000)});
2023 AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000);
2024
2025 // auction slot is owned by the creator of the AMM i.e. gw
2026 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
2027
2028 // gw attempts to burn all her LPTokens through a bid transaction
2029 // this transaction fails because AMMBid transaction can not burn
2030 // all the outstanding LPTokens
2031 env(amm.bid({
2032 .account = gw,
2033 .bidMin = 1'000'000,
2034 }),
2036 }
2037
2038 // burn all the LPTokens through a AMMBid transaction
2039 {
2040 Env env(*this);
2041 fund(env, gw, {alice}, XRP(2'000), {USD(2'000)});
2042 AMM amm(env, gw, XRP(1'000), USD(1'000), false, 1'000);
2043
2044 // auction slot is owned by the creator of the AMM i.e. gw
2045 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
2046
2047 // gw burns all but one of its LPTokens through a bid transaction
2048 // this transaction succeeds because the bid price is less than
2049 // the total outstanding LPToken balance
2050 env(amm.bid({
2051 .account = gw,
2052 .bidMin = STAmount{amm.lptIssue(), UINT64_C(999'999)},
2053 }),
2054 ter(tesSUCCESS))
2055 .close();
2056
2057 // gw must own the auction slot
2058 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{999'999}));
2059
2060 // 999'999 tokens are burned, only 1 LPToken is owned by gw
2061 BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(1'000), IOUAmount{1}));
2062
2063 // gw owns only 1 LPToken in its balance
2064 BEAST_EXPECT(Number{amm.getLPTokensBalance(gw)} == 1);
2065
2066 // gw attempts to burn the last of its LPTokens in an AMMBid
2067 // transaction. This transaction fails because it would burn all
2068 // the remaining LPTokens
2069 env(amm.bid({
2070 .account = gw,
2071 .bidMin = 1,
2072 }),
2074 }
2075
2076 testAMM([&](AMM& ammAlice, Env& env) {
2077 // Invalid flags
2078 env(ammAlice.bid({
2079 .account = carol,
2080 .bidMin = 0,
2081 .flags = tfWithdrawAll,
2082 }),
2084
2085 ammAlice.deposit(carol, 1'000'000);
2086 // Invalid Bid price <= 0
2087 for (auto bid : {0, -100})
2088 {
2089 env(ammAlice.bid({
2090 .account = carol,
2091 .bidMin = bid,
2092 }),
2093 ter(temBAD_AMOUNT));
2094 env(ammAlice.bid({
2095 .account = carol,
2096 .bidMax = bid,
2097 }),
2098 ter(temBAD_AMOUNT));
2099 }
2100
2101 // Invalid Min/Max combination
2102 env(ammAlice.bid({
2103 .account = carol,
2104 .bidMin = 200,
2105 .bidMax = 100,
2106 }),
2108
2109 // Invalid Account
2110 Account bad("bad");
2111 env.memoize(bad);
2112 env(ammAlice.bid({
2113 .account = bad,
2114 .bidMax = 100,
2115 }),
2116 seq(1),
2117 ter(terNO_ACCOUNT));
2118
2119 // Account is not LP
2120 Account const dan("dan");
2121 env.fund(XRP(1'000), dan);
2122 env(ammAlice.bid({
2123 .account = dan,
2124 .bidMin = 100,
2125 }),
2127 env(ammAlice.bid({
2128 .account = dan,
2129 }),
2131
2132 // Auth account is invalid.
2133 env(ammAlice.bid({
2134 .account = carol,
2135 .bidMin = 100,
2136 .authAccounts = {bob},
2137 }),
2138 ter(terNO_ACCOUNT));
2139
2140 // Invalid Assets
2141 env(ammAlice.bid({
2142 .account = alice,
2143 .bidMax = 100,
2144 .assets = {{USD, GBP}},
2145 }),
2146 ter(terNO_AMM));
2147
2148 // Invalid Min/Max issue
2149 env(ammAlice.bid({
2150 .account = alice,
2151 .bidMax = STAmount{USD, 100},
2152 }),
2153 ter(temBAD_AMM_TOKENS));
2154 env(ammAlice.bid({
2155 .account = alice,
2156 .bidMin = STAmount{USD, 100},
2157 }),
2158 ter(temBAD_AMM_TOKENS));
2159 });
2160
2161 // Invalid AMM
2162 testAMM([&](AMM& ammAlice, Env& env) {
2163 ammAlice.withdrawAll(alice);
2164 env(ammAlice.bid({
2165 .account = alice,
2166 .bidMax = 100,
2167 }),
2168 ter(terNO_AMM));
2169 });
2170
2171 // More than four Auth accounts.
2172 testAMM([&](AMM& ammAlice, Env& env) {
2173 Account ed("ed");
2174 Account bill("bill");
2175 Account scott("scott");
2176 Account james("james");
2177 env.fund(XRP(1'000), bob, ed, bill, scott, james);
2178 env.close();
2179 ammAlice.deposit(carol, 1'000'000);
2180 env(ammAlice.bid({
2181 .account = carol,
2182 .bidMin = 100,
2183 .authAccounts = {bob, ed, bill, scott, james},
2184 }),
2185 ter(temMALFORMED));
2186 });
2187
2188 // Bid price exceeds LP owned tokens
2189 testAMM([&](AMM& ammAlice, Env& env) {
2190 fund(env, gw, {bob}, XRP(1'000), {USD(100)}, Fund::Acct);
2191 ammAlice.deposit(carol, 1'000'000);
2192 ammAlice.deposit(bob, 10);
2193 env(ammAlice.bid({
2194 .account = carol,
2195 .bidMin = 1'000'001,
2196 }),
2197 ter(tecAMM_INVALID_TOKENS));
2198 env(ammAlice.bid({
2199 .account = carol,
2200 .bidMax = 1'000'001,
2201 }),
2202 ter(tecAMM_INVALID_TOKENS));
2203 env(ammAlice.bid({
2204 .account = carol,
2205 .bidMin = 1'000,
2206 }));
2207 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{1'000}));
2208 // Slot purchase price is more than 1000 but bob only has 10 tokens
2209 env(ammAlice.bid({
2210 .account = bob,
2211 }),
2212 ter(tecAMM_INVALID_TOKENS));
2213 });
2214
2215 // Bid all tokens, still own the slot
2216 {
2217 Env env(*this);
2218 fund(env, gw, {alice, bob}, XRP(1'000), {USD(1'000)});
2219 AMM amm(env, gw, XRP(10), USD(1'000));
2220 auto const lpIssue = amm.lptIssue();
2221 env.trust(STAmount{lpIssue, 100}, alice);
2222 env.trust(STAmount{lpIssue, 50}, bob);
2223 env(pay(gw, alice, STAmount{lpIssue, 100}));
2224 env(pay(gw, bob, STAmount{lpIssue, 50}));
2225 env(amm.bid({.account = alice, .bidMin = 100}));
2226 // Alice doesn't have any more tokens, but
2227 // she still owns the slot.
2228 env(amm.bid({
2229 .account = bob,
2230 .bidMax = 50,
2231 }),
2232 ter(tecAMM_FAILED));
2233 }
2234 }
2235
2236 void
2238 {
2239 testcase("Bid");
2240 using namespace jtx;
2241 using namespace std::chrono;
2242
2243 // For now, just disable SAV entirely, which locks in the small Number
2244 // mantissas
2245 features = features - featureSingleAssetVault - featureLendingProtocol;
2246
2247 // Auction slot initially is owned by AMM creator, who pays 0 price.
2248
2249 // Bid 110 tokens. Pay bidMin.
2250 testAMM(
2251 [&](AMM& ammAlice, Env& env) {
2252 ammAlice.deposit(carol, 1'000'000);
2253 env(ammAlice.bid({.account = carol, .bidMin = 110}));
2254 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2255 // 110 tokens are burned.
2256 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'890, 0}));
2257 },
2259 0,
2261 {features});
2262
2263 // Bid with min/max when the pay price is less than min.
2264 testAMM(
2265 [&](AMM& ammAlice, Env& env) {
2266 ammAlice.deposit(carol, 1'000'000);
2267 // Bid exactly 110. Pay 110 because the pay price is < 110.
2268 env(ammAlice.bid({.account = carol, .bidMin = 110, .bidMax = 110}));
2269 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2270 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'890}));
2271 // Bid exactly 180-200. Pay 180 because the pay price is < 180.
2272 env(ammAlice.bid({.account = alice, .bidMin = 180, .bidMax = 200}));
2273 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{180}));
2274 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), USD(11'000), IOUAmount{10'999'814'5, -1}));
2275 },
2277 0,
2279 {features});
2280
2281 // Start bid at bidMin 110.
2282 testAMM(
2283 [&](AMM& ammAlice, Env& env) {
2284 ammAlice.deposit(carol, 1'000'000);
2285 // Bid, pay bidMin.
2286 env(ammAlice.bid({.account = carol, .bidMin = 110}));
2287 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2288
2289 fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct);
2290 ammAlice.deposit(bob, 1'000'000);
2291 // Bid, pay the computed price.
2292 env(ammAlice.bid({.account = bob}));
2293 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount(1155, -1)));
2294
2295 // Bid bidMax fails because the computed price is higher.
2296 env(ammAlice.bid({
2297 .account = carol,
2298 .bidMax = 120,
2299 }),
2301 // Bid MaxSlotPrice succeeds - pay computed price
2302 env(ammAlice.bid({.account = carol, .bidMax = 600}));
2303 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{121'275, -3}));
2304
2305 // Bid Min/MaxSlotPrice fails because the computed price is not
2306 // in range
2307 env(ammAlice.bid({
2308 .account = carol,
2309 .bidMin = 10,
2310 .bidMax = 100,
2311 }),
2313 // Bid Min/MaxSlotPrice succeeds - pay computed price
2314 env(ammAlice.bid({.account = carol, .bidMin = 100, .bidMax = 600}));
2315 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{127'33875, -5}));
2316 },
2318 0,
2320 {features});
2321
2322 // Slot states.
2323 testAMM(
2324 [&](AMM& ammAlice, Env& env) {
2325 ammAlice.deposit(carol, 1'000'000);
2326
2327 fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct);
2328 ammAlice.deposit(bob, 1'000'000);
2329 if (!features[fixAMMv1_3])
2330 BEAST_EXPECT(ammAlice.expectBalances(XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0}));
2331 else
2332 BEAST_EXPECT(
2333 ammAlice.expectBalances(XRPAmount{12'000'000'001}, USD(12'000), IOUAmount{12'000'000, 0}));
2334
2335 // Initial state. Pay bidMin.
2336 env(ammAlice.bid({.account = carol, .bidMin = 110})).close();
2337 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
2338
2339 // 1st Interval after close, price for 0th interval.
2340 env(ammAlice.bid({.account = bob}));
2342 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 1, IOUAmount{1'155, -1}));
2343
2344 // 10th Interval after close, price for 1st interval.
2345 env(ammAlice.bid({.account = carol}));
2347 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 10, IOUAmount{121'275, -3}));
2348
2349 // 20th Interval (expired) after close, price for 10th interval.
2350 env(ammAlice.bid({.account = bob}));
2352 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{127'33875, -5}));
2353
2354 // 0 Interval.
2355 env(ammAlice.bid({.account = carol, .bidMin = 110})).close();
2356 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{110}));
2357 // ~321.09 tokens burnt on bidding fees.
2358 if (!features[fixAMMv1_3])
2359 BEAST_EXPECT(ammAlice.expectBalances(XRP(12'000), USD(12'000), IOUAmount{11'999'678'91, -2}));
2360 else
2361 BEAST_EXPECT(
2362 ammAlice.expectBalances(XRPAmount{12'000'000'001}, USD(12'000), IOUAmount{11'999'678'91, -2}));
2363 },
2365 0,
2367 {features});
2368
2369 // Pool's fee 1%. Bid bidMin.
2370 // Auction slot owner and auth account trade at discounted fee -
2371 // 1/10 of the trading fee.
2372 // Other accounts trade at 1% fee.
2373 testAMM(
2374 [&](AMM& ammAlice, Env& env) {
2375 Account const dan("dan");
2376 Account const ed("ed");
2377 fund(env, gw, {bob, dan, ed}, {USD(20'000)}, Fund::Acct);
2378 ammAlice.deposit(bob, 1'000'000);
2379 ammAlice.deposit(ed, 1'000'000);
2380 ammAlice.deposit(carol, 500'000);
2381 ammAlice.deposit(dan, 500'000);
2382 auto ammTokens = ammAlice.getLPTokensBalance();
2383 env(ammAlice.bid({
2384 .account = carol,
2385 .bidMin = 120,
2386 .authAccounts = {bob, ed},
2387 }));
2388 auto const slotPrice = IOUAmount{5'200};
2389 ammTokens -= slotPrice;
2390 BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice));
2391 if (!features[fixAMMv1_3])
2392 BEAST_EXPECT(ammAlice.expectBalances(XRP(13'000), USD(13'000), ammTokens));
2393 else
2394 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{13'000'000'003}, USD(13'000), ammTokens));
2395 // Discounted trade
2396 for (int i = 0; i < 10; ++i)
2397 {
2398 auto tokens = ammAlice.deposit(carol, USD(100));
2399 ammAlice.withdraw(carol, tokens, USD(0));
2400 tokens = ammAlice.deposit(bob, USD(100));
2401 ammAlice.withdraw(bob, tokens, USD(0));
2402 tokens = ammAlice.deposit(ed, USD(100));
2403 ammAlice.withdraw(ed, tokens, USD(0));
2404 }
2405 // carol, bob, and ed pay ~0.99USD in fees.
2406 if (!features[fixAMMv1_1])
2407 {
2408 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(29'499'00572620545), -11));
2409 BEAST_EXPECT(env.balance(bob, USD) == STAmount(USD, UINT64_C(18'999'00572616195), -11));
2410 BEAST_EXPECT(env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'00572611841), -11));
2411 // USD pool is slightly higher because of the fees.
2412 BEAST_EXPECT(ammAlice.expectBalances(
2413 XRP(13'000), STAmount(USD, UINT64_C(13'002'98282151419), -11), ammTokens));
2414 }
2415 else
2416 {
2417 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(29'499'00572620544), -11));
2418 BEAST_EXPECT(env.balance(bob, USD) == STAmount(USD, UINT64_C(18'999'00572616194), -11));
2419 BEAST_EXPECT(env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'0057261184), -10));
2420 // USD pool is slightly higher because of the fees.
2421 if (!features[fixAMMv1_3])
2422 BEAST_EXPECT(ammAlice.expectBalances(
2423 XRP(13'000), STAmount(USD, UINT64_C(13'002'98282151422), -11), ammTokens));
2424 else
2425 BEAST_EXPECT(ammAlice.expectBalances(
2426 XRPAmount{13'000'000'003}, STAmount(USD, UINT64_C(13'002'98282151422), -11), ammTokens));
2427 }
2428 ammTokens = ammAlice.getLPTokensBalance();
2429 // Trade with the fee
2430 for (int i = 0; i < 10; ++i)
2431 {
2432 auto const tokens = ammAlice.deposit(dan, USD(100));
2433 ammAlice.withdraw(dan, tokens, USD(0));
2434 }
2435 // dan pays ~9.94USD, which is ~10 times more in fees than
2436 // carol, bob, ed. the discounted fee is 10 times less
2437 // than the trading fee.
2438 if (!features[fixAMMv1_1])
2439 {
2440 BEAST_EXPECT(env.balance(dan, USD) == STAmount(USD, UINT64_C(19'490'056722744), -9));
2441 // USD pool gains more in dan's fees.
2442 BEAST_EXPECT(ammAlice.expectBalances(
2443 XRP(13'000), STAmount{USD, UINT64_C(13'012'92609877019), -11}, ammTokens));
2444 // Discounted fee payment
2445 ammAlice.deposit(carol, USD(100));
2446 ammTokens = ammAlice.getLPTokensBalance();
2447 BEAST_EXPECT(ammAlice.expectBalances(
2448 XRP(13'000), STAmount{USD, UINT64_C(13'112'92609877019), -11}, ammTokens));
2449 env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110)));
2450 env.close();
2451 // carol pays 100000 drops in fees
2452 // 99900668XRP swapped in for 100USD
2453 BEAST_EXPECT(ammAlice.expectBalances(
2454 XRPAmount{13'100'000'668}, STAmount{USD, UINT64_C(13'012'92609877019), -11}, ammTokens));
2455 }
2456 else
2457 {
2458 if (!features[fixAMMv1_3])
2459 BEAST_EXPECT(env.balance(dan, USD) == STAmount(USD, UINT64_C(19'490'05672274399), -11));
2460 else
2461 BEAST_EXPECT(env.balance(dan, USD) == STAmount(USD, UINT64_C(19'490'05672274398), -11));
2462 // USD pool gains more in dan's fees.
2463 if (!features[fixAMMv1_3])
2464 BEAST_EXPECT(ammAlice.expectBalances(
2465 XRP(13'000), STAmount{USD, UINT64_C(13'012'92609877023), -11}, ammTokens));
2466 else
2467 BEAST_EXPECT(ammAlice.expectBalances(
2468 XRPAmount{13'000'000'003}, STAmount{USD, UINT64_C(13'012'92609877024), -11}, ammTokens));
2469 // Discounted fee payment
2470 ammAlice.deposit(carol, USD(100));
2471 ammTokens = ammAlice.getLPTokensBalance();
2472 if (!features[fixAMMv1_3])
2473 BEAST_EXPECT(ammAlice.expectBalances(
2474 XRP(13'000), STAmount{USD, UINT64_C(13'112'92609877023), -11}, ammTokens));
2475 else
2476 BEAST_EXPECT(ammAlice.expectBalances(
2477 XRPAmount{13'000'000'003}, STAmount{USD, UINT64_C(13'112'92609877024), -11}, ammTokens));
2478 env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110)));
2479 env.close();
2480 // carol pays 100000 drops in fees
2481 // 99900668XRP swapped in for 100USD
2482 if (!features[fixAMMv1_3])
2483 BEAST_EXPECT(ammAlice.expectBalances(
2484 XRPAmount{13'100'000'668}, STAmount{USD, UINT64_C(13'012'92609877023), -11}, ammTokens));
2485 else
2486 BEAST_EXPECT(ammAlice.expectBalances(
2487 XRPAmount{13'100'000'671}, STAmount{USD, UINT64_C(13'012'92609877024), -11}, ammTokens));
2488 }
2489 // Payment with the trading fee
2490 env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(110)));
2491 env.close();
2492 // alice pays ~1.011USD in fees, which is ~10 times more
2493 // than carol's fee
2494 // 100.099431529USD swapped in for 100XRP
2495 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
2496 {
2497 BEAST_EXPECT(ammAlice.expectBalances(
2498 XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'114'03663047264), -11}, ammTokens));
2499 }
2500 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
2501 {
2502 BEAST_EXPECT(ammAlice.expectBalances(
2503 XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'114'03663047269), -11}, ammTokens));
2504 }
2505 else
2506 {
2507 BEAST_EXPECT(ammAlice.expectBalances(
2508 XRPAmount{13'000'000'671}, STAmount{USD, UINT64_C(13'114'03663044937), -11}, ammTokens));
2509 }
2510 // Auction slot expired, no discounted fee
2512 // clock is parent's based
2513 env.close();
2514 if (!features[fixAMMv1_1])
2515 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(29'399'00572620545), -11));
2516 else if (!features[fixAMMv1_3])
2517 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(29'399'00572620544), -11));
2518 ammTokens = ammAlice.getLPTokensBalance();
2519 for (int i = 0; i < 10; ++i)
2520 {
2521 auto const tokens = ammAlice.deposit(carol, USD(100));
2522 ammAlice.withdraw(carol, tokens, USD(0));
2523 }
2524 // carol pays ~9.94USD in fees, which is ~10 times more in
2525 // trading fees vs discounted fee.
2526 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
2527 {
2528 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(29'389'06197177128), -11));
2529 BEAST_EXPECT(ammAlice.expectBalances(
2530 XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'123'98038490681), -11}, ammTokens));
2531 }
2532 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
2533 {
2534 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(29'389'06197177124), -11));
2535 BEAST_EXPECT(ammAlice.expectBalances(
2536 XRPAmount{13'000'000'668}, STAmount{USD, UINT64_C(13'123'98038490689), -11}, ammTokens));
2537 }
2538 else
2539 {
2540 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(29'389'06197177129), -11));
2541 BEAST_EXPECT(ammAlice.expectBalances(
2542 XRPAmount{13'000'000'671}, STAmount{USD, UINT64_C(13'123'98038488352), -11}, ammTokens));
2543 }
2544 env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110)));
2545 env.close();
2546 // carol pays ~1.008XRP in trading fee, which is
2547 // ~10 times more than the discounted fee.
2548 // 99.815876XRP is swapped in for 100USD
2549 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
2550 {
2551 BEAST_EXPECT(ammAlice.expectBalances(
2552 XRPAmount(13'100'824'790), STAmount{USD, UINT64_C(13'023'98038490681), -11}, ammTokens));
2553 }
2554 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
2555 {
2556 BEAST_EXPECT(ammAlice.expectBalances(
2557 XRPAmount(13'100'824'790), STAmount{USD, UINT64_C(13'023'98038490689), -11}, ammTokens));
2558 }
2559 else
2560 {
2561 BEAST_EXPECT(ammAlice.expectBalances(
2562 XRPAmount(13'100'824'793), STAmount{USD, UINT64_C(13'023'98038488352), -11}, ammTokens));
2563 }
2564 },
2566 1'000,
2568 {features});
2569
2570 // Bid tiny amount
2571 testAMM(
2572 [&](AMM& ammAlice, Env& env) {
2573 // Bid a tiny amount
2574 auto const tiny = Number{STAmount::cMinValue, STAmount::cMinOffset};
2575 env(ammAlice.bid({.account = alice, .bidMin = IOUAmount{tiny}}));
2576 // Auction slot purchase price is equal to the tiny amount
2577 // since the minSlotPrice is 0 with no trading fee.
2578 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny}));
2579 // The purchase price is too small to affect the total tokens
2580 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
2581 // Bid the tiny amount
2582 env(ammAlice.bid({
2583 .account = alice,
2584 .bidMin = IOUAmount{STAmount::cMinValue, STAmount::cMinOffset},
2585 }));
2586 // Pay slightly higher price
2587 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny * Number{105, -2}}));
2588 // The purchase price is still too small to affect the total
2589 // tokens
2590 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
2591 },
2593 0,
2595 {features});
2596
2597 // Reset auth account
2598 testAMM(
2599 [&](AMM& ammAlice, Env& env) {
2600 env(ammAlice.bid({
2601 .account = alice,
2602 .bidMin = IOUAmount{100},
2603 .authAccounts = {carol},
2604 }));
2605 BEAST_EXPECT(ammAlice.expectAuctionSlot({carol}));
2606 env(ammAlice.bid({.account = alice, .bidMin = IOUAmount{100}}));
2607 BEAST_EXPECT(ammAlice.expectAuctionSlot({}));
2608 Account bob("bob");
2609 Account dan("dan");
2610 fund(env, {bob, dan}, XRP(1'000));
2611 env(ammAlice.bid({
2612 .account = alice,
2613 .bidMin = IOUAmount{100},
2614 .authAccounts = {bob, dan},
2615 }));
2616 BEAST_EXPECT(ammAlice.expectAuctionSlot({bob, dan}));
2617 },
2619 0,
2621 {features});
2622
2623 // Bid all tokens, still own the slot and trade at a discount
2624 {
2625 Env env(*this, features);
2626 fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)});
2627 AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000);
2628 auto const lpIssue = amm.lptIssue();
2629 env.trust(STAmount{lpIssue, 500}, alice);
2630 env.trust(STAmount{lpIssue, 50}, bob);
2631 env(pay(gw, alice, STAmount{lpIssue, 500}));
2632 env(pay(gw, bob, STAmount{lpIssue, 50}));
2633 // Alice doesn't have anymore lp tokens
2634 env(amm.bid({.account = alice, .bidMin = 500}));
2635 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{500}));
2636 BEAST_EXPECT(expectHolding(env, alice, STAmount{lpIssue, 0}));
2637 // But trades with the discounted fee since she still owns the slot.
2638 // Alice pays 10011 drops in fees
2639 env(pay(alice, bob, USD(10)), path(~USD), sendmax(XRP(11)));
2640 BEAST_EXPECT(amm.expectBalances(XRPAmount{1'010'010'011}, USD(1'000), IOUAmount{1'004'487'562112089, -9}));
2641 // Bob pays the full fee ~0.1USD
2642 env(pay(bob, alice, XRP(10)), path(~XRP), sendmax(USD(11)));
2643 if (!features[fixAMMv1_1])
2644 {
2645 BEAST_EXPECT(amm.expectBalances(
2646 XRPAmount{1'000'010'011},
2647 STAmount{USD, UINT64_C(1'010'10090898081), -11},
2648 IOUAmount{1'004'487'562112089, -9}));
2649 }
2650 else
2651 {
2652 BEAST_EXPECT(amm.expectBalances(
2653 XRPAmount{1'000'010'011},
2654 STAmount{USD, UINT64_C(1'010'100908980811), -12},
2655 IOUAmount{1'004'487'562112089, -9}));
2656 }
2657 }
2658
2659 // preflight tests
2660 {
2661 Env env(*this, features);
2662 auto const baseFee = env.current()->fees().base;
2663
2664 fund(env, gw, {alice, bob}, XRP(2'000), {USD(2'000)});
2665 AMM amm(env, gw, XRP(1'000), USD(1'010), false, 1'000);
2666 Json::Value tx = amm.bid({.account = alice, .bidMin = 500});
2667
2668 {
2669 auto jtx = env.jt(tx, seq(1), fee(baseFee));
2670 env.app().config().features.erase(featureAMM);
2671 PreflightContext pfCtx(env.app(), *jtx.stx, env.current()->rules(), tapNONE, env.journal);
2672 auto pf = Transactor::invokePreflight<AMMBid>(pfCtx);
2673 BEAST_EXPECT(pf == temDISABLED);
2674 env.app().config().features.insert(featureAMM);
2675 }
2676
2677 {
2678 auto jtx = env.jt(tx, seq(1), fee(baseFee));
2679 jtx.jv["TxnSignature"] = "deadbeef";
2680 jtx.stx = env.ust(jtx);
2681 PreflightContext pfCtx(env.app(), *jtx.stx, env.current()->rules(), tapNONE, env.journal);
2682 auto pf = Transactor::invokePreflight<AMMBid>(pfCtx);
2683 BEAST_EXPECT(pf != tesSUCCESS);
2684 }
2685
2686 {
2687 auto jtx = env.jt(tx, seq(1), fee(baseFee));
2688 jtx.jv["Asset2"]["currency"] = "XRP";
2689 jtx.jv["Asset2"].removeMember("issuer");
2690 jtx.stx = env.ust(jtx);
2691 PreflightContext pfCtx(env.app(), *jtx.stx, env.current()->rules(), tapNONE, env.journal);
2692 auto pf = Transactor::invokePreflight<AMMBid>(pfCtx);
2693 BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
2694 }
2695 }
2696 }
2697
2698 void
2700 {
2701 testcase("Invalid AMM Payment");
2702 using namespace jtx;
2703 using namespace std::chrono;
2704 using namespace std::literals::chrono_literals;
2705
2706 // Can't pay into AMM account.
2707 // Can't pay out since there is no keys
2708 for (auto const& acct : {gw, alice})
2709 {
2710 {
2711 Env env(*this);
2712 fund(env, gw, {alice, carol}, XRP(1'000), {USD(100)});
2713 // XRP balance is below reserve
2714 AMM ammAlice(env, acct, XRP(10), USD(10));
2715 // Pay below reserve
2716 env(pay(carol, ammAlice.ammAccount(), XRP(10)), ter(tecNO_PERMISSION));
2717 // Pay above reserve
2718 env(pay(carol, ammAlice.ammAccount(), XRP(300)), ter(tecNO_PERMISSION));
2719 // Pay IOU
2720 env(pay(carol, ammAlice.ammAccount(), USD(10)), ter(tecNO_PERMISSION));
2721 }
2722 {
2723 Env env(*this);
2724 fund(env, gw, {alice, carol}, XRP(10'000'000), {USD(10'000)});
2725 // XRP balance is above reserve
2726 AMM ammAlice(env, acct, XRP(1'000'000), USD(100));
2727 // Pay below reserve
2728 env(pay(carol, ammAlice.ammAccount(), XRP(10)), ter(tecNO_PERMISSION));
2729 // Pay above reserve
2730 env(pay(carol, ammAlice.ammAccount(), XRP(1'000'000)), ter(tecNO_PERMISSION));
2731 }
2732 }
2733
2734 // Can't pay into AMM with escrow.
2735 testAMM([&](AMM& ammAlice, Env& env) {
2736 auto const baseFee = env.current()->fees().base;
2737 env(escrow::create(carol, ammAlice.ammAccount(), XRP(1)),
2738 escrow::condition(escrow::cb1),
2739 escrow::finish_time(env.now() + 1s),
2740 escrow::cancel_time(env.now() + 2s),
2741 fee(baseFee * 150),
2743 });
2744
2745 // Can't pay into AMM with paychan.
2746 testAMM([&](AMM& ammAlice, Env& env) {
2747 auto const pk = carol.pk();
2748 auto const settleDelay = 100s;
2749 NetClock::time_point const cancelAfter = env.current()->header().parentCloseTime + 200s;
2750 env(paychan::create(carol, ammAlice.ammAccount(), XRP(1'000), settleDelay, pk, cancelAfter),
2752 });
2753
2754 // Can't pay into AMM with checks.
2755 testAMM([&](AMM& ammAlice, Env& env) {
2756 env(check::create(env.master.id(), ammAlice.ammAccount(), XRP(100)), ter(tecNO_PERMISSION));
2757 });
2758
2759 // Pay amounts close to one side of the pool
2760 testAMM(
2761 [&](AMM& ammAlice, Env& env) {
2762 // Can't consume whole pool
2763 env(pay(alice, carol, USD(100)), path(~USD), sendmax(XRP(1'000'000'000)), ter(tecPATH_PARTIAL));
2764 env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(1'000'000'000)), ter(tecPATH_PARTIAL));
2765 // Overflow
2766 env(pay(alice, carol, STAmount{USD, UINT64_C(99'999999999), -9}),
2767 path(~USD),
2768 sendmax(XRP(1'000'000'000)),
2770 env(pay(alice, carol, STAmount{USD, UINT64_C(999'99999999), -8}),
2771 path(~USD),
2772 sendmax(XRP(1'000'000'000)),
2774 env(pay(alice, carol, STAmount{xrpIssue(), 99'999'999}),
2775 path(~XRP),
2776 sendmax(USD(1'000'000'000)),
2778 // Sender doesn't have enough funds
2779 env(pay(alice, carol, USD(99.99)), path(~USD), sendmax(XRP(1'000'000'000)), ter(tecPATH_PARTIAL));
2780 env(pay(alice, carol, STAmount{xrpIssue(), 99'990'000}),
2781 path(~XRP),
2782 sendmax(USD(1'000'000'000)),
2784 },
2785 {{XRP(100), USD(100)}});
2786
2787 // Globally frozen
2788 testAMM([&](AMM& ammAlice, Env& env) {
2789 env(fset(gw, asfGlobalFreeze));
2790 env.close();
2791 env(pay(alice, carol, USD(1)),
2792 path(~USD),
2794 sendmax(XRP(10)),
2795 ter(tecPATH_DRY));
2796 env(pay(alice, carol, XRP(1)),
2797 path(~XRP),
2799 sendmax(USD(10)),
2800 ter(tecPATH_DRY));
2801 });
2802
2803 // Individually frozen AMM
2804 testAMM([&](AMM& ammAlice, Env& env) {
2805 env(trust(gw, STAmount{Issue{gw["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
2806 env.close();
2807 env(pay(alice, carol, USD(1)),
2808 path(~USD),
2810 sendmax(XRP(10)),
2811 ter(tecPATH_DRY));
2812 env(pay(alice, carol, XRP(1)),
2813 path(~XRP),
2815 sendmax(USD(10)),
2816 ter(tecPATH_DRY));
2817 });
2818
2819 // Individually frozen accounts
2820 testAMM([&](AMM& ammAlice, Env& env) {
2821 env(trust(gw, carol["USD"](0), tfSetFreeze));
2822 env(trust(gw, alice["USD"](0), tfSetFreeze));
2823 env.close();
2824 env(pay(alice, carol, XRP(1)),
2825 path(~XRP),
2826 sendmax(USD(10)),
2828 ter(tecPATH_DRY));
2829 });
2830 }
2831
2832 void
2834 {
2835 testcase("Basic Payment");
2836 using namespace jtx;
2837
2838 // For now, just disable SAV entirely, which locks in the small Number
2839 // mantissas
2840 features = features - featureSingleAssetVault - featureLendingProtocol - featureLendingProtocol;
2841
2842 // Payment 100USD for 100XRP.
2843 // Force one path with tfNoRippleDirect.
2844 testAMM(
2845 [&](AMM& ammAlice, Env& env) {
2846 env.fund(jtx::XRP(30'000), bob);
2847 env.close();
2848 env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(100)), txflags(tfNoRippleDirect));
2849 env.close();
2850 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
2851 // Initial balance 30,000 + 100
2852 BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
2853 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
2854 BEAST_EXPECT(expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
2855 },
2856 {{XRP(10'000), USD(10'100)}},
2857 0,
2859 {features});
2860
2861 // Payment 100USD for 100XRP, use default path.
2862 testAMM(
2863 [&](AMM& ammAlice, Env& env) {
2864 env.fund(jtx::XRP(30'000), bob);
2865 env.close();
2866 env(pay(bob, carol, USD(100)), sendmax(XRP(100)));
2867 env.close();
2868 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
2869 // Initial balance 30,000 + 100
2870 BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
2871 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
2872 BEAST_EXPECT(expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
2873 },
2874 {{XRP(10'000), USD(10'100)}},
2875 0,
2877 {features});
2878
2879 // This payment is identical to above. While it has
2880 // both default path and path, activeStrands has one path.
2881 testAMM(
2882 [&](AMM& ammAlice, Env& env) {
2883 env.fund(jtx::XRP(30'000), bob);
2884 env.close();
2885 env(pay(bob, carol, USD(100)), path(~USD), sendmax(XRP(100)));
2886 env.close();
2887 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
2888 // Initial balance 30,000 + 100
2889 BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
2890 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
2891 BEAST_EXPECT(expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
2892 },
2893 {{XRP(10'000), USD(10'100)}},
2894 0,
2896 {features});
2897
2898 // Payment with limitQuality set.
2899 testAMM(
2900 [&](AMM& ammAlice, Env& env) {
2901 env.fund(jtx::XRP(30'000), bob);
2902 env.close();
2903 // Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP
2904 // would have been sent has it not been for limitQuality.
2905 env(pay(bob, carol, USD(100)),
2906 path(~USD),
2907 sendmax(XRP(100)),
2909 env.close();
2910 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'010), USD(10'000), ammAlice.tokens()));
2911 // Initial balance 30,000 + 10(limited by limitQuality)
2912 BEAST_EXPECT(expectHolding(env, carol, USD(30'010)));
2913 // Initial balance 30,000 - 10(limited by limitQuality) - 10(tx
2914 // fee)
2915 BEAST_EXPECT(expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(10) - txfee(env, 1)));
2916
2917 // Fails because of limitQuality. Would have sent
2918 // ~98.91USD/110XRP has it not been for limitQuality.
2919 env(pay(bob, carol, USD(100)),
2920 path(~USD),
2921 sendmax(XRP(100)),
2923 ter(tecPATH_DRY));
2924 env.close();
2925 },
2926 {{XRP(10'000), USD(10'010)}},
2927 0,
2929 {features});
2930
2931 // Payment with limitQuality and transfer fee set.
2932 testAMM(
2933 [&](AMM& ammAlice, Env& env) {
2934 env(rate(gw, 1.1));
2935 env.close();
2936 env.fund(jtx::XRP(30'000), bob);
2937 env.close();
2938 // Pays 10USD for 10XRP. A larger payment of ~99.11USD/100XRP
2939 // would have been sent has it not been for limitQuality and
2940 // the transfer fee.
2941 env(pay(bob, carol, USD(100)),
2942 path(~USD),
2943 sendmax(XRP(110)),
2945 env.close();
2946 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'010), USD(10'000), ammAlice.tokens()));
2947 // 10USD - 10% transfer fee
2948 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, UINT64_C(30'009'09090909091), -11}));
2949 BEAST_EXPECT(expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(10) - txfee(env, 1)));
2950 },
2951 {{XRP(10'000), USD(10'010)}},
2952 0,
2954 {features});
2955
2956 // Fail when partial payment is not set.
2957 testAMM(
2958 [&](AMM& ammAlice, Env& env) {
2959 env.fund(jtx::XRP(30'000), bob);
2960 env.close();
2961 env(pay(bob, carol, USD(100)),
2962 path(~USD),
2963 sendmax(XRP(100)),
2966 },
2967 {{XRP(10'000), USD(10'000)}},
2968 0,
2970 {features});
2971
2972 // Non-default path (with AMM) has a better quality than default path.
2973 // The max possible liquidity is taken out of non-default
2974 // path ~29.9XRP/29.9EUR, ~29.9EUR/~29.99USD. The rest
2975 // is taken from the offer.
2976 {
2977 Env env(*this, features);
2978 fund(env, gw, {alice, carol}, {USD(30'000), EUR(30'000)}, Fund::All);
2979 env.close();
2980 env.fund(XRP(1'000), bob);
2981 env.close();
2982 auto ammEUR_XRP = AMM(env, alice, XRP(10'000), EUR(10'000));
2983 auto ammUSD_EUR = AMM(env, alice, EUR(10'000), USD(10'000));
2984 env(offer(alice, XRP(101), USD(100)), txflags(tfPassive));
2985 env.close();
2986 env(pay(bob, carol, USD(100)), path(~EUR, ~USD), sendmax(XRP(102)), txflags(tfPartialPayment));
2987 env.close();
2988 BEAST_EXPECT(ammEUR_XRP.expectBalances(
2989 XRPAmount(10'030'082'730), STAmount(EUR, UINT64_C(9'970'007498125468), -12), ammEUR_XRP.tokens()));
2990 if (!features[fixAMMv1_1])
2991 {
2992 BEAST_EXPECT(ammUSD_EUR.expectBalances(
2993 STAmount(USD, UINT64_C(9'970'097277662122), -12),
2994 STAmount(EUR, UINT64_C(10'029'99250187452), -11),
2995 ammUSD_EUR.tokens()));
2996
2997 // fixReducedOffersV2 changes the expected results slightly.
2998 Amounts const expectedAmounts = env.closed()->rules().enabled(fixReducedOffersV2)
2999 ? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233787816), -14)}
3000 : Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233787818), -14)};
3001
3002 BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}}));
3003 }
3004 else
3005 {
3006 BEAST_EXPECT(ammUSD_EUR.expectBalances(
3007 STAmount(USD, UINT64_C(9'970'097277662172), -12),
3008 STAmount(EUR, UINT64_C(10'029'99250187452), -11),
3009 ammUSD_EUR.tokens()));
3010
3011 // fixReducedOffersV2 changes the expected results slightly.
3012 Amounts const expectedAmounts = env.closed()->rules().enabled(fixReducedOffersV2)
3013 ? Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233782839), -14)}
3014 : Amounts{XRPAmount(30'201'749), STAmount(USD, UINT64_C(29'90272233782840), -14)};
3015
3016 BEAST_EXPECT(expectOffers(env, alice, 1, {{expectedAmounts}}));
3017 }
3018 // Initial 30,000 + 100
3019 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, 30'100}));
3020 // Initial 1,000 - 30082730(AMM pool) - 70798251(offer) - 10(tx fee)
3021 BEAST_EXPECT(expectLedgerEntryRoot(
3022 env, bob, XRP(1'000) - XRPAmount{30'082'730} - XRPAmount{70'798'251} - txfee(env, 1)));
3023 }
3024
3025 // Default path (with AMM) has a better quality than a non-default path.
3026 // The max possible liquidity is taken out of default
3027 // path ~49XRP/49USD. The rest is taken from the offer.
3028 testAMM(
3029 [&](AMM& ammAlice, Env& env) {
3030 env.fund(XRP(1'000), bob);
3031 env.close();
3032 env.trust(EUR(2'000), alice);
3033 env.close();
3034 env(pay(gw, alice, EUR(1'000)));
3035 env(offer(alice, XRP(101), EUR(100)), txflags(tfPassive));
3036 env.close();
3037 env(offer(alice, EUR(100), USD(100)), txflags(tfPassive));
3038 env.close();
3039 env(pay(bob, carol, USD(100)), path(~EUR, ~USD), sendmax(XRP(102)), txflags(tfPartialPayment));
3040 env.close();
3041 BEAST_EXPECT(ammAlice.expectBalances(
3042 XRPAmount(10'050'238'637), STAmount(USD, UINT64_C(9'950'01249687578), -11), ammAlice.tokens()));
3043 BEAST_EXPECT(expectOffers(
3044 env,
3045 alice,
3046 2,
3047 {{Amounts{XRPAmount(50'487'378), STAmount(EUR, UINT64_C(49'98750312422), -11)},
3048 Amounts{
3049 STAmount(EUR, UINT64_C(49'98750312422), -11),
3050 STAmount(USD, UINT64_C(49'98750312422), -11)}}}));
3051 // Initial 30,000 + 99.99999999999
3052 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, UINT64_C(30'099'99999999999), -11}));
3053 // Initial 1,000 - 50238637(AMM pool) - 50512622(offer) - 10(tx
3054 // fee)
3055 BEAST_EXPECT(expectLedgerEntryRoot(
3056 env, bob, XRP(1'000) - XRPAmount{50'238'637} - XRPAmount{50'512'622} - txfee(env, 1)));
3057 },
3059 0,
3061 {features});
3062
3063 // Default path with AMM and Order Book offer. AMM is consumed first,
3064 // remaining amount is consumed by the offer.
3065 testAMM(
3066 [&](AMM& ammAlice, Env& env) {
3067 fund(env, gw, {bob}, {USD(100)}, Fund::Acct);
3068 env.close();
3069 env(offer(bob, XRP(100), USD(100)), txflags(tfPassive));
3070 env.close();
3071 env(pay(alice, carol, USD(200)), sendmax(XRP(200)), txflags(tfPartialPayment));
3072 env.close();
3073 if (!features[fixAMMv1_1])
3074 {
3075 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
3076 // Initial 30,000 + 200
3077 BEAST_EXPECT(expectHolding(env, carol, USD(30'200)));
3078 }
3079 else
3080 {
3081 BEAST_EXPECT(ammAlice.expectBalances(
3082 XRP(10'100), STAmount(USD, UINT64_C(10'000'00000000001), -11), ammAlice.tokens()));
3083 BEAST_EXPECT(expectHolding(env, carol, STAmount(USD, UINT64_C(30'199'99999999999), -11)));
3084 }
3085 // Initial 30,000 - 10000(AMM pool LP) - 100(AMM offer) -
3086 // - 100(offer) - 10(tx fee) - one reserve
3087 BEAST_EXPECT(expectLedgerEntryRoot(
3088 env, alice, XRP(30'000) - XRP(10'000) - XRP(100) - XRP(100) - ammCrtFee(env) - txfee(env, 1)));
3089 BEAST_EXPECT(expectOffers(env, bob, 0));
3090 },
3091 {{XRP(10'000), USD(10'100)}},
3092 0,
3094 {features});
3095
3096 // Default path with AMM and Order Book offer.
3097 // Order Book offer is consumed first.
3098 // Remaining amount is consumed by AMM.
3099 {
3100 Env env(*this, features);
3101 fund(env, gw, {alice, bob, carol}, XRP(20'000), {USD(2'000)});
3102 env.close();
3103 env(offer(bob, XRP(50), USD(150)), txflags(tfPassive));
3104 env.close();
3105 AMM ammAlice(env, alice, XRP(1'000), USD(1'050));
3106 env(pay(alice, carol, USD(200)), sendmax(XRP(200)), txflags(tfPartialPayment));
3107 env.close();
3108 BEAST_EXPECT(ammAlice.expectBalances(XRP(1'050), USD(1'000), ammAlice.tokens()));
3109 BEAST_EXPECT(expectHolding(env, carol, USD(2'200)));
3110 BEAST_EXPECT(expectOffers(env, bob, 0));
3111 }
3112
3113 // Offer crossing XRP/IOU
3114 testAMM(
3115 [&](AMM& ammAlice, Env& env) {
3116 fund(env, gw, {bob}, {USD(1'000)}, Fund::Acct);
3117 env.close();
3118 env(offer(bob, USD(100), XRP(100)));
3119 env.close();
3120 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), USD(10'000), ammAlice.tokens()));
3121 // Initial 1,000 + 100
3122 BEAST_EXPECT(expectHolding(env, bob, USD(1'100)));
3123 // Initial 30,000 - 100(offer) - 10(tx fee)
3124 BEAST_EXPECT(expectLedgerEntryRoot(env, bob, XRP(30'000) - XRP(100) - txfee(env, 1)));
3125 BEAST_EXPECT(expectOffers(env, bob, 0));
3126 },
3127 {{XRP(10'000), USD(10'100)}},
3128 0,
3130 {features});
3131
3132 // Offer crossing IOU/IOU and transfer rate
3133 // Single path AMM offer
3134 testAMM(
3135 [&](AMM& ammAlice, Env& env) {
3136 env(rate(gw, 1.25));
3137 env.close();
3138 // This offer succeeds to cross pre- and post-amendment
3139 // because the strand's out amount is small enough to match
3140 // limitQuality value and limitOut() function in StrandFlow
3141 // doesn't require an adjustment to out value.
3142 env(offer(carol, EUR(100), GBP(100)));
3143 env.close();
3144 // No transfer fee
3145 BEAST_EXPECT(ammAlice.expectBalances(GBP(1'100), EUR(1'000), ammAlice.tokens()));
3146 // Initial 30,000 - 100(offer) - 25% transfer fee
3147 BEAST_EXPECT(expectHolding(env, carol, GBP(29'875)));
3148 // Initial 30,000 + 100(offer)
3149 BEAST_EXPECT(expectHolding(env, carol, EUR(30'100)));
3150 BEAST_EXPECT(expectOffers(env, bob, 0));
3151 },
3152 {{GBP(1'000), EUR(1'100)}},
3153 0,
3155 {features});
3156 // Single-path AMM offer
3157 testAMM(
3158 [&](AMM& amm, Env& env) {
3159 env(rate(gw, 1.001));
3160 env.close();
3161 env(offer(carol, XRP(100), USD(55)));
3162 env.close();
3163 if (!features[fixAMMv1_1])
3164 {
3165 // Pre-amendment the transfer fee is not taken into
3166 // account when calculating the limit out based on
3167 // limitQuality. Carol pays 0.1% on the takerGets, which
3168 // lowers the overall quality. AMM offer is generated based
3169 // on higher limit out, which generates a larger offer
3170 // with lower quality. Consequently, the offer fails
3171 // to cross.
3172 BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
3173 BEAST_EXPECT(expectOffers(env, carol, 1, {{Amounts{XRP(100), USD(55)}}}));
3174 }
3175 else
3176 {
3177 // Post-amendment the transfer fee is taken into account
3178 // when calculating the limit out based on limitQuality.
3179 // This increases the limitQuality and decreases
3180 // the limit out. Consequently, AMM offer size is decreased,
3181 // and the quality is increased, matching the overall
3182 // quality.
3183 // AMM offer ~50USD/91XRP
3184 BEAST_EXPECT(amm.expectBalances(
3185 XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens()));
3186 // Offer ~91XRP/49.99USD
3187 BEAST_EXPECT(
3188 expectOffers(env, carol, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
3189 // Carol pays 0.1% fee on ~50USD =~ 0.05USD
3190 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(29'949'94999999494), -11));
3191 }
3192 },
3193 {{XRP(1'000), USD(500)}},
3194 0,
3196 {features});
3197 testAMM(
3198 [&](AMM& amm, Env& env) {
3199 env(rate(gw, 1.001));
3200 env.close();
3201 env(offer(carol, XRP(10), USD(5.5)));
3202 env.close();
3203 if (!features[fixAMMv1_1])
3204 {
3205 BEAST_EXPECT(
3206 amm.expectBalances(XRP(990), STAmount{USD, UINT64_C(505'050505050505), -12}, amm.tokens()));
3207 BEAST_EXPECT(expectOffers(env, carol, 0));
3208 }
3209 else
3210 {
3211 BEAST_EXPECT(
3212 amm.expectBalances(XRP(990), STAmount{USD, UINT64_C(505'0505050505051), -13}, amm.tokens()));
3213 BEAST_EXPECT(expectOffers(env, carol, 0));
3214 }
3215 },
3216 {{XRP(1'000), USD(500)}},
3217 0,
3219 {features});
3220 // Multi-path AMM offer
3221 testAMM(
3222 [&](AMM& ammAlice, Env& env) {
3223 Account const ed("ed");
3224 fund(env, gw, {bob, ed}, XRP(30'000), {GBP(2'000), EUR(2'000)}, Fund::Acct);
3225 env(rate(gw, 1.25));
3226 env.close();
3227 // The auto-bridge is worse quality than AMM, is not consumed
3228 // first and initially forces multi-path AMM offer generation.
3229 // Multi-path AMM offers are consumed until their quality
3230 // is less than the auto-bridge offers quality. Auto-bridge
3231 // offers are consumed afterward. Then the behavior is
3232 // different pre-amendment and post-amendment.
3233 env(offer(bob, GBP(10), XRP(10)), txflags(tfPassive));
3234 env(offer(ed, XRP(10), EUR(10)), txflags(tfPassive));
3235 env.close();
3236 env(offer(carol, EUR(100), GBP(100)));
3237 env.close();
3238 if (!features[fixAMMv1_1])
3239 {
3240 // After the auto-bridge offers are consumed, single path
3241 // AMM offer is generated with the limit out not taking
3242 // into consideration the transfer fee. This results
3243 // in an overall lower quality offer than the limit quality
3244 // and the single path AMM offer fails to consume.
3245 // Total consumed ~37.06GBP/39.32EUR
3246 BEAST_EXPECT(ammAlice.expectBalances(
3247 STAmount{GBP, UINT64_C(1'037'06583722133), -11},
3248 STAmount{EUR, UINT64_C(1'060'684828792831), -12},
3249 ammAlice.tokens()));
3250 // Consumed offer ~49.32EUR/49.32GBP
3251 BEAST_EXPECT(expectOffers(
3252 env,
3253 carol,
3254 1,
3255 {Amounts{
3256 STAmount{EUR, UINT64_C(50'684828792831), -12},
3257 STAmount{GBP, UINT64_C(50'684828792831), -12}}}));
3258 BEAST_EXPECT(expectOffers(env, bob, 0));
3259 BEAST_EXPECT(expectOffers(env, ed, 0));
3260
3261 // Initial 30,000 - ~47.06(offers = 37.06(AMM) + 10(LOB))
3262 // * 1.25
3263 // = 58.825 = ~29941.17
3264 // carol bought ~72.93EUR at the cost of ~70.68GBP
3265 // the offer is partially consumed
3266 BEAST_EXPECT(expectHolding(env, carol, STAmount{GBP, UINT64_C(29'941'16770347333), -11}));
3267 // Initial 30,000 + ~49.3(offers = 39.3(AMM) + 10(LOB))
3268 BEAST_EXPECT(expectHolding(env, carol, STAmount{EUR, UINT64_C(30'049'31517120716), -11}));
3269 }
3270 else
3271 {
3272 // After the auto-bridge offers are consumed, single path
3273 // AMM offer is generated with the limit out taking
3274 // into consideration the transfer fee. This results
3275 // in an overall quality offer matching the limit quality
3276 // and the single path AMM offer is consumed. More
3277 // liquidity is consumed overall in post-amendment.
3278 // Total consumed ~60.68GBP/62.93EUR
3279 BEAST_EXPECT(ammAlice.expectBalances(
3280 STAmount{GBP, UINT64_C(1'060'684828792832), -12},
3281 STAmount{EUR, UINT64_C(1'037'06583722134), -11},
3282 ammAlice.tokens()));
3283 // Consumed offer ~72.93EUR/72.93GBP
3284 BEAST_EXPECT(expectOffers(
3285 env,
3286 carol,
3287 1,
3288 {Amounts{
3289 STAmount{EUR, UINT64_C(27'06583722134028), -14},
3290 STAmount{GBP, UINT64_C(27'06583722134028), -14}}}));
3291 BEAST_EXPECT(expectOffers(env, bob, 0));
3292 BEAST_EXPECT(expectOffers(env, ed, 0));
3293
3294 // Initial 30,000 - ~70.68(offers = 60.68(AMM) + 10(LOB))
3295 // * 1.25
3296 // = 88.35 = ~29911.64
3297 // carol bought ~72.93EUR at the cost of ~70.68GBP
3298 // the offer is partially consumed
3299 BEAST_EXPECT(expectHolding(env, carol, STAmount{GBP, UINT64_C(29'911'64396400896), -11}));
3300 // Initial 30,000 + ~72.93(offers = 62.93(AMM) + 10(LOB))
3301 BEAST_EXPECT(expectHolding(env, carol, STAmount{EUR, UINT64_C(30'072'93416277865), -11}));
3302 }
3303 // Initial 2000 + 10 = 2010
3304 BEAST_EXPECT(expectHolding(env, bob, GBP(2'010)));
3305 // Initial 2000 - 10 * 1.25 = 1987.5
3306 BEAST_EXPECT(expectHolding(env, ed, EUR(1'987.5)));
3307 },
3308 {{GBP(1'000), EUR(1'100)}},
3309 0,
3311 {features});
3312
3313 // Payment and transfer fee
3314 // Scenario:
3315 // Bob sends 125GBP to pay 80EUR to Carol
3316 // Payment execution:
3317 // bob's 125GBP/1.25 = 100GBP
3318 // 100GBP/100EUR AMM offer
3319 // 100EUR/1.25 = 80EUR paid to carol
3320 testAMM(
3321 [&](AMM& ammAlice, Env& env) {
3322 fund(env, gw, {bob}, {GBP(200), EUR(200)}, Fund::Acct);
3323 env(rate(gw, 1.25));
3324 env.close();
3325 env(pay(bob, carol, EUR(100)), path(~EUR), sendmax(GBP(125)), txflags(tfPartialPayment));
3326 env.close();
3327 BEAST_EXPECT(ammAlice.expectBalances(GBP(1'100), EUR(1'000), ammAlice.tokens()));
3328 BEAST_EXPECT(expectHolding(env, bob, GBP(75)));
3329 BEAST_EXPECT(expectHolding(env, carol, EUR(30'080)));
3330 },
3331 {{GBP(1'000), EUR(1'100)}},
3332 0,
3334 {features});
3335
3336 // Payment and transfer fee, multiple steps
3337 // Scenario:
3338 // Dan's offer 200CAN/200GBP
3339 // AMM 1000GBP/10125EUR
3340 // Ed's offer 200EUR/200USD
3341 // Bob sends 195.3125CAN to pay 100USD to Carol
3342 // Payment execution:
3343 // bob's 195.3125CAN/1.25 = 156.25CAN -> dan's offer
3344 // 156.25CAN/156.25GBP 156.25GBP/1.25 = 125GBP -> AMM's offer
3345 // 125GBP/125EUR 125EUR/1.25 = 100EUR -> ed's offer
3346 // 100EUR/100USD 100USD/1.25 = 80USD paid to carol
3347 testAMM(
3348 [&](AMM& ammAlice, Env& env) {
3349 Account const dan("dan");
3350 Account const ed("ed");
3351 auto const CAN = gw["CAN"];
3352 fund(env, gw, {dan}, {CAN(200), GBP(200)}, Fund::Acct);
3353 fund(env, gw, {ed}, {EUR(200), USD(200)}, Fund::Acct);
3354 fund(env, gw, {bob}, {CAN(195.3125)}, Fund::Acct);
3355 env(trust(carol, USD(100)));
3356 env(rate(gw, 1.25));
3357 env.close();
3358 env(offer(dan, CAN(200), GBP(200)));
3359 env(offer(ed, EUR(200), USD(200)));
3360 env.close();
3361 env(pay(bob, carol, USD(100)),
3362 path(~GBP, ~EUR, ~USD),
3363 sendmax(CAN(195.3125)),
3365 env.close();
3366 BEAST_EXPECT(expectHolding(env, bob, CAN(0)));
3367 BEAST_EXPECT(expectHolding(env, dan, CAN(356.25), GBP(43.75)));
3368 BEAST_EXPECT(ammAlice.expectBalances(GBP(10'125), EUR(10'000), ammAlice.tokens()));
3369 BEAST_EXPECT(expectHolding(env, ed, EUR(300), USD(100)));
3370 BEAST_EXPECT(expectHolding(env, carol, USD(80)));
3371 },
3372 {{GBP(10'000), EUR(10'125)}},
3373 0,
3375 {features});
3376
3377 // Pay amounts close to one side of the pool
3378 testAMM(
3379 [&](AMM& ammAlice, Env& env) {
3380 env(pay(alice, carol, USD(99.99)),
3381 path(~USD),
3382 sendmax(XRP(1)),
3384 ter(tesSUCCESS));
3385 env(pay(alice, carol, USD(100)),
3386 path(~USD),
3387 sendmax(XRP(1)),
3389 ter(tesSUCCESS));
3390 env(pay(alice, carol, XRP(100)),
3391 path(~XRP),
3392 sendmax(USD(1)),
3394 ter(tesSUCCESS));
3395 env(pay(alice, carol, STAmount{xrpIssue(), 99'999'900}),
3396 path(~XRP),
3397 sendmax(USD(1)),
3399 ter(tesSUCCESS));
3400 },
3401 {{XRP(100), USD(100)}},
3402 0,
3404 {features});
3405
3406 // Multiple paths/steps
3407 {
3408 Env env(*this, features);
3409 auto const ETH = gw["ETH"];
3410 fund(env, gw, {alice}, XRP(100'000), {EUR(50'000), BTC(50'000), ETH(50'000), USD(50'000)});
3411 fund(env, gw, {carol, bob}, XRP(1'000), {USD(200)}, Fund::Acct);
3412 AMM xrp_eur(env, alice, XRP(10'100), EUR(10'000));
3413 AMM eur_btc(env, alice, EUR(10'000), BTC(10'200));
3414 AMM btc_usd(env, alice, BTC(10'100), USD(10'000));
3415 AMM xrp_usd(env, alice, XRP(10'150), USD(10'200));
3416 AMM xrp_eth(env, alice, XRP(10'000), ETH(10'100));
3417 AMM eth_eur(env, alice, ETH(10'900), EUR(11'000));
3418 AMM eur_usd(env, alice, EUR(10'100), USD(10'000));
3419 env(pay(bob, carol, USD(100)),
3420 path(~EUR, ~BTC, ~USD),
3421 path(~USD),
3422 path(~ETH, ~EUR, ~USD),
3423 sendmax(XRP(200)));
3424 if (!features[fixAMMv1_1])
3425 {
3426 // XRP-ETH-EUR-USD
3427 // This path provides ~26.06USD/26.2XRP
3428 BEAST_EXPECT(xrp_eth.expectBalances(
3429 XRPAmount(10'026'208'900), STAmount{ETH, UINT64_C(10'073'65779244494), -11}, xrp_eth.tokens()));
3430 BEAST_EXPECT(eth_eur.expectBalances(
3431 STAmount{ETH, UINT64_C(10'926'34220755506), -11},
3432 STAmount{EUR, UINT64_C(10'973'54232078752), -11},
3433 eth_eur.tokens()));
3434 BEAST_EXPECT(eur_usd.expectBalances(
3435 STAmount{EUR, UINT64_C(10'126'45767921248), -11},
3436 STAmount{USD, UINT64_C(9'973'93151712086), -11},
3437 eur_usd.tokens()));
3438 // XRP-USD path
3439 // This path provides ~73.9USD/74.1XRP
3440 BEAST_EXPECT(xrp_usd.expectBalances(
3441 XRPAmount(10'224'106'246), STAmount{USD, UINT64_C(10'126'06848287914), -11}, xrp_usd.tokens()));
3442 }
3443 else
3444 {
3445 BEAST_EXPECT(xrp_eth.expectBalances(
3446 XRPAmount(10'026'208'900), STAmount{ETH, UINT64_C(10'073'65779244461), -11}, xrp_eth.tokens()));
3447 BEAST_EXPECT(eth_eur.expectBalances(
3448 STAmount{ETH, UINT64_C(10'926'34220755539), -11},
3449 STAmount{EUR, UINT64_C(10'973'5423207872), -10},
3450 eth_eur.tokens()));
3451 BEAST_EXPECT(eur_usd.expectBalances(
3452 STAmount{EUR, UINT64_C(10'126'4576792128), -10},
3453 STAmount{USD, UINT64_C(9'973'93151712057), -11},
3454 eur_usd.tokens()));
3455 // XRP-USD path
3456 // This path provides ~73.9USD/74.1XRP
3457 BEAST_EXPECT(xrp_usd.expectBalances(
3458 XRPAmount(10'224'106'246), STAmount{USD, UINT64_C(10'126'06848287943), -11}, xrp_usd.tokens()));
3459 }
3460
3461 // XRP-EUR-BTC-USD
3462 // This path doesn't provide any liquidity due to how
3463 // offers are generated in multi-path. Analytical solution
3464 // shows a different distribution:
3465 // XRP-EUR-BTC-USD 11.6USD/11.64XRP, XRP-USD 60.7USD/60.8XRP,
3466 // XRP-ETH-EUR-USD 27.6USD/27.6XRP
3467 BEAST_EXPECT(xrp_eur.expectBalances(XRP(10'100), EUR(10'000), xrp_eur.tokens()));
3468 BEAST_EXPECT(eur_btc.expectBalances(EUR(10'000), BTC(10'200), eur_btc.tokens()));
3469 BEAST_EXPECT(btc_usd.expectBalances(BTC(10'100), USD(10'000), btc_usd.tokens()));
3470
3471 BEAST_EXPECT(expectHolding(env, carol, USD(300)));
3472 }
3473
3474 // Dependent AMM
3475 {
3476 Env env(*this, features);
3477 auto const ETH = gw["ETH"];
3478 fund(env, gw, {alice}, XRP(40'000), {EUR(50'000), BTC(50'000), ETH(50'000), USD(50'000)});
3479 fund(env, gw, {carol, bob}, XRP(1000), {USD(200)}, Fund::Acct);
3480 AMM xrp_eur(env, alice, XRP(10'100), EUR(10'000));
3481 AMM eur_btc(env, alice, EUR(10'000), BTC(10'200));
3482 AMM btc_usd(env, alice, BTC(10'100), USD(10'000));
3483 AMM xrp_eth(env, alice, XRP(10'000), ETH(10'100));
3484 AMM eth_eur(env, alice, ETH(10'900), EUR(11'000));
3485 env(pay(bob, carol, USD(100)), path(~EUR, ~BTC, ~USD), path(~ETH, ~EUR, ~BTC, ~USD), sendmax(XRP(200)));
3486 if (!features[fixAMMv1_1])
3487 {
3488 // XRP-EUR-BTC-USD path provides ~17.8USD/~18.7XRP
3489 // XRP-ETH-EUR-BTC-USD path provides ~82.2USD/82.4XRP
3490 BEAST_EXPECT(xrp_eur.expectBalances(
3491 XRPAmount(10'118'738'472), STAmount{EUR, UINT64_C(9'981'544436337968), -12}, xrp_eur.tokens()));
3492 BEAST_EXPECT(eur_btc.expectBalances(
3493 STAmount{EUR, UINT64_C(10'101'16096785173), -11},
3494 STAmount{BTC, UINT64_C(10'097'91426968066), -11},
3495 eur_btc.tokens()));
3496 BEAST_EXPECT(btc_usd.expectBalances(
3497 STAmount{BTC, UINT64_C(10'202'08573031934), -11}, USD(9'900), btc_usd.tokens()));
3498 BEAST_EXPECT(xrp_eth.expectBalances(
3499 XRPAmount(10'082'446'397), STAmount{ETH, UINT64_C(10'017'41072778012), -11}, xrp_eth.tokens()));
3500 BEAST_EXPECT(eth_eur.expectBalances(
3501 STAmount{ETH, UINT64_C(10'982'58927221988), -11},
3502 STAmount{EUR, UINT64_C(10'917'2945958103), -10},
3503 eth_eur.tokens()));
3504 }
3505 else
3506 {
3507 BEAST_EXPECT(xrp_eur.expectBalances(
3508 XRPAmount(10'118'738'472), STAmount{EUR, UINT64_C(9'981'544436337923), -12}, xrp_eur.tokens()));
3509 BEAST_EXPECT(eur_btc.expectBalances(
3510 STAmount{EUR, UINT64_C(10'101'16096785188), -11},
3511 STAmount{BTC, UINT64_C(10'097'91426968059), -11},
3512 eur_btc.tokens()));
3513 BEAST_EXPECT(btc_usd.expectBalances(
3514 STAmount{BTC, UINT64_C(10'202'08573031941), -11}, USD(9'900), btc_usd.tokens()));
3515 BEAST_EXPECT(xrp_eth.expectBalances(
3516 XRPAmount(10'082'446'397), STAmount{ETH, UINT64_C(10'017'41072777996), -11}, xrp_eth.tokens()));
3517 BEAST_EXPECT(eth_eur.expectBalances(
3518 STAmount{ETH, UINT64_C(10'982'58927222004), -11},
3519 STAmount{EUR, UINT64_C(10'917'2945958102), -10},
3520 eth_eur.tokens()));
3521 }
3522 BEAST_EXPECT(expectHolding(env, carol, USD(300)));
3523 }
3524
3525 // AMM offers limit
3526 // Consuming 30 CLOB offers, results in hitting 30 AMM offers limit.
3527 testAMM(
3528 [&](AMM& ammAlice, Env& env) {
3529 env.fund(XRP(1'000), bob);
3530 fund(env, gw, {bob}, {EUR(400)}, Fund::IOUOnly);
3531 env(trust(alice, EUR(200)));
3532 for (int i = 0; i < 30; ++i)
3533 env(offer(alice, EUR(1.0 + 0.01 * i), XRP(1)));
3534 // This is worse quality offer than 30 offers above.
3535 // It will not be consumed because of AMM offers limit.
3536 env(offer(alice, EUR(140), XRP(100)));
3537 env(pay(bob, carol, USD(100)),
3538 path(~XRP, ~USD),
3539 sendmax(EUR(400)),
3541 if (!features[fixAMMv1_1])
3542 {
3543 // Carol gets ~29.91USD because of the AMM offers limit
3544 BEAST_EXPECT(ammAlice.expectBalances(
3545 XRP(10'030), STAmount{USD, UINT64_C(9'970'089730807577), -12}, ammAlice.tokens()));
3546 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, UINT64_C(30'029'91026919241), -11}));
3547 }
3548 else
3549 {
3550 BEAST_EXPECT(ammAlice.expectBalances(
3551 XRP(10'030), STAmount{USD, UINT64_C(9'970'089730807827), -12}, ammAlice.tokens()));
3552 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, UINT64_C(30'029'91026919217), -11}));
3553 }
3554 BEAST_EXPECT(expectOffers(env, alice, 1, {{{EUR(140), XRP(100)}}}));
3555 },
3557 0,
3559 {features});
3560 // This payment is fulfilled
3561 testAMM(
3562 [&](AMM& ammAlice, Env& env) {
3563 env.fund(XRP(1'000), bob);
3564 fund(env, gw, {bob}, {EUR(400)}, Fund::IOUOnly);
3565 env(trust(alice, EUR(200)));
3566 for (int i = 0; i < 29; ++i)
3567 env(offer(alice, EUR(1.0 + 0.01 * i), XRP(1)));
3568 // This is worse quality offer than 30 offers above.
3569 // It will not be consumed because of AMM offers limit.
3570 env(offer(alice, EUR(140), XRP(100)));
3571 env(pay(bob, carol, USD(100)),
3572 path(~XRP, ~USD),
3573 sendmax(EUR(400)),
3575 BEAST_EXPECT(ammAlice.expectBalances(XRPAmount{10'101'010'102}, USD(9'900), ammAlice.tokens()));
3576 if (!features[fixAMMv1_1])
3577 {
3578 // Carol gets ~100USD
3579 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, UINT64_C(30'099'99999999999), -11}));
3580 }
3581 else
3582 {
3583 BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
3584 }
3585 BEAST_EXPECT(
3586 expectOffers(env, alice, 1, {{{STAmount{EUR, UINT64_C(39'1858572), -7}, XRPAmount{27'989'898}}}}));
3587 },
3589 0,
3591 {features});
3592
3593 // Offer crossing with AMM and another offer. AMM has a better
3594 // quality and is consumed first.
3595 {
3596 Env env(*this, features);
3597 fund(env, gw, {alice, carol, bob}, XRP(30'000), {USD(30'000)});
3598 env(offer(bob, XRP(100), USD(100.001)));
3599 AMM ammAlice(env, alice, XRP(10'000), USD(10'100));
3600 env(offer(carol, USD(100), XRP(100)));
3601 if (!features[fixAMMv1_1])
3602 {
3603 BEAST_EXPECT(ammAlice.expectBalances(
3604 XRPAmount{10'049'825'373}, STAmount{USD, UINT64_C(10'049'92586949302), -11}, ammAlice.tokens()));
3605 BEAST_EXPECT(expectOffers(
3606 env, bob, 1, {{{XRPAmount{50'074'629}, STAmount{USD, UINT64_C(50'07513050698), -11}}}}));
3607 }
3608 else
3609 {
3610 BEAST_EXPECT(ammAlice.expectBalances(
3611 XRPAmount{10'049'825'372}, STAmount{USD, UINT64_C(10'049'92587049303), -11}, ammAlice.tokens()));
3612 BEAST_EXPECT(expectOffers(
3613 env, bob, 1, {{{XRPAmount{50'074'628}, STAmount{USD, UINT64_C(50'07512950697), -11}}}}));
3614 BEAST_EXPECT(expectHolding(env, carol, USD(30'100)));
3615 }
3616 }
3617
3618 // Individually frozen account
3619 testAMM(
3620 [&](AMM& ammAlice, Env& env) {
3621 env(trust(gw, carol["USD"](0), tfSetFreeze));
3622 env(trust(gw, alice["USD"](0), tfSetFreeze));
3623 env.close();
3624 env(pay(alice, carol, USD(1)),
3625 path(~USD),
3626 sendmax(XRP(10)),
3628 ter(tesSUCCESS));
3629 },
3631 0,
3633 {features});
3634 }
3635
3636 void
3638 {
3639 testcase("AMM Tokens");
3640 using namespace jtx;
3641
3642 // Offer crossing with AMM LPTokens and XRP.
3643 testAMM([&](AMM& ammAlice, Env& env) {
3644 auto const baseFee = env.current()->fees().base.drops();
3645 auto const token1 = ammAlice.lptIssue();
3646 auto priceXRP = ammAssetOut(
3647 STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 10'000'000}, STAmount{token1, 5'000'000}, 0);
3648 // Carol places an order to buy LPTokens
3649 env(offer(carol, STAmount{token1, 5'000'000}, priceXRP));
3650 // Alice places an order to sell LPTokens
3651 env(offer(alice, priceXRP, STAmount{token1, 5'000'000}));
3652 // Pool's LPTokens balance doesn't change
3653 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
3654 // Carol is Liquidity Provider
3655 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{5'000'000}));
3656 BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{5'000'000}));
3657 // Carol votes
3658 ammAlice.vote(carol, 1'000);
3659 BEAST_EXPECT(ammAlice.expectTradingFee(500));
3660 ammAlice.vote(carol, 0);
3661 BEAST_EXPECT(ammAlice.expectTradingFee(0));
3662 // Carol bids
3663 env(ammAlice.bid({.account = carol, .bidMin = 100}));
3664 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4'999'900}));
3665 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{100}));
3666 BEAST_EXPECT(accountBalance(env, carol) == std::to_string(22500000000 - 4 * baseFee));
3667 priceXRP = ammAssetOut(
3668 STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 9'999'900}, STAmount{token1, 4'999'900}, 0);
3669 // Carol withdraws
3670 ammAlice.withdrawAll(carol, XRP(0));
3671 BEAST_EXPECT(accountBalance(env, carol) == std::to_string(29999949999 - 5 * baseFee));
3672 BEAST_EXPECT(
3673 ammAlice.expectBalances(XRPAmount{10'000'000'000} - priceXRP, USD(10'000), IOUAmount{5'000'000}));
3674 BEAST_EXPECT(ammAlice.expectLPTokens(alice, IOUAmount{5'000'000}));
3675 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
3676 });
3677
3678 // Offer crossing with two AMM LPTokens.
3679 testAMM([&](AMM& ammAlice, Env& env) {
3680 ammAlice.deposit(carol, 1'000'000);
3681 fund(env, gw, {alice, carol}, {EUR(10'000)}, Fund::IOUOnly);
3682 AMM ammAlice1(env, alice, XRP(10'000), EUR(10'000));
3683 ammAlice1.deposit(carol, 1'000'000);
3684 auto const token1 = ammAlice.lptIssue();
3685 auto const token2 = ammAlice1.lptIssue();
3686 env(offer(alice, STAmount{token1, 100}, STAmount{token2, 100}), txflags(tfPassive));
3687 env.close();
3688 BEAST_EXPECT(expectOffers(env, alice, 1));
3689 env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100}));
3690 env.close();
3691 BEAST_EXPECT(
3692 expectHolding(env, alice, STAmount{token1, 10'000'100}) &&
3693 expectHolding(env, alice, STAmount{token2, 9'999'900}));
3694 BEAST_EXPECT(
3695 expectHolding(env, carol, STAmount{token2, 1'000'100}) &&
3696 expectHolding(env, carol, STAmount{token1, 999'900}));
3697 BEAST_EXPECT(expectOffers(env, alice, 0) && expectOffers(env, carol, 0));
3698 });
3699
3700 // LPs pay LPTokens directly. Must trust set because the trust line
3701 // is checked for the limit, which is 0 in the AMM auto-created
3702 // trust line.
3703 testAMM([&](AMM& ammAlice, Env& env) {
3704 auto const token1 = ammAlice.lptIssue();
3705 env.trust(STAmount{token1, 2'000'000}, carol);
3706 env.close();
3707 ammAlice.deposit(carol, 1'000'000);
3708 BEAST_EXPECT(
3709 ammAlice.expectLPTokens(alice, IOUAmount{10'000'000, 0}) &&
3710 ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
3711 // Pool balance doesn't change, only tokens moved from
3712 // one line to another.
3713 env(pay(alice, carol, STAmount{token1, 100}));
3714 env.close();
3715 BEAST_EXPECT(
3716 // Alice initial token1 10,000,000 - 100
3717 ammAlice.expectLPTokens(alice, IOUAmount{9'999'900, 0}) &&
3718 // Carol initial token1 1,000,000 + 100
3719 ammAlice.expectLPTokens(carol, IOUAmount{1'000'100, 0}));
3720
3721 env.trust(STAmount{token1, 20'000'000}, alice);
3722 env.close();
3723 env(pay(carol, alice, STAmount{token1, 100}));
3724 env.close();
3725 // Back to the original balance
3726 BEAST_EXPECT(
3727 ammAlice.expectLPTokens(alice, IOUAmount{10'000'000, 0}) &&
3728 ammAlice.expectLPTokens(carol, IOUAmount{1'000'000, 0}));
3729 });
3730 }
3731
3732 void
3734 {
3735 testcase("Amendment");
3737 FeatureBitset const noAMM{all - featureAMM};
3738 FeatureBitset const noNumber{all - fixUniversalNumber};
3739 FeatureBitset const noAMMAndNumber{all - featureAMM - fixUniversalNumber};
3740 using namespace jtx;
3741
3742 for (auto const& feature : {noAMM, noNumber, noAMMAndNumber})
3743 {
3744 Env env{*this, feature};
3745 fund(env, gw, {alice}, {USD(1'000)}, Fund::All);
3746 AMM amm(env, alice, XRP(1'000), USD(1'000), ter(temDISABLED));
3747
3748 env(amm.bid({.bidMax = 1000}), ter(temMALFORMED));
3749 env(amm.bid({}), ter(temDISABLED));
3750 amm.vote(VoteArg{.tfee = 100, .err = ter(temDISABLED)});
3751 amm.withdraw(WithdrawArg{.tokens = 100, .err = ter(temMALFORMED)});
3752 amm.withdraw(WithdrawArg{.err = ter(temDISABLED)});
3753 amm.deposit(DepositArg{.asset1In = USD(100), .err = ter(temDISABLED)});
3754 amm.ammDelete(alice, ter(temDISABLED));
3755 }
3756 }
3757
3758 void
3760 {
3761 testcase("Flags");
3762 using namespace jtx;
3763
3764 testAMM([&](AMM& ammAlice, Env& env) {
3765 auto const info = env.rpc(
3766 "json", "account_info", std::string("{\"account\": \"" + to_string(ammAlice.ammAccount()) + "\"}"));
3767 auto const flags = info[jss::result][jss::account_data][jss::Flags].asUInt();
3769 });
3770 }
3771
3772 void
3774 {
3775 testcase("Rippling");
3776 using namespace jtx;
3777
3778 // Rippling via AMM fails because AMM trust line has 0 limit.
3779 // Set up two issuers, A and B. Have each issue a token called TST.
3780 // Have another account C hold TST from both issuers,
3781 // and create an AMM for this pair.
3782 // Have a fourth account, D, create a trust line to the AMM for TST.
3783 // Send a payment delivering TST.AMM from C to D, using SendMax in
3784 // TST.A (or B) and a path through the AMM account. By normal
3785 // rippling rules, this would have caused the AMM's balances
3786 // to shift at a 1:1 rate with no fee applied has it not been
3787 // for 0 limit.
3788 {
3789 Env env(*this);
3790 auto const A = Account("A");
3791 auto const B = Account("B");
3792 auto const TSTA = A["TST"];
3793 auto const TSTB = B["TST"];
3794 auto const C = Account("C");
3795 auto const D = Account("D");
3796
3797 env.fund(XRP(10'000), A);
3798 env.fund(XRP(10'000), B);
3799 env.fund(XRP(10'000), C);
3800 env.fund(XRP(10'000), D);
3801
3802 env.trust(TSTA(10'000), C);
3803 env.trust(TSTB(10'000), C);
3804 env(pay(A, C, TSTA(10'000)));
3805 env(pay(B, C, TSTB(10'000)));
3806 AMM amm(env, C, TSTA(5'000), TSTB(5'000));
3807 auto const ammIss = Issue(TSTA.currency, amm.ammAccount());
3808
3809 // Can SetTrust only for AMM LP tokens
3810 env(trust(D, STAmount{ammIss, 10'000}), ter(tecNO_PERMISSION));
3811 env.close();
3812
3813 // The payment would fail because of above, but check just in case
3814 env(pay(C, D, STAmount{ammIss, 10}),
3815 sendmax(TSTA(100)),
3816 path(amm.ammAccount()),
3818 ter(tecPATH_DRY));
3819 }
3820 }
3821
3822 void
3824 {
3825 testcase("AMMAndCLOB, offer quality change");
3826 using namespace jtx;
3827 auto const gw = Account("gw");
3828 auto const TST = gw["TST"];
3829 auto const LP1 = Account("LP1");
3830 auto const LP2 = Account("LP2");
3831
3832 auto prep = [&](auto const& offerCb, auto const& expectCb) {
3833 Env env(*this, features);
3834 env.fund(XRP(30'000'000'000), gw);
3835 env(offer(gw, XRP(11'500'000'000), TST(1'000'000'000)));
3836
3837 env.fund(XRP(10'000), LP1);
3838 env.fund(XRP(10'000), LP2);
3839 env(offer(LP1, TST(25), XRPAmount(287'500'000)));
3840
3841 // Either AMM or CLOB offer
3842 offerCb(env);
3843
3844 env(offer(LP2, TST(25), XRPAmount(287'500'000)));
3845
3846 expectCb(env);
3847 };
3848
3849 // If we replace AMM with an equivalent CLOB offer, which AMM generates
3850 // when it is consumed, then the result must be equivalent, too.
3851 std::string lp2TSTBalance;
3852 std::string lp2TakerGets;
3853 std::string lp2TakerPays;
3854 // Execute with AMM first
3855 prep(
3856 [&](Env& env) { AMM amm(env, LP1, TST(25), XRP(250)); },
3857 [&](Env& env) {
3858 lp2TSTBalance = getAccountLines(env, LP2, TST)["lines"][0u]["balance"].asString();
3859 auto const offer = getAccountOffers(env, LP2)["offers"][0u];
3860 lp2TakerGets = offer["taker_gets"].asString();
3861 lp2TakerPays = offer["taker_pays"]["value"].asString();
3862 });
3863 // Execute with CLOB offer
3864 prep(
3865 [&](Env& env) {
3866 if (!features[fixAMMv1_1])
3867 env(offer(LP1, XRPAmount{18'095'133}, STAmount{TST, UINT64_C(1'68737984885388), -14}),
3869 else
3870 env(offer(LP1, XRPAmount{18'095'132}, STAmount{TST, UINT64_C(1'68737976189735), -14}),
3872 },
3873 [&](Env& env) {
3874 BEAST_EXPECT(lp2TSTBalance == getAccountLines(env, LP2, TST)["lines"][0u]["balance"].asString());
3875 auto const offer = getAccountOffers(env, LP2)["offers"][0u];
3876 BEAST_EXPECT(lp2TakerGets == offer["taker_gets"].asString());
3877 BEAST_EXPECT(lp2TakerPays == offer["taker_pays"]["value"].asString());
3878 });
3879 }
3880
3881 void
3883 {
3884 testcase("Trading Fee");
3885 using namespace jtx;
3886
3887 // Single Deposit, 1% fee
3888 testAMM(
3889 [&](AMM& ammAlice, Env& env) {
3890 // No fee
3891 ammAlice.deposit(carol, USD(3'000));
3892 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000}));
3893 ammAlice.withdrawAll(carol, USD(3'000));
3894 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0}));
3895 BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
3896 // Set fee to 1%
3897 ammAlice.vote(alice, 1'000);
3898 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
3899 // Carol gets fewer LPToken ~994, because of the single deposit
3900 // fee
3901 ammAlice.deposit(carol, USD(3'000));
3902 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{994'981155689671, -12}));
3903 BEAST_EXPECT(expectHolding(env, carol, USD(27'000)));
3904 // Set fee to 0
3905 ammAlice.vote(alice, 0);
3906 ammAlice.withdrawAll(carol, USD(0));
3907 // Carol gets back less than the original deposit
3908 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, UINT64_C(29'994'96220068281), -11}));
3909 },
3910 {{USD(1'000), EUR(1'000)}},
3911 0,
3913 {features});
3914
3915 // Single deposit with EP not exceeding specified:
3916 // 100USD with EP not to exceed 0.1 (AssetIn/TokensOut). 1% fee.
3917 testAMM(
3918 [&](AMM& ammAlice, Env& env) {
3919 auto const balance = env.balance(carol, USD);
3920 auto tokensFee = ammAlice.deposit(carol, USD(1'000), std::nullopt, STAmount{USD, 1, -1});
3921 auto const deposit = balance - env.balance(carol, USD);
3922 ammAlice.withdrawAll(carol, USD(0));
3923 ammAlice.vote(alice, 0);
3924 BEAST_EXPECT(ammAlice.expectTradingFee(0));
3925 auto const tokensNoFee = ammAlice.deposit(carol, deposit);
3926 // carol pays ~2008 LPTokens in fees or ~0.5% of the no-fee
3927 // LPTokens
3928 BEAST_EXPECT(tokensFee == IOUAmount(485'636'0611129, -7));
3929 BEAST_EXPECT(tokensNoFee == IOUAmount(487'644'85901109, -8));
3930 },
3932 1'000,
3934 {features});
3935
3936 // Single deposit with EP not exceeding specified:
3937 // 200USD with EP not to exceed 0.002020 (AssetIn/TokensOut). 1% fee
3938 testAMM(
3939 [&](AMM& ammAlice, Env& env) {
3940 auto const balance = env.balance(carol, USD);
3941 auto const tokensFee = ammAlice.deposit(carol, USD(200), std::nullopt, STAmount{USD, 2020, -6});
3942 auto const deposit = balance - env.balance(carol, USD);
3943 ammAlice.withdrawAll(carol, USD(0));
3944 ammAlice.vote(alice, 0);
3945 BEAST_EXPECT(ammAlice.expectTradingFee(0));
3946 auto const tokensNoFee = ammAlice.deposit(carol, deposit);
3947 // carol pays ~475 LPTokens in fees or ~0.5% of the no-fee
3948 // LPTokens
3949 BEAST_EXPECT(tokensFee == IOUAmount(98'000'00000002, -8));
3950 BEAST_EXPECT(tokensNoFee == IOUAmount(98'475'81871545, -8));
3951 },
3953 1'000,
3955 {features});
3956
3957 // Single Withdrawal, 1% fee
3958 testAMM(
3959 [&](AMM& ammAlice, Env& env) {
3960 // No fee
3961 ammAlice.deposit(carol, USD(3'000));
3962
3963 BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{1'000}));
3964 BEAST_EXPECT(expectHolding(env, carol, USD(27'000)));
3965 // Set fee to 1%
3966 ammAlice.vote(alice, 1'000);
3967 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
3968 // Single withdrawal. Carol gets ~5USD less than deposited.
3969 ammAlice.withdrawAll(carol, USD(0));
3970 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, UINT64_C(29'994'97487437186), -11}));
3971 },
3972 {{USD(1'000), EUR(1'000)}},
3973 0,
3975 {features});
3976
3977 // Withdraw with EPrice limit, 1% fee.
3978 testAMM(
3979 [&](AMM& ammAlice, Env& env) {
3980 ammAlice.deposit(carol, 1'000'000);
3981 auto const tokensFee = ammAlice.withdraw(carol, USD(100), std::nullopt, IOUAmount{520, 0});
3982 // carol withdraws ~1,443.44USD
3983 auto const balanceAfterWithdraw = [&]() {
3984 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
3985 return STAmount(USD, UINT64_C(30'443'43891402715), -11);
3986 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
3987 return STAmount(USD, UINT64_C(30'443'43891402714), -11);
3988 else
3989 return STAmount(USD, UINT64_C(30'443'43891402713), -11);
3990 }();
3991 BEAST_EXPECT(env.balance(carol, USD) == balanceAfterWithdraw);
3992 // Set to original pool size
3993 auto const deposit = balanceAfterWithdraw - USD(29'000);
3994 ammAlice.deposit(carol, deposit);
3995 // fee 0%
3996 ammAlice.vote(alice, 0);
3997 BEAST_EXPECT(ammAlice.expectTradingFee(0));
3998 auto const tokensNoFee = ammAlice.withdraw(carol, deposit);
3999 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
4000 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(30'443'43891402717), -11));
4001 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
4002 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(30'443'43891402716), -11));
4003 else
4004 BEAST_EXPECT(env.balance(carol, USD) == STAmount(USD, UINT64_C(30'443'43891402713), -11));
4005 // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee
4006 // LPTokens
4007 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
4008 BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779913, -8));
4009 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
4010 BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779912, -8));
4011 else
4012 BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779911, -8));
4013 BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8));
4014 },
4016 1'000,
4018 {features});
4019
4020 // Payment, 1% fee
4021 testAMM(
4022 [&](AMM& ammAlice, Env& env) {
4023 fund(env, gw, {bob}, XRP(1'000), {USD(1'000), EUR(1'000)}, Fund::Acct);
4024 // Alice contributed 1010EUR and 1000USD to the pool
4025 BEAST_EXPECT(expectHolding(env, alice, EUR(28'990)));
4026 BEAST_EXPECT(expectHolding(env, alice, USD(29'000)));
4027 BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
4028 // Carol pays to Alice with no fee
4029 env(pay(carol, alice, EUR(10)), path(~EUR), sendmax(USD(10)), txflags(tfNoRippleDirect));
4030 env.close();
4031 // Alice has 10EUR more and Carol has 10USD less
4032 BEAST_EXPECT(expectHolding(env, alice, EUR(29'000)));
4033 BEAST_EXPECT(expectHolding(env, alice, USD(29'000)));
4034 BEAST_EXPECT(expectHolding(env, carol, USD(29'990)));
4035
4036 // Set fee to 1%
4037 ammAlice.vote(alice, 1'000);
4038 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
4039 // Bob pays to Carol with 1% fee
4040 env(pay(bob, carol, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect));
4041 env.close();
4042 // Bob sends 10.1~EUR to pay 10USD
4043 BEAST_EXPECT(expectHolding(env, bob, STAmount{EUR, UINT64_C(989'8989898989899), -13}));
4044 // Carol got 10USD
4045 BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
4046 BEAST_EXPECT(ammAlice.expectBalances(
4047 USD(1'000), STAmount{EUR, UINT64_C(1'010'10101010101), -11}, ammAlice.tokens()));
4048 },
4049 {{USD(1'000), EUR(1'010)}},
4050 0,
4052 {features});
4053
4054 // Offer crossing, 0.5% fee
4055 testAMM(
4056 [&](AMM& ammAlice, Env& env) {
4057 // No fee
4058 env(offer(carol, EUR(10), USD(10)));
4059 env.close();
4060 BEAST_EXPECT(expectHolding(env, carol, USD(29'990)));
4061 BEAST_EXPECT(expectHolding(env, carol, EUR(30'010)));
4062 // Change pool composition back
4063 env(offer(carol, USD(10), EUR(10)));
4064 env.close();
4065 // Set fee to 0.5%
4066 ammAlice.vote(alice, 500);
4067 BEAST_EXPECT(ammAlice.expectTradingFee(500));
4068 env(offer(carol, EUR(10), USD(10)));
4069 env.close();
4070 // Alice gets fewer ~4.97EUR for ~5.02USD, the difference goes
4071 // to the pool
4072 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, UINT64_C(29'995'02512562814), -11}));
4073 BEAST_EXPECT(expectHolding(env, carol, STAmount{EUR, UINT64_C(30'004'97487437186), -11}));
4074 BEAST_EXPECT(expectOffers(
4075 env,
4076 carol,
4077 1,
4078 {{Amounts{
4079 STAmount{EUR, UINT64_C(5'025125628140703), -15},
4080 STAmount{USD, UINT64_C(5'025125628140703), -15}}}}));
4081 if (!features[fixAMMv1_1])
4082 {
4083 BEAST_EXPECT(ammAlice.expectBalances(
4084 STAmount{USD, UINT64_C(1'004'974874371859), -12},
4085 STAmount{EUR, UINT64_C(1'005'025125628141), -12},
4086 ammAlice.tokens()));
4087 }
4088 else
4089 {
4090 BEAST_EXPECT(ammAlice.expectBalances(
4091 STAmount{USD, UINT64_C(1'004'97487437186), -11},
4092 STAmount{EUR, UINT64_C(1'005'025125628141), -12},
4093 ammAlice.tokens()));
4094 }
4095 },
4096 {{USD(1'000), EUR(1'010)}},
4097 0,
4099 {features});
4100
4101 // Payment with AMM and CLOB offer, 0 fee
4102 // AMM liquidity is consumed first up to CLOB offer quality
4103 // CLOB offer is fully consumed next
4104 // Remaining amount is consumed via AMM liquidity
4105 {
4106 Env env(*this, features);
4107 Account const ed("ed");
4108 fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
4109 env(offer(carol, EUR(5), USD(5)));
4110 AMM ammAlice(env, alice, USD(1'005), EUR(1'000));
4111 env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect));
4112 BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
4113 if (!features[fixAMMv1_1])
4114 {
4115 BEAST_EXPECT(expectHolding(env, bob, EUR(1'990)));
4116 BEAST_EXPECT(ammAlice.expectBalances(USD(1'000), EUR(1'005), ammAlice.tokens()));
4117 }
4118 else
4119 {
4120 BEAST_EXPECT(expectHolding(env, bob, STAmount(EUR, UINT64_C(1989'999999999999), -12)));
4121 BEAST_EXPECT(ammAlice.expectBalances(
4122 USD(1'000), STAmount(EUR, UINT64_C(1005'000000000001), -12), ammAlice.tokens()));
4123 }
4124 BEAST_EXPECT(expectOffers(env, carol, 0));
4125 }
4126
4127 // Payment with AMM and CLOB offer. Same as above but with 0.25%
4128 // fee.
4129 {
4130 Env env(*this, features);
4131 Account const ed("ed");
4132 fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
4133 env(offer(carol, EUR(5), USD(5)));
4134 // Set 0.25% fee
4135 AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 250);
4136 env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect));
4137 BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
4138 if (!features[fixAMMv1_1])
4139 {
4140 BEAST_EXPECT(expectHolding(env, bob, STAmount{EUR, UINT64_C(1'989'987453007618), -12}));
4141 BEAST_EXPECT(ammAlice.expectBalances(
4142 USD(1'000), STAmount{EUR, UINT64_C(1'005'012546992382), -12}, ammAlice.tokens()));
4143 }
4144 else
4145 {
4146 BEAST_EXPECT(expectHolding(env, bob, STAmount{EUR, UINT64_C(1'989'987453007628), -12}));
4147 BEAST_EXPECT(ammAlice.expectBalances(
4148 USD(1'000), STAmount{EUR, UINT64_C(1'005'012546992372), -12}, ammAlice.tokens()));
4149 }
4150 BEAST_EXPECT(expectOffers(env, carol, 0));
4151 }
4152
4153 // Payment with AMM and CLOB offer. AMM has a better
4154 // spot price quality, but 1% fee offsets that. As the result
4155 // the entire trade is executed via LOB.
4156 {
4157 Env env(*this, features);
4158 Account const ed("ed");
4159 fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
4160 env(offer(carol, EUR(10), USD(10)));
4161 // Set 1% fee
4162 AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 1'000);
4163 env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect));
4164 BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
4165 BEAST_EXPECT(expectHolding(env, bob, EUR(1'990)));
4166 BEAST_EXPECT(ammAlice.expectBalances(USD(1'005), EUR(1'000), ammAlice.tokens()));
4167 BEAST_EXPECT(expectOffers(env, carol, 0));
4168 }
4169
4170 // Payment with AMM and CLOB offer. AMM has a better
4171 // spot price quality, but 1% fee offsets that.
4172 // The CLOB offer is consumed first and the remaining
4173 // amount is consumed via AMM liquidity.
4174 {
4175 Env env(*this, features);
4176 Account const ed("ed");
4177 fund(env, gw, {alice, bob, carol, ed}, XRP(1'000), {USD(2'000), EUR(2'000)});
4178 env(offer(carol, EUR(9), USD(9)));
4179 // Set 1% fee
4180 AMM ammAlice(env, alice, USD(1'005), EUR(1'000), false, 1'000);
4181 env(pay(bob, ed, USD(10)), path(~USD), sendmax(EUR(15)), txflags(tfNoRippleDirect));
4182 BEAST_EXPECT(expectHolding(env, ed, USD(2'010)));
4183 BEAST_EXPECT(expectHolding(env, bob, STAmount{EUR, UINT64_C(1'989'993923296712), -12}));
4184 BEAST_EXPECT(ammAlice.expectBalances(
4185 USD(1'004), STAmount{EUR, UINT64_C(1'001'006076703288), -12}, ammAlice.tokens()));
4186 BEAST_EXPECT(expectOffers(env, carol, 0));
4187 }
4188 }
4189
4190 void
4192 {
4193 testcase("Adjusted Deposit/Withdraw Tokens");
4194
4195 using namespace jtx;
4196
4197 // Deposit/Withdraw in USD
4198 testAMM(
4199 [&](AMM& ammAlice, Env& env) {
4200 Account const bob("bob");
4201 Account const ed("ed");
4202 Account const paul("paul");
4203 Account const dan("dan");
4204 Account const chris("chris");
4205 Account const simon("simon");
4206 Account const ben("ben");
4207 Account const nataly("nataly");
4208 fund(env, gw, {bob, ed, paul, dan, chris, simon, ben, nataly}, {USD(1'500'000)}, Fund::Acct);
4209 for (int i = 0; i < 10; ++i)
4210 {
4211 ammAlice.deposit(ben, STAmount{USD, 1, -10});
4212 ammAlice.withdrawAll(ben, USD(0));
4213 ammAlice.deposit(simon, USD(0.1));
4214 ammAlice.withdrawAll(simon, USD(0));
4215 ammAlice.deposit(chris, USD(1));
4216 ammAlice.withdrawAll(chris, USD(0));
4217 ammAlice.deposit(dan, USD(10));
4218 ammAlice.withdrawAll(dan, USD(0));
4219 ammAlice.deposit(bob, USD(100));
4220 ammAlice.withdrawAll(bob, USD(0));
4221 ammAlice.deposit(carol, USD(1'000));
4222 ammAlice.withdrawAll(carol, USD(0));
4223 ammAlice.deposit(ed, USD(10'000));
4224 ammAlice.withdrawAll(ed, USD(0));
4225 ammAlice.deposit(paul, USD(100'000));
4226 ammAlice.withdrawAll(paul, USD(0));
4227 ammAlice.deposit(nataly, USD(1'000'000));
4228 ammAlice.withdrawAll(nataly, USD(0));
4229 }
4230 // Due to round off some accounts have a tiny gain, while
4231 // other have a tiny loss. The last account to withdraw
4232 // gets everything in the pool.
4233 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
4234 BEAST_EXPECT(ammAlice.expectBalances(
4235 XRP(10'000), STAmount{USD, UINT64_C(10'000'0000000013), -10}, IOUAmount{10'000'000}));
4236 else if (features[fixAMMv1_3])
4237 BEAST_EXPECT(ammAlice.expectBalances(
4238 XRP(10'000), STAmount{USD, UINT64_C(10'000'0000000003), -10}, IOUAmount{10'000'000}));
4239 else
4240 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
4241 BEAST_EXPECT(expectHolding(env, ben, USD(1'500'000)));
4242 BEAST_EXPECT(expectHolding(env, simon, USD(1'500'000)));
4243 BEAST_EXPECT(expectHolding(env, chris, USD(1'500'000)));
4244 BEAST_EXPECT(expectHolding(env, dan, USD(1'500'000)));
4245 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
4246 BEAST_EXPECT(expectHolding(env, carol, STAmount{USD, UINT64_C(30'000'00000000001), -11}));
4247 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
4248 BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
4249 else
4250 BEAST_EXPECT(expectHolding(env, carol, USD(30'000)));
4251 BEAST_EXPECT(expectHolding(env, ed, USD(1'500'000)));
4252 BEAST_EXPECT(expectHolding(env, paul, USD(1'500'000)));
4253 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
4254 BEAST_EXPECT(expectHolding(env, nataly, STAmount{USD, UINT64_C(1'500'000'000000002), -9}));
4255 else if (features[fixAMMv1_1] && !features[fixAMMv1_3])
4256 BEAST_EXPECT(expectHolding(env, nataly, STAmount{USD, UINT64_C(1'500'000'000000005), -9}));
4257 else
4258 BEAST_EXPECT(expectHolding(env, nataly, USD(1'500'000)));
4259 ammAlice.withdrawAll(alice);
4260 BEAST_EXPECT(!ammAlice.ammExists());
4261 if (!features[fixAMMv1_1])
4262 BEAST_EXPECT(expectHolding(env, alice, STAmount{USD, UINT64_C(30'000'0000000013), -10}));
4263 else if (features[fixAMMv1_3])
4264 BEAST_EXPECT(expectHolding(env, alice, STAmount{USD, UINT64_C(30'000'0000000003), -10}));
4265 else
4266 BEAST_EXPECT(expectHolding(env, alice, USD(30'000)));
4267 // alice XRP balance is 30,000initial - 50 ammcreate fee -
4268 // 10drops fee
4269 BEAST_EXPECT(
4270 accountBalance(env, alice) == std::to_string(29950000000 - env.current()->fees().base.drops()));
4271 },
4273 0,
4275 {features});
4276
4277 // Same as above but deposit/withdraw in XRP
4278 testAMM(
4279 [&](AMM& ammAlice, Env& env) {
4280 Account const bob("bob");
4281 Account const ed("ed");
4282 Account const paul("paul");
4283 Account const dan("dan");
4284 Account const chris("chris");
4285 Account const simon("simon");
4286 Account const ben("ben");
4287 Account const nataly("nataly");
4288 fund(env, gw, {bob, ed, paul, dan, chris, simon, ben, nataly}, XRP(2'000'000), {}, Fund::Acct);
4289 for (int i = 0; i < 10; ++i)
4290 {
4291 ammAlice.deposit(ben, XRPAmount{1});
4292 ammAlice.withdrawAll(ben, XRP(0));
4293 ammAlice.deposit(simon, XRPAmount(1'000));
4294 ammAlice.withdrawAll(simon, XRP(0));
4295 ammAlice.deposit(chris, XRP(1));
4296 ammAlice.withdrawAll(chris, XRP(0));
4297 ammAlice.deposit(dan, XRP(10));
4298 ammAlice.withdrawAll(dan, XRP(0));
4299 ammAlice.deposit(bob, XRP(100));
4300 ammAlice.withdrawAll(bob, XRP(0));
4301 ammAlice.deposit(carol, XRP(1'000));
4302 ammAlice.withdrawAll(carol, XRP(0));
4303 ammAlice.deposit(ed, XRP(10'000));
4304 ammAlice.withdrawAll(ed, XRP(0));
4305 ammAlice.deposit(paul, XRP(100'000));
4306 ammAlice.withdrawAll(paul, XRP(0));
4307 ammAlice.deposit(nataly, XRP(1'000'000));
4308 ammAlice.withdrawAll(nataly, XRP(0));
4309 }
4310 auto const baseFee = env.current()->fees().base.drops();
4311 if (!features[fixAMMv1_3])
4312 {
4313 // No round off with XRP in this test
4314 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), IOUAmount{10'000'000}));
4315 ammAlice.withdrawAll(alice);
4316 BEAST_EXPECT(!ammAlice.ammExists());
4317 // 20,000 initial - (deposit+withdraw) * 10
4318 auto const xrpBalance = (XRP(2'000'000) - txfee(env, 20)).getText();
4319 BEAST_EXPECT(accountBalance(env, ben) == xrpBalance);
4320 BEAST_EXPECT(accountBalance(env, simon) == xrpBalance);
4321 BEAST_EXPECT(accountBalance(env, chris) == xrpBalance);
4322 BEAST_EXPECT(accountBalance(env, dan) == xrpBalance);
4323
4324 // 30,000 initial - (deposit+withdraw) * 10
4325 BEAST_EXPECT(accountBalance(env, carol) == std::to_string(30'000'000'000 - 20 * baseFee));
4326 BEAST_EXPECT(accountBalance(env, ed) == xrpBalance);
4327 BEAST_EXPECT(accountBalance(env, paul) == xrpBalance);
4328 BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance);
4329 // 30,000 initial - 50 ammcreate fee - 10drops withdraw fee
4330 BEAST_EXPECT(accountBalance(env, alice) == std::to_string(29'950'000'000 - baseFee));
4331 }
4332 else
4333 {
4334 // post-amendment the rounding takes place to ensure
4335 // AMM invariant
4336 BEAST_EXPECT(
4337 ammAlice.expectBalances(XRPAmount(10'000'000'080), USD(10'000), IOUAmount{10'000'000}));
4338 ammAlice.withdrawAll(alice);
4339 BEAST_EXPECT(!ammAlice.ammExists());
4340 auto const xrpBalance = XRP(2'000'000) - txfee(env, 20) - drops(10);
4341 auto const xrpBalanceText = xrpBalance.getText();
4342 BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText);
4343 BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText);
4344 BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText);
4345 BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText);
4346 BEAST_EXPECT(accountBalance(env, carol) == std::to_string(30'000'000'000 - 20 * baseFee - 10));
4347 BEAST_EXPECT(accountBalance(env, ed) == (xrpBalance + drops(2)).getText());
4348 BEAST_EXPECT(accountBalance(env, paul) == (xrpBalance + drops(3)).getText());
4349 BEAST_EXPECT(accountBalance(env, nataly) == (xrpBalance + drops(5)).getText());
4350 BEAST_EXPECT(accountBalance(env, alice) == std::to_string(29'950'000'000 - baseFee + 80));
4351 }
4352 },
4354 0,
4356 {features});
4357 }
4358
4359 void
4361 {
4362 testcase("Auto Delete");
4363
4364 using namespace jtx;
4366
4367 {
4368 Env env(
4369 *this,
4371 cfg->FEES.reference_fee = XRPAmount(1);
4372 return cfg;
4373 }),
4374 all);
4375 fund(env, gw, {alice}, XRP(20'000), {USD(10'000)});
4376 AMM amm(env, gw, XRP(10'000), USD(10'000));
4377 for (auto i = 0; i < maxDeletableAMMTrustLines + 10; ++i)
4378 {
4379 Account const a{std::to_string(i)};
4380 env.fund(XRP(1'000), a);
4381 env(trust(a, STAmount{amm.lptIssue(), 10'000}));
4382 env.close();
4383 }
4384 // The trustlines are partially deleted,
4385 // AMM is set to an empty state.
4386 amm.withdrawAll(gw);
4387 BEAST_EXPECT(amm.ammExists());
4388
4389 // Bid,Vote,Deposit,Withdraw,SetTrust failing with
4390 // tecAMM_EMPTY. Deposit succeeds with tfTwoAssetIfEmpty option.
4391 env(amm.bid({
4392 .account = alice,
4393 .bidMin = 1000,
4394 }),
4395 ter(tecAMM_EMPTY));
4397 amm.withdraw(alice, 100, std::nullopt, std::nullopt, ter(tecAMM_EMPTY));
4398 amm.deposit(alice, USD(100), std::nullopt, std::nullopt, std::nullopt, ter(tecAMM_EMPTY));
4399 env(trust(alice, STAmount{amm.lptIssue(), 10'000}), ter(tecAMM_EMPTY));
4400
4401 // Can deposit with tfTwoAssetIfEmpty option
4402 amm.deposit(
4403 alice,
4405 XRP(10'000),
4406 USD(10'000),
4411 1'000);
4412 BEAST_EXPECT(amm.expectBalances(XRP(10'000), USD(10'000), amm.tokens()));
4413 BEAST_EXPECT(amm.expectTradingFee(1'000));
4414 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
4415
4416 // Withdrawing all tokens deletes AMM since the number
4417 // of remaining trustlines is less than max
4418 amm.withdrawAll(alice);
4419 BEAST_EXPECT(!amm.ammExists());
4420 BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
4421 }
4422
4423 {
4424 Env env(
4425 *this,
4427 cfg->FEES.reference_fee = XRPAmount(1);
4428 return cfg;
4429 }),
4430 all);
4431 fund(env, gw, {alice}, XRP(20'000), {USD(10'000)});
4432 AMM amm(env, gw, XRP(10'000), USD(10'000));
4433 for (auto i = 0; i < maxDeletableAMMTrustLines * 2 + 10; ++i)
4434 {
4435 Account const a{std::to_string(i)};
4436 env.fund(XRP(1'000), a);
4437 env(trust(a, STAmount{amm.lptIssue(), 10'000}));
4438 env.close();
4439 }
4440 // The trustlines are partially deleted.
4441 amm.withdrawAll(gw);
4442 BEAST_EXPECT(amm.ammExists());
4443
4444 // AMMDelete has to be called twice to delete AMM.
4445 amm.ammDelete(alice, ter(tecINCOMPLETE));
4446 BEAST_EXPECT(amm.ammExists());
4447 // Deletes remaining trustlines and deletes AMM.
4448 amm.ammDelete(alice);
4449 BEAST_EXPECT(!amm.ammExists());
4450 BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
4451
4452 // Try redundant delete
4453 amm.ammDelete(alice, ter(terNO_AMM));
4454 }
4455 }
4456
4457 void
4459 {
4460 testcase("Clawback");
4461 using namespace jtx;
4462 Env env(*this);
4463 env.fund(XRP(2'000), gw);
4464 env.fund(XRP(2'000), alice);
4465 AMM amm(env, gw, XRP(1'000), USD(1'000));
4467 }
4468
4469 void
4471 {
4472 testcase("AMMID");
4473 using namespace jtx;
4474 testAMM([&](AMM& amm, Env& env) {
4475 amm.setClose(false);
4476 auto const info =
4477 env.rpc("json", "account_info", std::string("{\"account\": \"" + to_string(amm.ammAccount()) + "\"}"));
4478 try
4479 {
4480 BEAST_EXPECT(info[jss::result][jss::account_data][jss::AMMID].asString() == to_string(amm.ammID()));
4481 }
4482 catch (...)
4483 {
4484 fail();
4485 }
4486 amm.deposit(carol, 1'000);
4487 auto affected = env.meta()->getJson(JsonOptions::none)[sfAffectedNodes.fieldName];
4488 try
4489 {
4490 bool found = false;
4491 for (auto const& node : affected)
4492 {
4493 if (node.isMember(sfModifiedNode.fieldName) &&
4494 node[sfModifiedNode.fieldName][sfLedgerEntryType.fieldName].asString() == "AccountRoot" &&
4495 node[sfModifiedNode.fieldName][sfFinalFields.fieldName][jss::Account].asString() ==
4496 to_string(amm.ammAccount()))
4497 {
4498 found = node[sfModifiedNode.fieldName][sfFinalFields.fieldName][jss::AMMID].asString() ==
4499 to_string(amm.ammID());
4500 break;
4501 }
4502 }
4503 BEAST_EXPECT(found);
4504 }
4505 catch (...)
4506 {
4507 fail();
4508 }
4509 });
4510 }
4511
4512 void
4514 {
4515 testcase("Offer/Strand Selection");
4516 using namespace jtx;
4517 Account const ed("ed");
4518 Account const gw1("gw1");
4519 auto const ETH = gw1["ETH"];
4520 auto const CAN = gw1["CAN"];
4521
4522 // These tests are expected to fail if the OwnerPaysFee feature
4523 // is ever supported. Updates will need to be made to AMM handling
4524 // in the payment engine, and these tests will need to be updated.
4525
4526 auto prep = [&](Env& env, auto gwRate, auto gw1Rate) {
4527 fund(env, gw, {alice, carol, bob, ed}, XRP(2'000), {USD(2'000)});
4528 env.fund(XRP(2'000), gw1);
4529 fund(env, gw1, {alice, carol, bob, ed}, {ETH(2'000), CAN(2'000)}, Fund::IOUOnly);
4530 env(rate(gw, gwRate));
4531 env(rate(gw1, gw1Rate));
4532 env.close();
4533 };
4534
4535 for (auto const& rates : {std::make_pair(1.5, 1.9), std::make_pair(1.9, 1.5)})
4536 {
4537 // Offer Selection
4538
4539 // Cross-currency payment: AMM has the same spot price quality
4540 // as CLOB's offer and can't generate a better quality offer.
4541 // The transfer fee in this case doesn't change the CLOB quality
4542 // because trIn is ignored on adjustment and trOut on payment is
4543 // also ignored because ownerPaysTransferFee is false in this
4544 // case. Run test for 0) offer, 1) AMM, 2) offer and AMM to
4545 // verify that the quality is better in the first case, and CLOB
4546 // is selected in the second case.
4547 {
4549 for (auto i = 0; i < 3; ++i)
4550 {
4551 Env env(*this, features);
4552 prep(env, rates.first, rates.second);
4554 if (i == 0 || i == 2)
4555 {
4556 env(offer(ed, ETH(400), USD(400)), txflags(tfPassive));
4557 env.close();
4558 }
4559 if (i > 0)
4560 amm.emplace(env, ed, USD(1'000), ETH(1'000));
4561 env(pay(carol, bob, USD(100)), path(~USD), sendmax(ETH(500)));
4562 env.close();
4563 // CLOB and AMM, AMM is not selected
4564 if (i == 2)
4565 {
4566 BEAST_EXPECT(amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens()));
4567 }
4568 BEAST_EXPECT(expectHolding(env, bob, USD(2'100)));
4569 q[i] = Quality(Amounts{ETH(2'000) - env.balance(carol, ETH), env.balance(bob, USD) - USD(2'000)});
4570 }
4571 // CLOB is better quality than AMM
4572 BEAST_EXPECT(q[0] > q[1]);
4573 // AMM is not selected with CLOB
4574 BEAST_EXPECT(q[0] == q[2]);
4575 }
4576 // Offer crossing: AMM has the same spot price quality
4577 // as CLOB's offer and can't generate a better quality offer.
4578 // The transfer fee in this case doesn't change the CLOB quality
4579 // because the quality adjustment is ignored for the offer
4580 // crossing.
4581 for (auto i = 0; i < 3; ++i)
4582 {
4583 Env env(*this, features);
4584 prep(env, rates.first, rates.second);
4586 if (i == 0 || i == 2)
4587 {
4588 env(offer(ed, ETH(400), USD(400)), txflags(tfPassive));
4589 env.close();
4590 }
4591 if (i > 0)
4592 amm.emplace(env, ed, USD(1'000), ETH(1'000));
4593 env(offer(alice, USD(400), ETH(400)));
4594 env.close();
4595 // AMM is not selected
4596 if (i > 0)
4597 {
4598 BEAST_EXPECT(amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens()));
4599 }
4600 if (i == 0 || i == 2)
4601 {
4602 // Fully crosses
4603 BEAST_EXPECT(expectOffers(env, alice, 0));
4604 }
4605 // Fails to cross because AMM is not selected
4606 else
4607 {
4608 BEAST_EXPECT(expectOffers(env, alice, 1, {Amounts{USD(400), ETH(400)}}));
4609 }
4610 BEAST_EXPECT(expectOffers(env, ed, 0));
4611 }
4612
4613 // Show that the CLOB quality reduction
4614 // results in AMM offer selection.
4615
4616 // Same as the payment but reduced offer quality
4617 {
4619 for (auto i = 0; i < 3; ++i)
4620 {
4621 Env env(*this, features);
4622 prep(env, rates.first, rates.second);
4624 if (i == 0 || i == 2)
4625 {
4626 env(offer(ed, ETH(400), USD(300)), txflags(tfPassive));
4627 env.close();
4628 }
4629 if (i > 0)
4630 amm.emplace(env, ed, USD(1'000), ETH(1'000));
4631 env(pay(carol, bob, USD(100)), path(~USD), sendmax(ETH(500)));
4632 env.close();
4633 // AMM and CLOB are selected
4634 if (i > 0)
4635 {
4636 BEAST_EXPECT(!amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens()));
4637 }
4638 if (i == 2 && !features[fixAMMv1_1])
4639 {
4640 if (rates.first == 1.5)
4641 {
4642 if (!features[fixAMMv1_1])
4643 BEAST_EXPECT(expectOffers(
4644 env,
4645 ed,
4646 1,
4647 {{Amounts{
4648 STAmount{ETH, UINT64_C(378'6327949540823), -13},
4649 STAmount{USD, UINT64_C(283'9745962155617), -13}}}}));
4650 else
4651 BEAST_EXPECT(expectOffers(
4652 env,
4653 ed,
4654 1,
4655 {{Amounts{
4656 STAmount{ETH, UINT64_C(378'6327949540813), -13},
4657 STAmount{USD, UINT64_C(283'974596215561), -12}}}}));
4658 }
4659 else
4660 {
4661 if (!features[fixAMMv1_1])
4662 BEAST_EXPECT(expectOffers(
4663 env,
4664 ed,
4665 1,
4666 {{Amounts{
4667 STAmount{ETH, UINT64_C(325'299461620749), -12},
4668 STAmount{USD, UINT64_C(243'9745962155617), -13}}}}));
4669 else
4670 BEAST_EXPECT(expectOffers(
4671 env,
4672 ed,
4673 1,
4674 {{Amounts{
4675 STAmount{ETH, UINT64_C(325'299461620748), -12},
4676 STAmount{USD, UINT64_C(243'974596215561), -12}}}}));
4677 }
4678 }
4679 else if (i == 2)
4680 {
4681 if (rates.first == 1.5)
4682 {
4683 BEAST_EXPECT(expectOffers(
4684 env,
4685 ed,
4686 1,
4687 {{Amounts{
4688 STAmount{ETH, UINT64_C(378'6327949540812), -13},
4689 STAmount{USD, UINT64_C(283'9745962155609), -13}}}}));
4690 }
4691 else
4692 {
4693 BEAST_EXPECT(expectOffers(
4694 env,
4695 ed,
4696 1,
4697 {{Amounts{
4698 STAmount{ETH, UINT64_C(325'2994616207479), -13},
4699 STAmount{USD, UINT64_C(243'9745962155609), -13}}}}));
4700 }
4701 }
4702 BEAST_EXPECT(expectHolding(env, bob, USD(2'100)));
4703 q[i] = Quality(Amounts{ETH(2'000) - env.balance(carol, ETH), env.balance(bob, USD) - USD(2'000)});
4704 }
4705 // AMM is better quality
4706 BEAST_EXPECT(q[1] > q[0]);
4707 // AMM and CLOB produce better quality
4708 BEAST_EXPECT(q[2] > q[1]);
4709 }
4710
4711 // Same as the offer-crossing but reduced offer quality
4712 for (auto i = 0; i < 3; ++i)
4713 {
4714 Env env(*this, features);
4715 prep(env, rates.first, rates.second);
4717 if (i == 0 || i == 2)
4718 {
4719 env(offer(ed, ETH(400), USD(250)), txflags(tfPassive));
4720 env.close();
4721 }
4722 if (i > 0)
4723 amm.emplace(env, ed, USD(1'000), ETH(1'000));
4724 env(offer(alice, USD(250), ETH(400)));
4725 env.close();
4726 // AMM is selected in both cases
4727 if (i > 0)
4728 {
4729 BEAST_EXPECT(!amm->expectBalances(USD(1'000), ETH(1'000), amm->tokens()));
4730 }
4731 // Partially crosses, AMM is selected, CLOB fails
4732 // limitQuality
4733 if (i == 2)
4734 {
4735 if (rates.first == 1.5)
4736 {
4737 if (!features[fixAMMv1_1])
4738 {
4739 BEAST_EXPECT(expectOffers(env, ed, 1, {{Amounts{ETH(400), USD(250)}}}));
4740 BEAST_EXPECT(expectOffers(
4741 env,
4742 alice,
4743 1,
4744 {{Amounts{
4745 STAmount{USD, UINT64_C(40'5694150420947), -13},
4746 STAmount{ETH, UINT64_C(64'91106406735152), -14},
4747 }}}));
4748 }
4749 else
4750 {
4751 // Ed offer is partially crossed.
4752 // The updated rounding makes limitQuality
4753 // work if both amendments are enabled
4754 BEAST_EXPECT(expectOffers(
4755 env,
4756 ed,
4757 1,
4758 {{Amounts{
4759 STAmount{ETH, UINT64_C(335'0889359326475), -13},
4760 STAmount{USD, UINT64_C(209'4305849579047), -13},
4761 }}}));
4762 BEAST_EXPECT(expectOffers(env, alice, 0));
4763 }
4764 }
4765 else
4766 {
4767 if (!features[fixAMMv1_1])
4768 {
4769 // Ed offer is partially crossed.
4770 BEAST_EXPECT(expectOffers(
4771 env,
4772 ed,
4773 1,
4774 {{Amounts{
4775 STAmount{ETH, UINT64_C(335'0889359326485), -13},
4776 STAmount{USD, UINT64_C(209'4305849579053), -13},
4777 }}}));
4778 BEAST_EXPECT(expectOffers(env, alice, 0));
4779 }
4780 else
4781 {
4782 // Ed offer is partially crossed.
4783 BEAST_EXPECT(expectOffers(
4784 env,
4785 ed,
4786 1,
4787 {{Amounts{
4788 STAmount{ETH, UINT64_C(335'0889359326475), -13},
4789 STAmount{USD, UINT64_C(209'4305849579047), -13},
4790 }}}));
4791 BEAST_EXPECT(expectOffers(env, alice, 0));
4792 }
4793 }
4794 }
4795 }
4796
4797 // Strand selection
4798
4799 // Two book steps strand quality is 1.
4800 // AMM strand's best quality is equal to AMM's spot price
4801 // quality, which is 1. Both strands (steps) are adjusted
4802 // for the transfer fee in qualityUpperBound. In case
4803 // of two strands, AMM offers have better quality and are
4804 // consumed first, remaining liquidity is generated by CLOB
4805 // offers. Liquidity from two strands is better in this case
4806 // than in case of one strand with two book steps. Liquidity
4807 // from one strand with AMM has better quality than either one
4808 // strand with two book steps or two strands. It may appear
4809 // unintuitive, but one strand with AMM is optimized and
4810 // generates one AMM offer, while in case of two strands,
4811 // multiple AMM offers are generated, which results in slightly
4812 // worse overall quality.
4813 {
4815 for (auto i = 0; i < 3; ++i)
4816 {
4817 Env env(*this, features);
4818 prep(env, rates.first, rates.second);
4820
4821 if (i == 0 || i == 2)
4822 {
4823 env(offer(ed, ETH(400), CAN(400)), txflags(tfPassive));
4824 env(offer(ed, CAN(400), USD(400))), txflags(tfPassive);
4825 env.close();
4826 }
4827
4828 if (i > 0)
4829 amm.emplace(env, ed, ETH(1'000), USD(1'000));
4830
4831 env(pay(carol, bob, USD(100)), path(~USD), path(~CAN, ~USD), sendmax(ETH(600)));
4832 env.close();
4833
4834 BEAST_EXPECT(expectHolding(env, bob, USD(2'100)));
4835
4836 if (i == 2 && !features[fixAMMv1_1])
4837 {
4838 if (rates.first == 1.5)
4839 {
4840 // Liquidity is consumed from AMM strand only
4841 BEAST_EXPECT(amm->expectBalances(
4842 STAmount{ETH, UINT64_C(1'176'66038955758), -11}, USD(850), amm->tokens()));
4843 }
4844 else
4845 {
4846 BEAST_EXPECT(amm->expectBalances(
4847 STAmount{ETH, UINT64_C(1'179'540094339627), -12},
4848 STAmount{USD, UINT64_C(847'7880529867501), -13},
4849 amm->tokens()));
4850 BEAST_EXPECT(expectOffers(
4851 env,
4852 ed,
4853 2,
4854 {{Amounts{
4855 STAmount{ETH, UINT64_C(343'3179205198749), -13},
4856 STAmount{CAN, UINT64_C(343'3179205198749), -13},
4857 },
4858 Amounts{
4859 STAmount{CAN, UINT64_C(362'2119470132499), -13},
4860 STAmount{USD, UINT64_C(362'2119470132499), -13},
4861 }}}));
4862 }
4863 }
4864 else if (i == 2)
4865 {
4866 if (rates.first == 1.5)
4867 {
4868 // Liquidity is consumed from AMM strand only
4869 BEAST_EXPECT(amm->expectBalances(
4870 STAmount{ETH, UINT64_C(1'176'660389557593), -12}, USD(850), amm->tokens()));
4871 }
4872 else
4873 {
4874 BEAST_EXPECT(amm->expectBalances(
4875 STAmount{ETH, UINT64_C(1'179'54009433964), -11},
4876 STAmount{USD, UINT64_C(847'7880529867501), -13},
4877 amm->tokens()));
4878 BEAST_EXPECT(expectOffers(
4879 env,
4880 ed,
4881 2,
4882 {{Amounts{
4883 STAmount{ETH, UINT64_C(343'3179205198749), -13},
4884 STAmount{CAN, UINT64_C(343'3179205198749), -13},
4885 },
4886 Amounts{
4887 STAmount{CAN, UINT64_C(362'2119470132499), -13},
4888 STAmount{USD, UINT64_C(362'2119470132499), -13},
4889 }}}));
4890 }
4891 }
4892 q[i] = Quality(Amounts{ETH(2'000) - env.balance(carol, ETH), env.balance(bob, USD) - USD(2'000)});
4893 }
4894 BEAST_EXPECT(q[1] > q[0]);
4895 BEAST_EXPECT(q[2] > q[0] && q[2] < q[1]);
4896 }
4897 }
4898 }
4899
4900 void
4902 {
4903 testcase("Fix Default Inner Object");
4904 using namespace jtx;
4906
4907 auto test = [&](FeatureBitset features,
4908 TER const& err1,
4909 TER const& err2,
4910 TER const& err3,
4911 TER const& err4,
4912 std::uint16_t tfee,
4913 bool closeLedger,
4915 Env env(*this, features);
4916 fund(env, gw, {alice}, XRP(1'000), {USD(10)});
4917 AMM amm(env, gw, XRP(10), USD(10), {.tfee = tfee, .close = closeLedger});
4918 amm.deposit(alice, USD(10), XRP(10));
4919 amm.vote(VoteArg{.account = alice, .tfee = tfee, .err = ter(err1)});
4920 amm.withdraw(WithdrawArg{.account = gw, .asset1Out = USD(1), .err = ter(err2)});
4921 // with the amendment disabled and ledger not closed,
4922 // second vote succeeds if the first vote sets the trading fee
4923 // to non-zero; if the first vote sets the trading fee to >0 &&
4924 // <9 then the second withdraw succeeds if the second vote sets
4925 // the trading fee so that the discounted fee is non-zero
4926 amm.vote(VoteArg{.account = alice, .tfee = 20, .err = ter(err3)});
4927 amm.withdraw(WithdrawArg{.account = gw, .asset1Out = USD(2), .err = ter(err4)});
4928 };
4929
4930 // ledger is closed after each transaction, vote/withdraw don't fail
4931 // regardless whether the amendment is enabled or not
4932 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true);
4933 test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, true);
4934 // ledger is not closed after each transaction
4935 // vote/withdraw don't fail if the amendment is enabled
4936 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 0, false);
4937 // vote/withdraw fail if the amendment is not enabled
4938 // second vote/withdraw still fail: second vote fails because
4939 // the initial trading fee is 0, consequently second withdraw fails
4940 // because the second vote fails
4941 test(all - fixInnerObjTemplate, tefEXCEPTION, tefEXCEPTION, tefEXCEPTION, tefEXCEPTION, 0, false);
4942 // if non-zero trading/discounted fee then vote/withdraw
4943 // don't fail whether the ledger is closed or not and
4944 // the amendment is enabled or not
4945 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true);
4946 test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, true);
4947 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false);
4948 test(all - fixInnerObjTemplate, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 10, false);
4949 // non-zero trading fee but discounted fee is 0, vote doesn't fail
4950 // but withdraw fails
4951 test(all, tesSUCCESS, tesSUCCESS, tesSUCCESS, tesSUCCESS, 9, false);
4952 // second vote sets the trading fee to non-zero, consequently
4953 // second withdraw doesn't fail even if the amendment is not
4954 // enabled and the ledger is not closed
4955 test(all - fixInnerObjTemplate, tesSUCCESS, tefEXCEPTION, tesSUCCESS, tesSUCCESS, 9, false);
4956 }
4957
4958 void
4960 {
4961 testcase("Fix changeSpotPriceQuality");
4962 using namespace jtx;
4963
4964 std::string logs;
4965
4966 enum class Status {
4967 SucceedShouldSucceedResize, // Succeed in pre-fix because
4968 // error allowance, succeed post-fix
4969 // because of offer resizing
4970 FailShouldSucceed, // Fail in pre-fix due to rounding,
4971 // succeed after fix because of XRP
4972 // side is generated first
4973 SucceedShouldFail, // Succeed in pre-fix, fail after fix
4974 // due to small quality difference
4975 Fail, // Both fail because the quality can't be matched
4976 Succeed // Both succeed
4977 };
4978 using enum Status;
4979 auto const xrpIouAmounts10_100 = TAmounts{XRPAmount{10}, IOUAmount{100}};
4980 auto const iouXrpAmounts10_100 = TAmounts{IOUAmount{10}, XRPAmount{100}};
4981 // clang-format off
4983 //Pool In , Pool Out, Quality , Fee, Status
4984 {"0.001519763260828713", "1558701", Quality{5414253689393440221}, 1000, FailShouldSucceed},
4985 {"0.01099814367603737", "1892611", Quality{5482264816516900274}, 1000, FailShouldSucceed},
4986 {"0.78", "796599", Quality{5630392334958379008}, 1000, FailShouldSucceed},
4987 {"105439.2955578965", "49398693", Quality{5910869983721805038}, 400, FailShouldSucceed},
4988 {"12408293.23445213", "4340810521", Quality{5911611095910090752}, 997, FailShouldSucceed},
4989 {"1892611", "0.01099814367603737", Quality{6703103457950430139}, 1000, FailShouldSucceed},
4990 {"423028.8508101858", "3392804520", Quality{5837920340654162816}, 600, FailShouldSucceed},
4991 {"44565388.41001027", "73890647", Quality{6058976634606450001}, 1000, FailShouldSucceed},
4992 {"66831.68494832662", "16", Quality{6346111134641742975}, 0, FailShouldSucceed},
4993 {"675.9287302203422", "1242632304", Quality{5625960929244093294}, 300, FailShouldSucceed},
4994 {"7047.112186735699", "1649845866", Quality{5696855348026306945}, 504, FailShouldSucceed},
4995 {"840236.4402981238", "47419053", Quality{5982561601648018688}, 499, FailShouldSucceed},
4996 {"992715.618909774", "189445631733", Quality{5697835648288106944}, 815, SucceedShouldSucceedResize},
4997 {"504636667521", "185545883.9506651", Quality{6343802275337659280}, 503, SucceedShouldSucceedResize},
4998 {"992706.7218636649", "189447316000", Quality{5697835648288106944}, 797, SucceedShouldSucceedResize},
4999 {"1.068737911388205", "127860278877", Quality{5268604356368739396}, 293, SucceedShouldSucceedResize},
5000 {"17932506.56880419", "189308.6043676173", Quality{6206460598195440068}, 311, SucceedShouldSucceedResize},
5001 {"1.066379294658174", "128042251493", Quality{5268559341368739328}, 270, SucceedShouldSucceedResize},
5002 {"350131413924", "1576879.110907892", Quality{6487411636539049449}, 650, Fail},
5003 {"422093460", "2.731797662057464", Quality{6702911108534394924}, 1000, Fail},
5004 {"76128132223", "367172.7148422662", Quality{6487263463413514240}, 548, Fail},
5005 {"132701839250", "280703770.7695443", Quality{6273750681188885075}, 562, Fail},
5006 {"994165.7604612011", "189551302411", Quality{5697835592690668727}, 815, Fail},
5007 {"45053.33303227917", "86612695359", Quality{5625695218943638190}, 500, Fail},
5008 {"199649.077043865", "14017933007", Quality{5766034667318524880}, 324, Fail},
5009 {"27751824831.70903", "78896950", Quality{6272538159621630432}, 500, Fail},
5010 {"225.3731275781907", "156431793648", Quality{5477818047604078924}, 989, Fail},
5011 {"199649.077043865", "14017933007", Quality{5766036094462806309}, 324, Fail},
5012 {"3.590272027140361", "20677643641", Quality{5406056147042156356}, 808, Fail},
5013 {"1.070884664490231", "127604712776", Quality{5268620608623825741}, 293, Fail},
5014 {"3272.448829820197", "6275124076", Quality{5625710328924117902}, 81, Fail},
5015 {"0.009059512633902926", "7994028", Quality{5477511954775533172}, 1000, Fail},
5016 {"1", "1.0", Quality{0}, 100, Fail},
5017 {"1.0", "1", Quality{0}, 100, Fail},
5018 {"10", "10.0", Quality{xrpIouAmounts10_100}, 100, Fail},
5019 {"10.0", "10", Quality{iouXrpAmounts10_100}, 100, Fail},
5020 {"69864389131", "287631.4543025075", Quality{6487623473313516078}, 451, Succeed},
5021 {"4328342973", "12453825.99247381", Quality{6272522264364865181}, 997, Succeed},
5022 {"32347017", "7003.93031579449", Quality{6347261126087916670}, 1000, Succeed},
5023 {"61697206161", "36631.4583206413", Quality{6558965195382476659}, 500, Succeed},
5024 {"1654524979", "7028.659825511603", Quality{6487551345110052981}, 504, Succeed},
5025 {"88621.22277293179", "5128418948", Quality{5766347291552869205}, 380, Succeed},
5026 {"1892611", "0.01099814367603737", Quality{6703102780512015436}, 1000, Succeed},
5027 {"4542.639373338766", "24554809", Quality{5838994982188783710}, 0, Succeed},
5028 {"5132932546", "88542.99750172683", Quality{6419203342950054537}, 380, Succeed},
5029 {"78929964.1549083", "1506494795", Quality{5986890029845558688}, 589, Succeed},
5030 {"10096561906", "44727.72453735605", Quality{6487455290284644551}, 250, Succeed},
5031 {"5092.219565514988", "8768257694", Quality{5626349534958379008}, 503, Succeed},
5032 {"1819778294", "8305.084302902864", Quality{6487429398998540860}, 415, Succeed},
5033 {"6970462.633911943", "57359281", Quality{6054087899185946624}, 850, Succeed},
5034 {"3983448845", "2347.543644281467", Quality{6558965195382476659}, 856, Succeed},
5035 // This is a tiny offer 12drops/19321952e-15 it succeeds pre-amendment because of the error allowance.
5036 // Post amendment it is resized to 11drops/17711789e-15 but the quality is still less than
5037 // the target quality and the offer fails.
5038 {"771493171", "1.243473020567508", Quality{6707566798038544272}, 100, SucceedShouldFail},
5039 };
5040 // clang-format on
5041
5042 boost::regex rx("^\\d+$");
5043 boost::smatch match;
5044 // tests that succeed should have the same amounts pre-fix and post-fix
5046 Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
5047 auto rules = env.current()->rules();
5049 NumberMantissaScaleGuard sg(MantissaRange::small);
5050
5051 for (auto const& t : tests)
5052 {
5053 auto getPool = [&](std::string const& v, bool isXRP) {
5054 if (isXRP)
5055 return amountFromString(xrpIssue(), v);
5056 return amountFromString(noIssue(), v);
5057 };
5058 auto const& quality = std::get<Quality>(t);
5059 auto const tfee = std::get<std::uint16_t>(t);
5060 auto const status = std::get<Status>(t);
5061 auto const poolInIsXRP = boost::regex_search(std::get<0>(t), match, rx);
5062 auto const poolOutIsXRP = boost::regex_search(std::get<1>(t), match, rx);
5063 assert(!(poolInIsXRP && poolOutIsXRP));
5064 auto const poolIn = getPool(std::get<0>(t), poolInIsXRP);
5065 auto const poolOut = getPool(std::get<1>(t), poolOutIsXRP);
5066 try
5067 {
5068 auto const amounts = changeSpotPriceQuality(
5069 Amounts{poolIn, poolOut}, quality, tfee, env.current()->rules(), env.journal);
5070 if (amounts)
5071 {
5072 if (status == SucceedShouldSucceedResize)
5073 {
5074 if (!features[fixAMMv1_1])
5075 BEAST_EXPECT(Quality{*amounts} < quality);
5076 else
5077 BEAST_EXPECT(Quality{*amounts} >= quality);
5078 }
5079 else if (status == Succeed)
5080 {
5081 if (!features[fixAMMv1_1])
5082 BEAST_EXPECT(
5083 Quality{*amounts} >= quality ||
5084 withinRelativeDistance(Quality{*amounts}, quality, Number{1, -7}));
5085 else
5086 BEAST_EXPECT(Quality{*amounts} >= quality);
5087 }
5088 else if (status == FailShouldSucceed)
5089 {
5090 BEAST_EXPECT(features[fixAMMv1_1] && Quality{*amounts} >= quality);
5091 }
5092 else if (status == SucceedShouldFail)
5093 {
5094 BEAST_EXPECT(
5095 !features[fixAMMv1_1] && Quality{*amounts} < quality &&
5096 withinRelativeDistance(Quality{*amounts}, quality, Number{1, -7}));
5097 }
5098 }
5099 else
5100 {
5101 // Fails pre- and post-amendment because the quality can't
5102 // be matched. Verify by generating a tiny offer, which
5103 // doesn't match the quality. Exclude zero quality since
5104 // no offer is generated in this case.
5105 if (status == Fail && quality != Quality{0})
5106 {
5107 auto tinyOffer = [&]() {
5108 if (isXRP(poolIn))
5109 {
5110 auto const takerPays = STAmount{xrpIssue(), 1};
5111 return Amounts{takerPays, swapAssetIn(Amounts{poolIn, poolOut}, takerPays, tfee)};
5112 }
5113 else if (isXRP(poolOut))
5114 {
5115 auto const takerGets = STAmount{xrpIssue(), 1};
5116 return Amounts{swapAssetOut(Amounts{poolIn, poolOut}, takerGets, tfee), takerGets};
5117 }
5118 auto const takerPays = toAmount<STAmount>(getIssue(poolIn), Number{1, -10} * poolIn);
5119 return Amounts{takerPays, swapAssetIn(Amounts{poolIn, poolOut}, takerPays, tfee)};
5120 }();
5121 BEAST_EXPECT(Quality(tinyOffer) < quality);
5122 }
5123 else if (status == FailShouldSucceed)
5124 {
5125 BEAST_EXPECT(!features[fixAMMv1_1]);
5126 }
5127 else if (status == SucceedShouldFail)
5128 {
5129 BEAST_EXPECT(features[fixAMMv1_1]);
5130 }
5131 }
5132 }
5133 catch (std::runtime_error const& e)
5134 {
5135 BEAST_EXPECT(!strcmp(e.what(), "changeSpotPriceQuality failed"));
5136 BEAST_EXPECT(!features[fixAMMv1_1] && status == FailShouldSucceed);
5137 }
5138 }
5139
5140 // Test negative discriminant
5141 {
5142 // b**2 - 4 * a * c -> 1 * 1 - 4 * 1 * 1 = -3
5143 auto const res = solveQuadraticEqSmallest(Number{1}, Number{1}, Number{1});
5144 BEAST_EXPECT(!res.has_value());
5145 }
5146 }
5147
5148 void
5150 {
5151 using namespace jtx;
5152
5153 testAMM([&](AMM& ammAlice, Env& env) {
5154 WithdrawArg args{
5156 .err = ter(temMALFORMED),
5157 };
5158 ammAlice.withdraw(args);
5159 });
5160
5161 testAMM([&](AMM& ammAlice, Env& env) {
5162 WithdrawArg args{
5164 .err = ter(temMALFORMED),
5165 };
5166 ammAlice.withdraw(args);
5167 });
5168
5169 testAMM([&](AMM& ammAlice, Env& env) {
5170 WithdrawArg args{
5172 .err = ter(temMALFORMED),
5173 };
5174 ammAlice.withdraw(args);
5175 });
5176
5177 testAMM([&](AMM& ammAlice, Env& env) {
5178 WithdrawArg args{
5179 .asset1Out = XRP(100),
5180 .asset2Out = XRP(100),
5181 .err = ter(temBAD_AMM_TOKENS),
5182 };
5183 ammAlice.withdraw(args);
5184 });
5185
5186 testAMM([&](AMM& ammAlice, Env& env) {
5187 WithdrawArg args{
5188 .asset1Out = XRP(100),
5189 .asset2Out = BAD(100),
5190 .err = ter(temBAD_CURRENCY),
5191 };
5192 ammAlice.withdraw(args);
5193 });
5194
5195 testAMM([&](AMM& ammAlice, Env& env) {
5196 Json::Value jv;
5197 jv[jss::TransactionType] = jss::AMMWithdraw;
5198 jv[jss::Flags] = tfLimitLPToken;
5199 jv[jss::Account] = alice.human();
5200 ammAlice.setTokens(jv);
5201 XRP(100).value().setJson(jv[jss::Amount]);
5202 USD(100).value().setJson(jv[jss::EPrice]);
5203 env(jv, ter(temBAD_AMM_TOKENS));
5204 });
5205 }
5206
5207 void
5209 {
5210 using namespace jtx;
5211 using namespace std::chrono;
5212 FeatureBitset const all{featuresInitial};
5213
5214 std::string logs;
5215
5216 Account const gatehub{"gatehub"};
5217 Account const bitstamp{"bitstamp"};
5218 Account const trader{"trader"};
5219 auto const usdGH = gatehub["USD"];
5220 auto const btcGH = gatehub["BTC"];
5221 auto const usdBIT = bitstamp["USD"];
5222
5223 struct InputSet
5224 {
5225 char const* testCase;
5226 double const poolUsdBIT;
5227 double const poolUsdGH;
5228 sendmax const sendMaxUsdBIT;
5229 STAmount const sendUsdGH;
5230 STAmount const failUsdGH;
5231 STAmount const failUsdGHr;
5232 STAmount const failUsdBIT;
5233 STAmount const failUsdBITr;
5234 STAmount const goodUsdGH;
5235 STAmount const goodUsdGHr;
5236 STAmount const goodUsdBIT;
5237 STAmount const goodUsdBITr;
5238 IOUAmount const lpTokenBalance;
5239 std::optional<IOUAmount> const lpTokenBalanceAlt = {};
5240 double const offer1BtcGH = 0.1;
5241 double const offer2BtcGH = 0.1;
5242 double const offer2UsdGH = 1;
5243 double const rateBIT = 0.0;
5244 double const rateGH = 0.0;
5245 };
5246
5247 using uint64_t = std::uint64_t;
5248
5249 for (auto const& input : {
5250 InputSet{
5251 .testCase = "Test Fix Overflow Offer", //
5252 .poolUsdBIT = 3, //
5253 .poolUsdGH = 273, //
5254 .sendMaxUsdBIT{usdBIT(50)}, //
5255 .sendUsdGH{usdGH, uint64_t(272'455089820359), -12}, //
5256 .failUsdGH = STAmount{0}, //
5257 .failUsdGHr = STAmount{0}, //
5258 .failUsdBIT{usdBIT, uint64_t(46'47826086956522), -14}, //
5259 .failUsdBITr{usdBIT, uint64_t(46'47826086956521), -14}, //
5260 .goodUsdGH{usdGH, uint64_t(96'7543114220382), -13}, //
5261 .goodUsdGHr{usdGH, uint64_t(96'7543114222965), -13}, //
5262 .goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15}, //
5263 .goodUsdBITr{usdBIT, uint64_t(8'464739069098152), -15}, //
5264 .lpTokenBalance = {28'61817604250837, -14}, //
5265 .lpTokenBalanceAlt = IOUAmount{28'61817604250836, -14}, //
5266 .offer1BtcGH = 0.1, //
5267 .offer2BtcGH = 0.1, //
5268 .offer2UsdGH = 1, //
5269 .rateBIT = 1.15, //
5270 .rateGH = 1.2, //
5271 },
5272 InputSet{
5273 .testCase = "Overflow test {1, 100, 0.111}", //
5274 .poolUsdBIT = 1, //
5275 .poolUsdGH = 100, //
5276 .sendMaxUsdBIT{usdBIT(0.111)}, //
5277 .sendUsdGH{usdGH, 100}, //
5278 .failUsdGH = STAmount{0}, //
5279 .failUsdGHr = STAmount{0}, //
5280 .failUsdBIT{usdBIT, uint64_t(1'111), -3}, //
5281 .failUsdBITr{usdBIT, uint64_t(1'111), -3}, //
5282 .goodUsdGH{usdGH, uint64_t(90'04347888284115), -14}, //
5283 .goodUsdGHr{usdGH, uint64_t(90'04347888284201), -14}, //
5284 .goodUsdBIT{usdBIT, uint64_t(1'111), -3}, //
5285 .goodUsdBITr{usdBIT, uint64_t(1'111), -3}, //
5286 .lpTokenBalance{10, 0}, //
5287 .offer1BtcGH = 1e-5, //
5288 .offer2BtcGH = 1, //
5289 .offer2UsdGH = 1e-5, //
5290 .rateBIT = 0, //
5291 .rateGH = 0, //
5292 },
5293 InputSet{
5294 .testCase = "Overflow test {1, 100, 1.00}", //
5295 .poolUsdBIT = 1, //
5296 .poolUsdGH = 100, //
5297 .sendMaxUsdBIT{usdBIT(1.00)}, //
5298 .sendUsdGH{usdGH, 100}, //
5299 .failUsdGH = STAmount{0}, //
5300 .failUsdGHr = STAmount{0}, //
5301 .failUsdBIT{usdBIT, uint64_t(2), 0}, //
5302 .failUsdBITr{usdBIT, uint64_t(2), 0}, //
5303 .goodUsdGH{usdGH, uint64_t(52'94379354424079), -14}, //
5304 .goodUsdGHr{usdGH, uint64_t(52'94379354424135), -14}, //
5305 .goodUsdBIT{usdBIT, uint64_t(2), 0}, //
5306 .goodUsdBITr{usdBIT, uint64_t(2), 0}, //
5307 .lpTokenBalance{10, 0}, //
5308 .offer1BtcGH = 1e-5, //
5309 .offer2BtcGH = 1, //
5310 .offer2UsdGH = 1e-5, //
5311 .rateBIT = 0, //
5312 .rateGH = 0, //
5313 },
5314 InputSet{
5315 .testCase = "Overflow test {1, 100, 4.6432}", //
5316 .poolUsdBIT = 1, //
5317 .poolUsdGH = 100, //
5318 .sendMaxUsdBIT{usdBIT(4.6432)}, //
5319 .sendUsdGH{usdGH, 100}, //
5320 .failUsdGH = STAmount{0}, //
5321 .failUsdGHr = STAmount{0}, //
5322 .failUsdBIT{usdBIT, uint64_t(5'6432), -4}, //
5323 .failUsdBITr{usdBIT, uint64_t(5'6432), -4}, //
5324 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
5325 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
5326 .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15}, //
5327 .goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15}, //
5328 .lpTokenBalance{10, 0}, //
5329 .offer1BtcGH = 1e-5, //
5330 .offer2BtcGH = 1, //
5331 .offer2UsdGH = 1e-5, //
5332 .rateBIT = 0, //
5333 .rateGH = 0, //
5334 },
5335 InputSet{
5336 .testCase = "Overflow test {1, 100, 10}", //
5337 .poolUsdBIT = 1, //
5338 .poolUsdGH = 100, //
5339 .sendMaxUsdBIT{usdBIT(10)}, //
5340 .sendUsdGH{usdGH, 100}, //
5341 .failUsdGH = STAmount{0}, //
5342 .failUsdGHr = STAmount{0}, //
5343 .failUsdBIT{usdBIT, uint64_t(11), 0}, //
5344 .failUsdBITr{usdBIT, uint64_t(11), 0}, //
5345 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
5346 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
5347 .goodUsdBIT{usdBIT, uint64_t(2'821579689703915), -15}, //
5348 .goodUsdBITr{usdBIT, uint64_t(2'821579689703954), -15}, //
5349 .lpTokenBalance{10, 0}, //
5350 .offer1BtcGH = 1e-5, //
5351 .offer2BtcGH = 1, //
5352 .offer2UsdGH = 1e-5, //
5353 .rateBIT = 0, //
5354 .rateGH = 0, //
5355 },
5356 InputSet{
5357 .testCase = "Overflow test {50, 100, 5.55}", //
5358 .poolUsdBIT = 50, //
5359 .poolUsdGH = 100, //
5360 .sendMaxUsdBIT{usdBIT(5.55)}, //
5361 .sendUsdGH{usdGH, 100}, //
5362 .failUsdGH = STAmount{0}, //
5363 .failUsdGHr = STAmount{0}, //
5364 .failUsdBIT{usdBIT, uint64_t(55'55), -2}, //
5365 .failUsdBITr{usdBIT, uint64_t(55'55), -2}, //
5366 .goodUsdGH{usdGH, uint64_t(90'04347888284113), -14}, //
5367 .goodUsdGHr{usdGH, uint64_t(90'0434788828413), -13}, //
5368 .goodUsdBIT{usdBIT, uint64_t(55'55), -2}, //
5369 .goodUsdBITr{usdBIT, uint64_t(55'55), -2}, //
5370 .lpTokenBalance{uint64_t(70'71067811865475), -14}, //
5371 .offer1BtcGH = 1e-5, //
5372 .offer2BtcGH = 1, //
5373 .offer2UsdGH = 1e-5, //
5374 .rateBIT = 0, //
5375 .rateGH = 0, //
5376 },
5377 InputSet{
5378 .testCase = "Overflow test {50, 100, 50.00}", //
5379 .poolUsdBIT = 50, //
5380 .poolUsdGH = 100, //
5381 .sendMaxUsdBIT{usdBIT(50.00)}, //
5382 .sendUsdGH{usdGH, 100}, //
5383 .failUsdGH{usdGH, uint64_t(52'94379354424081), -14}, //
5384 .failUsdGHr{usdGH, uint64_t(52'94379354424092), -14}, //
5385 .failUsdBIT{usdBIT, uint64_t(100), 0}, //
5386 .failUsdBITr{usdBIT, uint64_t(100), 0}, //
5387 .goodUsdGH{usdGH, uint64_t(52'94379354424081), -14}, //
5388 .goodUsdGHr{usdGH, uint64_t(52'94379354424092), -14}, //
5389 .goodUsdBIT{usdBIT, uint64_t(100), 0}, //
5390 .goodUsdBITr{usdBIT, uint64_t(100), 0}, //
5391 .lpTokenBalance{uint64_t(70'71067811865475), -14}, //
5392 .offer1BtcGH = 1e-5, //
5393 .offer2BtcGH = 1, //
5394 .offer2UsdGH = 1e-5, //
5395 .rateBIT = 0, //
5396 .rateGH = 0, //
5397 },
5398 InputSet{
5399 .testCase = "Overflow test {50, 100, 232.16}", //
5400 .poolUsdBIT = 50, //
5401 .poolUsdGH = 100, //
5402 .sendMaxUsdBIT{usdBIT(232.16)}, //
5403 .sendUsdGH{usdGH, 100}, //
5404 .failUsdGH = STAmount{0}, //
5405 .failUsdGHr = STAmount{0}, //
5406 .failUsdBIT{usdBIT, uint64_t(282'16), -2}, //
5407 .failUsdBITr{usdBIT, uint64_t(282'16), -2}, //
5408 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
5409 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
5410 .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13}, //
5411 .goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13}, //
5412 .lpTokenBalance{70'71067811865475, -14}, //
5413 .offer1BtcGH = 1e-5, //
5414 .offer2BtcGH = 1, //
5415 .offer2UsdGH = 1e-5, //
5416 .rateBIT = 0, //
5417 .rateGH = 0, //
5418 },
5419 InputSet{
5420 .testCase = "Overflow test {50, 100, 500}", //
5421 .poolUsdBIT = 50, //
5422 .poolUsdGH = 100, //
5423 .sendMaxUsdBIT{usdBIT(500)}, //
5424 .sendUsdGH{usdGH, 100}, //
5425 .failUsdGH = STAmount{0}, //
5426 .failUsdGHr = STAmount{0}, //
5427 .failUsdBIT{usdBIT, uint64_t(550), 0}, //
5428 .failUsdBITr{usdBIT, uint64_t(550), 0}, //
5429 .goodUsdGH{usdGH, uint64_t(35'44113971506987), -14}, //
5430 .goodUsdGHr{usdGH, uint64_t(35'44113971506987), -14}, //
5431 .goodUsdBIT{usdBIT, uint64_t(141'0789844851958), -13}, //
5432 .goodUsdBITr{usdBIT, uint64_t(141'0789844851962), -13}, //
5433 .lpTokenBalance{70'71067811865475, -14}, //
5434 .offer1BtcGH = 1e-5, //
5435 .offer2BtcGH = 1, //
5436 .offer2UsdGH = 1e-5, //
5437 .rateBIT = 0, //
5438 .rateGH = 0, //
5439 },
5440 })
5441 {
5442 testcase(input.testCase);
5443 for (auto const& features : {all - fixAMMOverflowOffer - fixAMMv1_1 - fixAMMv1_3, all})
5444 {
5445 Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
5446
5447 env.fund(XRP(5'000), gatehub, bitstamp, trader);
5448 env.close();
5449
5450 if (input.rateGH != 0.0)
5451 env(rate(gatehub, input.rateGH));
5452 if (input.rateBIT != 0.0)
5453 env(rate(bitstamp, input.rateBIT));
5454
5455 env(trust(trader, usdGH(10'000'000)));
5456 env(trust(trader, usdBIT(10'000'000)));
5457 env(trust(trader, btcGH(10'000'000)));
5458 env.close();
5459
5460 env(pay(gatehub, trader, usdGH(100'000)));
5461 env(pay(gatehub, trader, btcGH(100'000)));
5462 env(pay(bitstamp, trader, usdBIT(100'000)));
5463 env.close();
5464
5465 AMM amm{env, trader, usdGH(input.poolUsdGH), usdBIT(input.poolUsdBIT)};
5466 env.close();
5467
5468 IOUAmount const preSwapLPTokenBalance = amm.getLPTokensBalance();
5469
5470 env(offer(trader, usdBIT(1), btcGH(input.offer1BtcGH)));
5471 env(offer(trader, btcGH(input.offer2BtcGH), usdGH(input.offer2UsdGH)));
5472 env.close();
5473
5474 env(pay(trader, trader, input.sendUsdGH),
5475 path(~usdGH),
5476 path(~btcGH, ~usdGH),
5477 sendmax(input.sendMaxUsdBIT),
5479 env.close();
5480
5481 auto const failUsdGH = features[fixAMMv1_1] ? input.failUsdGHr : input.failUsdGH;
5482 auto const failUsdBIT = features[fixAMMv1_1] ? input.failUsdBITr : input.failUsdBIT;
5483 auto const goodUsdGH = features[fixAMMv1_1] ? input.goodUsdGHr : input.goodUsdGH;
5484 auto const goodUsdBIT = features[fixAMMv1_1] ? input.goodUsdBITr : input.goodUsdBIT;
5485 auto const lpTokenBalance = env.enabled(fixAMMv1_3) && input.lpTokenBalanceAlt
5486 ? *input.lpTokenBalanceAlt
5487 : input.lpTokenBalance;
5488 if (!features[fixAMMOverflowOffer])
5489 {
5490 BEAST_EXPECT(amm.expectBalances(failUsdGH, failUsdBIT, lpTokenBalance));
5491 }
5492 else
5493 {
5494 BEAST_EXPECT(amm.expectBalances(goodUsdGH, goodUsdBIT, lpTokenBalance));
5495
5496 // Invariant: LPToken balance must not change in a
5497 // payment or a swap transaction
5498 BEAST_EXPECT(amm.getLPTokensBalance() == preSwapLPTokenBalance);
5499
5500 // Invariant: The square root of (product of the pool
5501 // balances) must be at least the LPTokenBalance
5502 Number const sqrtPoolProduct = root2(goodUsdGH * goodUsdBIT);
5503
5504 // Include a tiny tolerance for the test cases using
5505 // .goodUsdGH{usdGH, uint64_t(35'44113971506987),
5506 // -14}, .goodUsdBIT{usdBIT,
5507 // uint64_t(2'821579689703915), -15},
5508 // These two values multiply
5509 // to 99.99999999999994227040383754105 which gets
5510 // internally rounded to 100, due to representation
5511 // error.
5512 BEAST_EXPECT((sqrtPoolProduct + Number{1, -14} >= input.lpTokenBalance));
5513 }
5514 }
5515 }
5516 }
5517
5518 void
5520 {
5521 testcase("swapRounding");
5522 using namespace jtx;
5523
5524 STAmount const xrpPool{XRP, UINT64_C(51600'000981)};
5525 STAmount const iouPool{USD, UINT64_C(803040'9987141784), -10};
5526
5527 STAmount const xrpBob{XRP, UINT64_C(1092'878933)};
5528 STAmount const iouBob{USD, UINT64_C(3'988035892323031), -28}; // 3.9...e-13
5529
5530 testAMM(
5531 [&](AMM& amm, Env& env) {
5532 // Check our AMM starting conditions.
5533 auto [xrpBegin, iouBegin, lptBegin] = amm.balances(XRP, USD);
5534
5535 // Set Bob's starting conditions.
5536 env.fund(xrpBob, bob);
5537 env.trust(USD(1'000'000), bob);
5538 env(pay(gw, bob, iouBob));
5539 env.close();
5540
5541 env(offer(bob, XRP(6300), USD(100'000)));
5542 env.close();
5543
5544 // Assert that AMM is unchanged.
5545 BEAST_EXPECT(amm.expectBalances(xrpBegin, iouBegin, amm.tokens()));
5546 },
5547 {{xrpPool, iouPool}},
5548 889,
5550 {testable_amendments() | fixAMMv1_1});
5551 }
5552
5553 void
5555 {
5556 testcase("AMM Offer Blocked By LOB");
5557 using namespace jtx;
5558
5559 // Low quality LOB offer blocks AMM liquidity
5560
5561 // USD/XRP crosses AMM
5562 {
5563 Env env(*this, features);
5564
5565 fund(env, gw, {alice, carol}, XRP(1'000'000), {USD(1'000'000)});
5566 // This offer blocks AMM offer in pre-amendment
5567 env(offer(alice, XRP(1), USD(0.01)));
5568 env.close();
5569
5570 AMM amm(env, gw, XRP(200'000), USD(100'000));
5571
5572 // The offer doesn't cross AMM in pre-amendment code
5573 // It crosses AMM in post-amendment code
5574 env(offer(carol, USD(0.49), XRP(1)));
5575 env.close();
5576
5577 if (!features[fixAMMv1_1])
5578 {
5579 BEAST_EXPECT(amm.expectBalances(XRP(200'000), USD(100'000), amm.tokens()));
5580 BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{XRP(1), USD(0.01)}}}));
5581 // Carol's offer is blocked by alice's offer
5582 BEAST_EXPECT(expectOffers(env, carol, 1, {{Amounts{USD(0.49), XRP(1)}}}));
5583 }
5584 else
5585 {
5586 BEAST_EXPECT(amm.expectBalances(XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens()));
5587 BEAST_EXPECT(expectOffers(env, alice, 1, {{Amounts{XRP(1), USD(0.01)}}}));
5588 // Carol's offer crosses AMM
5589 BEAST_EXPECT(expectOffers(env, carol, 0));
5590 }
5591 }
5592
5593 // There is no blocking offer, the same AMM liquidity is consumed
5594 // pre- and post-amendment.
5595 {
5596 Env env(*this, features);
5597
5598 fund(env, gw, {alice, carol}, XRP(1'000'000), {USD(1'000'000)});
5599 // There is no blocking offer
5600 // env(offer(alice, XRP(1), USD(0.01)));
5601
5602 AMM amm(env, gw, XRP(200'000), USD(100'000));
5603
5604 // The offer crosses AMM
5605 env(offer(carol, USD(0.49), XRP(1)));
5606 env.close();
5607
5608 // The same result as with the blocking offer
5609 BEAST_EXPECT(amm.expectBalances(XRPAmount(200'000'980'005), USD(99'999.51), amm.tokens()));
5610 // Carol's offer crosses AMM
5611 BEAST_EXPECT(expectOffers(env, carol, 0));
5612 }
5613
5614 // XRP/USD crosses AMM
5615 {
5616 Env env(*this, features);
5617 fund(env, gw, {alice, carol, bob}, XRP(10'000), {USD(1'000)});
5618
5619 // This offer blocks AMM offer in pre-amendment
5620 // It crosses AMM in post-amendment code
5621 env(offer(bob, USD(1), XRPAmount(500)));
5622 env.close();
5623 AMM amm(env, alice, XRP(1'000), USD(500));
5624 env(offer(carol, XRP(100), USD(55)));
5625 env.close();
5626 if (!features[fixAMMv1_1])
5627 {
5628 BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
5629 BEAST_EXPECT(expectOffers(env, bob, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
5630 BEAST_EXPECT(expectOffers(env, carol, 1, {{Amounts{XRP(100), USD(55)}}}));
5631 }
5632 else
5633 {
5634 BEAST_EXPECT(amm.expectBalances(
5635 XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens()));
5636 BEAST_EXPECT(
5637 expectOffers(env, carol, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
5638 BEAST_EXPECT(expectOffers(env, bob, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
5639 }
5640 }
5641
5642 // There is no blocking offer, the same AMM liquidity is consumed
5643 // pre- and post-amendment.
5644 {
5645 Env env(*this, features);
5646 fund(env, gw, {alice, carol, bob}, XRP(10'000), {USD(1'000)});
5647
5648 AMM amm(env, alice, XRP(1'000), USD(500));
5649 env(offer(carol, XRP(100), USD(55)));
5650 env.close();
5651 BEAST_EXPECT(
5652 amm.expectBalances(XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens()));
5653 BEAST_EXPECT(expectOffers(env, carol, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
5654 }
5655 }
5656
5657 void
5659 {
5660 testcase("LPToken Balance");
5661 using namespace jtx;
5662
5663 // Last Liquidity Provider is the issuer of one token
5664 {
5665 std::string logs;
5666 Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
5667 fund(env, gw, {alice, carol}, XRP(1'000'000'000), {USD(1'000'000'000)});
5668 AMM amm(env, gw, XRP(2), USD(1));
5669 amm.deposit(alice, IOUAmount{1'876123487565916, -15});
5670 amm.deposit(carol, IOUAmount{1'000'000});
5671 amm.withdrawAll(alice);
5672 BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{0}));
5673 amm.withdrawAll(carol);
5674 BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount{0}));
5675 auto const lpToken = getAccountLines(env, gw, amm.lptIssue())[jss::lines][0u][jss::balance];
5676 auto const lpTokenBalance = amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value];
5677 BEAST_EXPECT(lpToken == "1414.213562373095" && lpTokenBalance == "1414.213562373");
5678 if (!features[fixAMMv1_1])
5679 {
5680 amm.withdrawAll(gw, std::nullopt, ter(tecAMM_BALANCE));
5681 BEAST_EXPECT(amm.ammExists());
5682 }
5683 else
5684 {
5685 amm.withdrawAll(gw);
5686 BEAST_EXPECT(!amm.ammExists());
5687 }
5688 }
5689
5690 // Last Liquidity Provider is the issuer of two tokens, or not
5691 // the issuer
5692 for (auto const& lp : {gw, bob})
5693 {
5694 Env env(*this, features);
5695 auto const ABC = gw["ABC"];
5696 fund(env, gw, {alice, carol, bob}, XRP(1'000), {USD(1'000'000'000), ABC(1'000'000'000'000)});
5697 AMM amm(env, lp, ABC(2'000'000), USD(1));
5698 amm.deposit(alice, IOUAmount{1'876123487565916, -15});
5699 amm.deposit(carol, IOUAmount{1'000'000});
5700 amm.withdrawAll(alice);
5701 amm.withdrawAll(carol);
5702 auto const lpToken = getAccountLines(env, lp, amm.lptIssue())[jss::lines][0u][jss::balance];
5703 auto const lpTokenBalance = amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value];
5704 BEAST_EXPECT(lpToken == "1414.213562373095" && lpTokenBalance == "1414.213562373");
5705 if (!features[fixAMMv1_1])
5706 {
5707 amm.withdrawAll(lp, std::nullopt, ter(tecAMM_BALANCE));
5708 BEAST_EXPECT(amm.ammExists());
5709 }
5710 else
5711 {
5712 amm.withdrawAll(lp);
5713 BEAST_EXPECT(!amm.ammExists());
5714 }
5715 }
5716
5717 // More than one Liquidity Provider
5718 // XRP/IOU
5719 {
5720 Env env(*this, features);
5721 fund(env, gw, {alice}, XRP(1'000), {USD(1'000)});
5722 AMM amm(env, gw, XRP(10), USD(10));
5723 amm.deposit(alice, 1'000);
5724 auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
5725 BEAST_EXPECT(res && !res.value());
5726 res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
5727 BEAST_EXPECT(res && !res.value());
5728 }
5729 // IOU/IOU, issuer of both IOU
5730 {
5731 Env env(*this, features);
5732 fund(env, gw, {alice}, XRP(1'000), {USD(1'000), EUR(1'000)});
5733 AMM amm(env, gw, EUR(10), USD(10));
5734 amm.deposit(alice, 1'000);
5735 auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
5736 BEAST_EXPECT(res && !res.value());
5737 res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
5738 BEAST_EXPECT(res && !res.value());
5739 }
5740 // IOU/IOU, issuer of one IOU
5741 {
5742 Env env(*this, features);
5743 Account const gw1("gw1");
5744 auto const YAN = gw1["YAN"];
5745 fund(env, gw, {gw1}, XRP(1'000), {USD(1'000)});
5746 fund(env, gw1, {gw}, XRP(1'000), {YAN(1'000)}, Fund::IOUOnly);
5747 AMM amm(env, gw1, YAN(10), USD(10));
5748 amm.deposit(gw, 1'000);
5749 auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw);
5750 BEAST_EXPECT(res && !res.value());
5751 res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), gw1);
5752 BEAST_EXPECT(res && !res.value());
5753 }
5754 }
5755
5756 void
5758 {
5759 testcase("test clawback from AMM account");
5760 using namespace jtx;
5761
5762 // Issuer has clawback enabled
5763 Env env(*this, features);
5764 env.fund(XRP(1'000), gw);
5766 fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct);
5767 env.close();
5768
5769 // If featureAMMClawback is not enabled, AMMCreate is not allowed for
5770 // clawback-enabled issuer
5771 if (!features[featureAMMClawback])
5772 {
5773 AMM amm(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION));
5774 AMM amm1(env, alice, USD(100), XRP(100), ter(tecNO_PERMISSION));
5776 env.close();
5777 // Can't be cleared
5778 AMM amm2(env, gw, XRP(100), USD(100), ter(tecNO_PERMISSION));
5779 }
5780 // If featureAMMClawback is enabled, AMMCreate is allowed for
5781 // clawback-enabled issuer. Clawback from the AMM Account is not
5782 // allowed, which will return tecAMM_ACCOUNT or tecPSEUDO_ACCOUNT,
5783 // depending on whether SingleAssetVault is enabled. We can only use
5784 // AMMClawback transaction to claw back from AMM Account.
5785 else
5786 {
5787 AMM amm(env, gw, XRP(100), USD(100), ter(tesSUCCESS));
5788 AMM amm1(env, alice, USD(100), XRP(200), ter(tecDUPLICATE));
5789
5790 // Construct the amount being clawed back using AMM account.
5791 // By doing this, we make the clawback transaction's Amount field's
5792 // subfield `issuer` to be the AMM account, which means
5793 // we are clawing back from an AMM account. This should return an
5794 // error because regular Clawback transaction is not
5795 // allowed for clawing back from an AMM account. Please notice the
5796 // `issuer` subfield represents the account being clawed back, which
5797 // is confusing.
5798 auto const error = features[featureSingleAssetVault] ? ter{tecPSEUDO_ACCOUNT} : ter{tecAMM_ACCOUNT};
5799 Issue usd(USD.issue().currency, amm.ammAccount());
5800 auto amount = amountFromString(usd, "10");
5801 env(claw(gw, amount), error);
5802 }
5803 }
5804
5805 void
5807 {
5808 testcase("test AMMDeposit with frozen assets");
5809 using namespace jtx;
5810
5811 // This lambda function is used to create trustlines
5812 // between gw and alice, and create an AMM account.
5813 // And also test the callback function.
5814 auto testAMMDeposit = [&](Env& env, std::function<void(AMM & amm)> cb) {
5815 env.fund(XRP(1'000), gw);
5816 fund(env, gw, {alice}, XRP(1'000), {USD(1'000)}, Fund::Acct);
5817 env.close();
5818 AMM amm(env, alice, XRP(100), USD(100), ter(tesSUCCESS));
5819 env(trust(gw, alice["USD"](0), tfSetFreeze));
5820 cb(amm);
5821 };
5822
5823 // Deposit two assets, one of which is frozen,
5824 // then we should get tecFROZEN error.
5825 {
5826 Env env(*this, features);
5827 testAMMDeposit(env, [&](AMM& amm) {
5828 amm.deposit(alice, USD(100), XRP(100), std::nullopt, tfTwoAsset, ter(tecFROZEN));
5829 });
5830 }
5831
5832 // Deposit one asset, which is the frozen token,
5833 // then we should get tecFROZEN error.
5834 {
5835 Env env(*this, features);
5836 testAMMDeposit(env, [&](AMM& amm) {
5837 amm.deposit(alice, USD(100), std::nullopt, std::nullopt, tfSingleAsset, ter(tecFROZEN));
5838 });
5839 }
5840
5841 if (features[featureAMMClawback])
5842 {
5843 // Deposit one asset which is not the frozen token,
5844 // but the other asset is frozen. We should get tecFROZEN error
5845 // when feature AMMClawback is enabled.
5846 Env env(*this, features);
5847 testAMMDeposit(env, [&](AMM& amm) {
5848 amm.deposit(alice, XRP(100), std::nullopt, std::nullopt, tfSingleAsset, ter(tecFROZEN));
5849 });
5850 }
5851 else
5852 {
5853 // Deposit one asset which is not the frozen token,
5854 // but the other asset is frozen. We will get tecSUCCESS
5855 // when feature AMMClawback is not enabled.
5856 Env env(*this, features);
5857 testAMMDeposit(env, [&](AMM& amm) {
5858 amm.deposit(alice, XRP(100), std::nullopt, std::nullopt, tfSingleAsset, ter(tesSUCCESS));
5859 });
5860 }
5861 }
5862
5863 void
5865 {
5866 testcase("Fix Reserve Check On Withdrawal");
5867 using namespace jtx;
5868
5869 auto const err = features[fixAMMv1_2] ? ter(tecINSUFFICIENT_RESERVE) : ter(tesSUCCESS);
5870
5871 auto test = [&](auto&& cb) {
5872 Env env(*this, features);
5873 auto const starting_xrp = reserve(env, 2) + env.current()->fees().base * 5;
5874 env.fund(starting_xrp, gw);
5875 env.fund(starting_xrp, alice);
5876 env.trust(USD(2'000), alice);
5877 env.close();
5878 env(pay(gw, alice, USD(2'000)));
5879 env.close();
5880 AMM amm(env, gw, EUR(1'000), USD(1'000));
5881 amm.deposit(alice, USD(1));
5882 cb(amm);
5883 };
5884
5885 // Equal withdraw
5886 test([&](AMM& amm) { amm.withdrawAll(alice, std::nullopt, err); });
5887
5888 // Equal withdraw with a limit
5889 test([&](AMM& amm) {
5890 amm.withdraw(WithdrawArg{.account = alice, .asset1Out = EUR(0.1), .asset2Out = USD(0.1), .err = err});
5891 amm.withdraw(WithdrawArg{.account = alice, .asset1Out = USD(0.1), .asset2Out = EUR(0.1), .err = err});
5892 });
5893
5894 // Single withdraw
5895 test([&](AMM& amm) {
5896 amm.withdraw(WithdrawArg{.account = alice, .asset1Out = EUR(0.1), .err = err});
5897 amm.withdraw(WithdrawArg{.account = alice, .asset1Out = USD(0.1)});
5898 });
5899 }
5900
5901 void
5903 {
5904 using namespace test::jtx;
5905
5906 auto const testCase = [&](std::string suffix, FeatureBitset features) {
5907 testcase("Fail pseudo-account allocation " + suffix);
5908 std::string logs;
5909 Env env{*this, features, std::make_unique<CaptureLogs>(&logs)};
5910 env.fund(XRP(30'000), gw, alice);
5911 env.close();
5912 env(trust(alice, gw["USD"](30'000), 0));
5913 env(pay(gw, alice, USD(10'000)));
5914 env.close();
5915
5916 STAmount amount = XRP(10'000);
5917 STAmount amount2 = USD(10'000);
5918 auto const keylet = keylet::amm(amount.issue(), amount2.issue());
5919 for (int i = 0; i < 256; ++i)
5920 {
5921 AccountID const accountId = xrpl::pseudoAccountAddress(*env.current(), keylet.key);
5922
5923 env(pay(env.master.id(), accountId, XRP(1000)), seq(autofill), fee(autofill), sig(autofill));
5924 }
5925
5926 AMM ammAlice(
5927 env,
5928 alice,
5929 amount,
5930 amount2,
5931 features[featureSingleAssetVault] ? ter{terADDRESS_COLLISION} : ter{tecDUPLICATE});
5932 };
5933
5934 testCase("tecDUPLICATE", testable_amendments() - featureSingleAssetVault);
5935 testCase("terADDRESS_COLLISION", testable_amendments() | featureSingleAssetVault);
5936 }
5937
5938 void
5940 {
5941 testcase("Deposit and Withdraw Rounding V2");
5942 using namespace jtx;
5943
5944 auto const XPM = gw["XPM"];
5945 STAmount xrpBalance{XRPAmount(692'614'492'126)};
5946 STAmount xpmBalance{XPM, UINT64_C(18'610'359'80246901), -8};
5947 STAmount amount{XPM, UINT64_C(6'566'496939465400), -12};
5948 std::uint16_t tfee = 941;
5949
5950 auto test = [&](auto&& cb, std::uint16_t tfee_) {
5951 Env env(*this, features);
5952 env.fund(XRP(1'000'000), gw);
5953 env.fund(XRP(1'000), alice);
5954 env(trust(alice, XPM(7'000)));
5955 env(pay(gw, alice, amount));
5956
5957 AMM amm(env, gw, xrpBalance, xpmBalance, CreateArg{.tfee = tfee_});
5958 // AMM LPToken balance required to replicate single deposit failure
5959 STAmount lptAMMBalance{amm.lptIssue(), UINT64_C(3'234'987'266'485968), -6};
5960 auto const burn = IOUAmount{amm.getLPTokensBalance() - lptAMMBalance};
5961 // burn tokens to get to the required AMM state
5962 env(amm.bid(BidArg{.account = gw, .bidMin = burn, .bidMax = burn}));
5963 cb(amm, env);
5964 };
5965 test(
5966 [&](AMM& amm, Env& env) {
5967 auto const err = env.enabled(fixAMMv1_3) ? ter(tesSUCCESS) : ter(tecUNFUNDED_AMM);
5968 amm.deposit(DepositArg{.account = alice, .asset1In = amount, .err = err});
5969 },
5970 tfee);
5971 test(
5972 [&](AMM& amm, Env& env) {
5973 auto const [amount, amount2, lptAMM] = amm.balances(XRP, XPM);
5974 auto const withdraw = STAmount{XPM, 1, -5};
5975 amm.withdraw(WithdrawArg{.asset1Out = STAmount{XPM, 1, -5}});
5976 auto const [amount_, amount2_, lptAMM_] = amm.balances(XRP, XPM);
5977 if (!env.enabled(fixAMMv1_3))
5978 BEAST_EXPECT((amount2 - amount2_) > withdraw);
5979 else
5980 BEAST_EXPECT((amount2 - amount2_) <= withdraw);
5981 },
5982 0);
5983 }
5984
5985 void
5986 invariant(jtx::AMM& amm, jtx::Env& env, std::string const& msg, bool shouldFail)
5987 {
5988 auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR);
5989
5990 NumberMantissaScaleGuard sg(MantissaRange::small);
5991 NumberRoundModeGuard g(env.enabled(fixAMMv1_3) ? Number::upward : Number::getround());
5992 auto const res = root2(amount * amount2);
5993
5994 if (shouldFail)
5995 BEAST_EXPECT(res < lptBalance);
5996 else
5997 BEAST_EXPECT(res >= lptBalance);
5998 }
5999
6000 void
6002 {
6003 testcase("Deposit Rounding");
6004 using namespace jtx;
6005
6006 // Single asset deposit
6007 for (auto const& deposit :
6008 {STAmount(EUR, 1, 1),
6009 STAmount(EUR, 1, 2),
6010 STAmount(EUR, 1, 5),
6011 STAmount(EUR, 1, -3), // fail
6012 STAmount(EUR, 1, -6),
6013 STAmount(EUR, 1, -9)})
6014 {
6015 testAMM(
6016 [&](AMM& ammAlice, Env& env) {
6017 fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
6018 env.close();
6019
6020 ammAlice.deposit(DepositArg{.account = bob, .asset1In = deposit});
6021 invariant(ammAlice, env, "dep1", deposit == STAmount{EUR, 1, -3} && !env.enabled(fixAMMv1_3));
6022 },
6023 {{GBP(30'000), EUR(30'000)}},
6024 0,
6026 {all});
6027 }
6028
6029 // Two-asset proportional deposit (1:1 pool ratio)
6030 testAMM(
6031 [&](AMM& ammAlice, Env& env) {
6032 fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
6033 env.close();
6034
6035 STAmount const depositEuro{EUR, UINT64_C(10'1234567890123456), -16};
6036 STAmount const depositGBP{GBP, UINT64_C(10'1234567890123456), -16};
6037
6038 ammAlice.deposit(DepositArg{.account = bob, .asset1In = depositEuro, .asset2In = depositGBP});
6039 invariant(ammAlice, env, "dep2", false);
6040 },
6041 {{GBP(30'000), EUR(30'000)}},
6042 0,
6044 {all});
6045
6046 // Two-asset proportional deposit (1:3 pool ratio)
6047 for (auto const& exponent : {1, 2, 3, 4, -3 /*fail*/, -6, -9})
6048 {
6049 testAMM(
6050 [&](AMM& ammAlice, Env& env) {
6051 fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
6052 env.close();
6053
6054 STAmount const depositEuro{EUR, 1, exponent};
6055 STAmount const depositGBP{GBP, 1, exponent};
6056
6057 ammAlice.deposit(DepositArg{.account = bob, .asset1In = depositEuro, .asset2In = depositGBP});
6058 invariant(ammAlice, env, "dep3", exponent != -3 && !env.enabled(fixAMMv1_3));
6059 },
6060 {{GBP(10'000), EUR(30'000)}},
6061 0,
6063 {all});
6064 }
6065
6066 // tfLPToken deposit
6067 testAMM(
6068 [&](AMM& ammAlice, Env& env) {
6069 fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
6070 env.close();
6071
6072 ammAlice.deposit(DepositArg{.account = bob, .tokens = IOUAmount{10'1234567890123456, -16}});
6073 invariant(ammAlice, env, "dep4", false);
6074 },
6075 {{GBP(7'000), EUR(30'000)}},
6076 0,
6078 {all});
6079
6080 // tfOneAssetLPToken deposit
6081 for (auto const& tokens :
6082 {IOUAmount{1, -3},
6083 IOUAmount{1, -2},
6084 IOUAmount{1, -1},
6085 IOUAmount{1},
6086 IOUAmount{10},
6087 IOUAmount{100},
6088 IOUAmount{1'000},
6089 IOUAmount{10'000}})
6090 {
6091 testAMM(
6092 [&](AMM& ammAlice, Env& env) {
6093 fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(1'000'000)}, Fund::Acct);
6094 env.close();
6095
6096 ammAlice.deposit(DepositArg{.account = bob, .tokens = tokens, .asset1In = STAmount{EUR, 1, 6}});
6097 invariant(ammAlice, env, "dep5", false);
6098 },
6099 {{GBP(7'000), EUR(30'000)}},
6100 0,
6102 {all});
6103 }
6104
6105 // Single deposit with EP not exceeding specified:
6106 // 1'000 GBP with EP not to exceed 5 (GBP/TokensOut)
6107 testAMM(
6108 [&](AMM& ammAlice, Env& env) {
6109 fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
6110 env.close();
6111
6112 ammAlice.deposit(bob, GBP(1'000), std::nullopt, STAmount{GBP, 5});
6113 invariant(ammAlice, env, "dep6", false);
6114 },
6115 {{GBP(30'000), EUR(30'000)}},
6116 0,
6118 {all});
6119 }
6120
6121 void
6123 {
6124 testcase("Withdraw Rounding");
6125
6126 using namespace jtx;
6127
6128 // tfLPToken mode
6129 testAMM(
6130 [&](AMM& ammAlice, Env& env) {
6131 ammAlice.withdraw(alice, 1'000);
6132 invariant(ammAlice, env, "with1", false);
6133 },
6134 {{GBP(7'000), EUR(30'000)}},
6135 0,
6137 {all});
6138
6139 // tfWithdrawAll mode
6140 testAMM(
6141 [&](AMM& ammAlice, Env& env) {
6142 ammAlice.withdraw(WithdrawArg{.account = alice, .flags = tfWithdrawAll});
6143 invariant(ammAlice, env, "with2", false);
6144 },
6145 {{GBP(7'000), EUR(30'000)}},
6146 0,
6148 {all});
6149
6150 // tfTwoAsset withdraw mode
6151 testAMM(
6152 [&](AMM& ammAlice, Env& env) {
6153 ammAlice.withdraw(WithdrawArg{
6154 .account = alice,
6155 .asset1Out = STAmount{GBP, 3'500},
6156 .asset2Out = STAmount{EUR, 15'000},
6157 .flags = tfTwoAsset});
6158 invariant(ammAlice, env, "with3", false);
6159 },
6160 {{GBP(7'000), EUR(30'000)}},
6161 0,
6163 {all});
6164
6165 // tfSingleAsset withdraw mode
6166 // Note: This test fails with 0 trading fees, but doesn't fail if
6167 // trading fees is set to 1'000 -- I suspect the compound operations
6168 // in AMMHelpers.cpp:withdrawByTokens compensate for the rounding
6169 // errors
6170 testAMM(
6171 [&](AMM& ammAlice, Env& env) {
6172 ammAlice.withdraw(
6173 WithdrawArg{.account = alice, .asset1Out = STAmount{GBP, 1'234}, .flags = tfSingleAsset});
6174 invariant(ammAlice, env, "with4", false);
6175 },
6176 {{GBP(7'000), EUR(30'000)}},
6177 0,
6179 {all});
6180
6181 // tfOneAssetWithdrawAll mode
6182 testAMM(
6183 [&](AMM& ammAlice, Env& env) {
6184 fund(env, gw, {bob}, XRP(10'000'000), {GBP(100'000), EUR(100'000)}, Fund::Acct);
6185 env.close();
6186
6187 ammAlice.deposit(DepositArg{.account = bob, .asset1In = STAmount{GBP, 3'456}});
6188
6189 ammAlice.withdraw(
6190 WithdrawArg{.account = bob, .asset1Out = STAmount{GBP, 1'000}, .flags = tfOneAssetWithdrawAll});
6191 invariant(ammAlice, env, "with5", false);
6192 },
6193 {{GBP(7'000), EUR(30'000)}},
6194 0,
6196 {all});
6197
6198 // tfOneAssetLPToken mode
6199 testAMM(
6200 [&](AMM& ammAlice, Env& env) {
6201 ammAlice.withdraw(WithdrawArg{
6202 .account = alice, .tokens = 1'000, .asset1Out = STAmount{GBP, 100}, .flags = tfOneAssetLPToken});
6203 invariant(ammAlice, env, "with6", false);
6204 },
6205 {{GBP(7'000), EUR(30'000)}},
6206 0,
6208 {all});
6209
6210 // tfLimitLPToken mode
6211 testAMM(
6212 [&](AMM& ammAlice, Env& env) {
6213 ammAlice.withdraw(WithdrawArg{
6214 .account = alice, .asset1Out = STAmount{GBP, 100}, .maxEP = IOUAmount{2}, .flags = tfLimitLPToken});
6215 invariant(ammAlice, env, "with7", true);
6216 },
6217 {{GBP(7'000), EUR(30'000)}},
6218 0,
6220 {all});
6221 }
6222
6223 void
6224 run() override
6225 {
6227 testInvalidInstance();
6228 testInstanceCreate();
6229 testInvalidDeposit(all);
6230 testInvalidDeposit(all - featureAMMClawback);
6231 testDeposit();
6232 testInvalidWithdraw();
6233 testWithdraw();
6234 testInvalidFeeVote();
6235 testFeeVote();
6236 testInvalidBid();
6237 testBid(all);
6238 testBid(all - fixAMMv1_3);
6239 testBid(all - fixAMMv1_1 - fixAMMv1_3);
6240 testInvalidAMMPayment();
6241 testBasicPaymentEngine(all);
6242 testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3);
6243 testBasicPaymentEngine(all - fixReducedOffersV2);
6244 testBasicPaymentEngine(all - fixAMMv1_1 - fixAMMv1_3 - fixReducedOffersV2);
6245 testAMMTokens();
6246 testAmendment();
6247 testFlags();
6248 testRippling();
6249 testAMMAndCLOB(all);
6250 testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3);
6251 testTradingFee(all);
6252 testTradingFee(all - fixAMMv1_3);
6253 testTradingFee(all - fixAMMv1_1 - fixAMMv1_3);
6254 testAdjustedTokens(all);
6255 testAdjustedTokens(all - fixAMMv1_3);
6256 testAdjustedTokens(all - fixAMMv1_1 - fixAMMv1_3);
6257 testAutoDelete();
6258 testClawback();
6259 testAMMID();
6260 testSelection(all);
6261 testSelection(all - fixAMMv1_1 - fixAMMv1_3);
6262 testFixDefaultInnerObj();
6263 testMalformed();
6264 testFixOverflowOffer(all);
6265 testFixOverflowOffer(all - fixAMMv1_3);
6266 testFixOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3);
6267 testSwapRounding();
6268 testFixChangeSpotPriceQuality(all);
6269 testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3);
6270 testFixAMMOfferBlockedByLOB(all);
6271 testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3);
6272 testLPTokenBalance(all);
6273 testLPTokenBalance(all - fixAMMv1_3);
6274 testLPTokenBalance(all - fixAMMv1_1 - fixAMMv1_3);
6275 testAMMClawback(all);
6276 testAMMClawback(all - featureSingleAssetVault);
6277 testAMMClawback(all - featureAMMClawback - featureSingleAssetVault);
6278 testAMMClawback(all - featureAMMClawback);
6279 testAMMClawback(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback);
6280 testAMMDepositWithFrozenAssets(all);
6281 testAMMDepositWithFrozenAssets(all - featureAMMClawback);
6282 testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - featureAMMClawback);
6283 testAMMDepositWithFrozenAssets(all - fixAMMv1_1 - fixAMMv1_3 - featureAMMClawback);
6284 testFixReserveCheckOnWithdrawal(all);
6285 testFixReserveCheckOnWithdrawal(all - fixAMMv1_2);
6286 testDepositAndWithdrawRounding(all);
6287 testDepositAndWithdrawRounding(all - fixAMMv1_3);
6288 testDepositRounding(all);
6289 testDepositRounding(all - fixAMMv1_3);
6290 testWithdrawRounding(all);
6291 testWithdrawRounding(all - fixAMMv1_3);
6292 testFailedPseudoAccount();
6293 }
6294};
6295
6296BEAST_DEFINE_TESTSUITE_PRIO(AMM, app, xrpl, 1);
6297
6298} // namespace test
6299} // namespace xrpl
Represents a JSON value.
Definition json_value.h:130
std::string asString() const
Returns the unquoted string value.
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:147
RAII class to set and restore the current transaction rules.
Definition Rules.h:87
Floating point representation of amounts with high dynamic range.
Definition IOUAmount.h:25
A currency issued by an account.
Definition Issue.h:13
Currency currency
Definition Issue.h:15
Sets the new scale and restores the old scale when it leaves scope.
Definition Number.h:800
Number is a floating point type that can represent a wide range of values.
Definition Number.h:207
Issue const & issue() const
Definition STAmount.h:454
Json::Value getJson(JsonOptions) const override
Definition STIssue.cpp:78
jtx::Account const gw
Definition AMMTest.h:59
jtx::Account const bob
Definition AMMTest.h:62
jtx::Account const alice
Definition AMMTest.h:61
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:60
XRPAmount ammCrtFee(jtx::Env &env) const
Definition AMMTest.cpp:151
XRPAmount reserve(jtx::Env &env, std::uint32_t count) const
Definition AMMTest.cpp:145
Convenience class to test AMM functionality.
Definition AMM.h:104
bool expectTradingFee(std::uint16_t fee) const
Definition AMM.cpp:229
IOUAmount getLPTokensBalance(std::optional< AccountID > const &account=std::nullopt) const
Definition AMM.cpp:181
Issue lptIssue() const
Definition AMM.h:303
bool ammExists() const
Definition AMM.cpp:236
void setTokens(Json::Value &jv, std::optional< std::pair< Issue, Issue > > const &assets=std::nullopt)
Definition AMM.cpp:280
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:496
bool expectAuctionSlot(std::uint32_t fee, std::optional< std::uint8_t > timeSlot, IOUAmount expectedPrice) const
Definition AMM.cpp:202
IOUAmount tokens() const
Definition AMM.h:309
IOUAmount withdrawAll(std::optional< Account > const &account, std::optional< STAmount > const &asset1OutDetails=std::nullopt, std::optional< ter > const &ter=std::nullopt)
Definition AMM.h:249
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:422
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:314
AccountID const & ammAccount() const
Definition AMM.h:297
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:115
Json::Value bid(BidArg const &arg)
Definition AMM.cpp:523
bool expectLPTokens(AccountID const &account, IOUAmount const &tokens) const
Definition AMM.cpp:191
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:170
Immutable cryptographic account descriptor.
Definition Account.h:19
std::string const & human() const
Returns the human readable public key.
Definition Account.h:94
AccountID id() const
Returns the Account ID.
Definition Account.h:87
A transaction testing environment.
Definition Env.h:97
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:97
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:91
std::shared_ptr< SLE const > le(Account const &account) const
Return an account root.
Definition Env.cpp:248
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:260
Account const & master
Definition Env.h:101
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:157
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:283
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:748
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:450
bool enabled(uint256 feature) const
Definition Env.h:599
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:130
beast::Journal const journal
Definition Env.h:138
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:297
NetClock::time_point now()
Returns the current network time.
Definition Env.h:252
A balance matches.
Definition balance.h:19
Set the fee on a JTx.
Definition fee.h:17
Match set account flags.
Definition flags.h:108
Add a path.
Definition paths.h:37
Sets the SendMax on a JTx.
Definition sendmax.h:13
Set the regular signature on a JTx.
Definition sig.h:15
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:15
Set the flags on a JTx.
Definition txflags.h:11
T is_same_v
T make_pair(T... args)
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:26
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:325
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:393
Json::Value pay(Account const &account, AccountID const &to, STAmount const &amount)
Definition AMM.cpp:662
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:46
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:90
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:101
FeatureBitset testable_amendments()
Definition Env.h:54
auto const amount
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:34
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:5
constexpr std::uint32_t asfAllowTrustLineClawback
Definition TxFlags.h:74
std::uint32_t constexpr AUCTION_SLOT_INTERVAL_DURATION
Definition AMMCore.h:20
@ telINSUF_FEE_P
Definition TER.h:37
@ terNO_AMM
Definition TER.h:207
@ terNO_RIPPLE
Definition TER.h:204
@ terADDRESS_COLLISION
Definition TER.h:208
@ terNO_ACCOUNT
Definition TER.h:197
base_uint< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition UintTypes.h:36
constexpr std::uint32_t asfGlobalFreeze
Definition TxFlags.h:63
constexpr std::uint32_t tfPassive
Definition TxFlags.h:78
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:97
std::uint16_t constexpr AUCTION_SLOT_TIME_INTERVALS
Definition AMMCore.h:15
AccountID pseudoAccountAddress(ReadView const &view, uint256 const &pseudoOwnerKey)
Definition View.cpp:971
constexpr std::uint32_t tfWithdrawAll
Definition TxFlags.h:225
bool isXRP(AccountID const &c)
Definition AccountID.h:70
constexpr std::uint32_t tfSingleAsset
Definition TxFlags.h:227
std::uint32_t constexpr TOTAL_TIME_SLOT_SECS
Definition AMMCore.h:14
constexpr std::uint32_t const tfBurnable
Definition TxFlags.h:119
@ tefEXCEPTION
Definition TER.h:152
std::uint16_t constexpr maxDeletableAMMTrustLines
The maximum number of trustlines to delete as part of AMM account deletion cleanup.
Definition Protocol.h:265
@ Fail
Should not be retried in this ledger.
constexpr std::uint32_t tfLimitQuality
Definition TxFlags.h:89
STAmount amountFromString(Asset const &asset, std::string const &amount)
Definition STAmount.cpp:939
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:395
constexpr std::uint32_t tfTwoAsset
Definition TxFlags.h:228
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:314
constexpr std::uint32_t tfOneAssetLPToken
Definition TxFlags.h:229
constexpr std::uint32_t asfDefaultRipple
Definition TxFlags.h:64
constexpr std::uint32_t tfOneAssetWithdrawAll
Definition TxFlags.h:226
constexpr std::uint32_t tfClearFreeze
Definition TxFlags.h:99
constexpr std::uint32_t tfTwoAssetIfEmpty
Definition TxFlags.h:231
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:87
Number root2(Number f)
Definition Number.cpp:1010
constexpr std::uint32_t tfSetfAuth
Definition TxFlags.h:95
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:281
constexpr std::uint32_t asfRequireAuth
Definition TxFlags.h:58
@ temBAD_CURRENCY
Definition TER.h:70
@ temBAD_FEE
Definition TER.h:72
@ temBAD_AMM_TOKENS
Definition TER.h:109
@ temINVALID_FLAG
Definition TER.h:91
@ temMALFORMED
Definition TER.h:67
@ temDISABLED
Definition TER.h:94
@ temBAD_AMOUNT
Definition TER.h:69
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:463
constexpr std::uint32_t tfLimitLPToken
Definition TxFlags.h:230
constexpr std::uint32_t tfLPToken
Definition TxFlags.h:224
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition Issue.h:105
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:98
@ tecPSEUDO_ACCOUNT
Definition TER.h:343
@ tecAMM_EMPTY
Definition TER.h:313
@ tecPATH_PARTIAL
Definition TER.h:263
@ tecAMM_INVALID_TOKENS
Definition TER.h:312
@ tecINSUF_RESERVE_LINE
Definition TER.h:269
@ tecAMM_FAILED
Definition TER.h:311
@ tecAMM_NOT_EMPTY
Definition TER.h:314
@ tecPATH_DRY
Definition TER.h:275
@ tecINCOMPLETE
Definition TER.h:316
@ tecUNFUNDED_AMM
Definition TER.h:309
@ tecNO_AUTH
Definition TER.h:281
@ tecAMM_BALANCE
Definition TER.h:310
@ tecINVARIANT_FAILED
Definition TER.h:294
@ tecFROZEN
Definition TER.h:284
@ tecAMM_ACCOUNT
Definition TER.h:315
@ tecOWNERS
Definition TER.h:279
@ tecINSUFFICIENT_RESERVE
Definition TER.h:288
@ tecNO_PERMISSION
Definition TER.h:286
@ tecDUPLICATE
Definition TER.h:296
@ lsfDepositAuth
@ lsfDefaultRipple
@ lsfDisableMaster
constexpr std::uint32_t tfPartialPayment
Definition TxFlags.h:88
constexpr std::uint32_t tfSetFreeze
Definition TxFlags.h:98
@ tesSUCCESS
Definition TER.h:225
Issue getIssue(T const &amt)
T push_back(T... args)
Zero allows classes to offer efficient comparisons to zero.
Definition Zero.h:25
uint256 key
Definition Keylet.h:20
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:396
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:45
std::optional< LPToken > tokens
Definition AMM.h:57
std::optional< Account > account
Definition AMM.h:56
std::optional< STAmount > asset1In
Definition AMM.h:58
std::uint32_t tfee
Definition AMM.h:84
std::optional< Account > account
Definition AMM.h:83
std::optional< ter > err
Definition AMM.h:78
std::optional< STAmount > asset1Out
Definition AMM.h:72
std::optional< std::uint32_t > flags
Definition AMM.h:75
std::optional< Account > account
Definition AMM.h:70
std::optional< LPToken > tokens
Definition AMM.h:71
Set the sequence number on a JTx.
Definition seq.h:14
T to_string(T... args)
T what(T... args)