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