xrpld
Loading...
Searching...
No Matches
AMMMPT_test.cpp
1#include <test/jtx/AMM.h>
2#include <test/jtx/AMMTest.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/TestHelpers.h>
5#include <test/jtx/amount.h>
6#include <test/jtx/balance.h>
7#include <test/jtx/envconfig.h>
8#include <test/jtx/escrow.h>
9#include <test/jtx/fee.h>
10#include <test/jtx/flags.h>
11#include <test/jtx/mpt.h>
12#include <test/jtx/offer.h>
13#include <test/jtx/paths.h>
14#include <test/jtx/pay.h>
15#include <test/jtx/rate.h>
16#include <test/jtx/sendmax.h>
17#include <test/jtx/seq.h>
18#include <test/jtx/ter.h>
19#include <test/jtx/trust.h>
20#include <test/jtx/txflags.h>
21#include <test/jtx/vault.h>
22
23#include <xrpl/basics/base_uint.h>
24#include <xrpl/basics/chrono.h>
25#include <xrpl/beast/unit_test/suite.h>
26#include <xrpl/beast/utility/Journal.h>
27#include <xrpl/json/json_value.h>
28#include <xrpl/ledger/ApplyView.h>
29#include <xrpl/ledger/helpers/AMMHelpers.h>
30#include <xrpl/protocol/AMMCore.h>
31#include <xrpl/protocol/AccountID.h>
32#include <xrpl/protocol/Feature.h>
33#include <xrpl/protocol/Indexes.h>
34#include <xrpl/protocol/Issue.h>
35#include <xrpl/protocol/MPTIssue.h>
36#include <xrpl/protocol/Protocol.h>
37#include <xrpl/protocol/Quality.h>
38#include <xrpl/protocol/SField.h>
39#include <xrpl/protocol/STAmount.h>
40#include <xrpl/protocol/TER.h>
41#include <xrpl/protocol/TxFlags.h>
42#include <xrpl/protocol/UintTypes.h>
43#include <xrpl/protocol/jss.h>
44#include <xrpl/tx/Transactor.h>
45#include <xrpl/tx/transactors/dex/AMMBid.h>
46
47#include <array>
48#include <chrono>
49#include <cstdint>
50#include <functional>
51#include <initializer_list>
52#include <memory>
53#include <optional>
54#include <string>
55#include <type_traits>
56#include <utility>
57#include <vector>
58
59namespace xrpl::test {
60
66{
67private:
68 void
70 {
71 testcase("Instance Create");
72
73 using namespace jtx;
74
75 // XRP to MPT
76 testAMM(
77 [&](AMM& ammAlice, Env&) {
78 BEAST_EXPECT(ammAlice.expectBalances(
79 XRP(10'000), MPT(ammAlice[1])(10'000), IOUAmount{10'000'000}));
80 },
81 {{XRP(10'000), gAmmmpt(10'000)}});
82
83 // IOU to MPT
84 testAMM(
85 [&](AMM& ammAlice, Env&) {
86 BEAST_EXPECT(ammAlice.expectBalances(
87 USD(20'000), MPT(ammAlice[1])(20'000), IOUAmount{20'000}));
88 },
89 {{USD(20'000), gAmmmpt(20'000)}});
90
91 // MPT to MPT
92 testAMM(
93 [&](AMM& ammAlice, Env&) {
94 BEAST_EXPECT(ammAlice.expectBalances(
95 MPT(ammAlice[0])(20'000), MPT(ammAlice[1])(20'000), IOUAmount{20'000}));
96 },
97 {{gAmmmpt(20'000), gAmmmpt(20'000)}});
98
99 // IOU to MPT + transfer fee
100 {
101 Env env{*this};
102 fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All);
103 env(rate(gw_, 1.25));
104 env.close();
105 MPT const btc = MPTTester(
106 {.env = env,
107 .issuer = gw_,
108 .holders = {alice_},
109 .transferFee = 1'500,
110 .pay = 30'000});
111 // no transfer fee on create
112 AMM const ammAlice(env, alice_, USD(20'000), btc(20'000));
113 BEAST_EXPECT(ammAlice.expectBalances(USD(20'000), btc(20'000), IOUAmount{20'000, 0}));
114 BEAST_EXPECT(expectHolding(env, alice_, USD(0)));
115 // alice initially had 30'000
116 BEAST_EXPECT(expectMPT(env, alice_, btc(10'000)));
117 }
118
119 // Require authorization is set, account is authorized
120 {
121 Env env{*this};
122 env.fund(XRP(30'000), gw_, alice_);
123 env.close();
124 env(fset(gw_, asfRequireAuth));
125 env(trust(alice_, gw_["USD"](30'000), 0));
126 env(trust(gw_, alice_["USD"](0), tfSetfAuth));
127 env(pay(gw_, alice_, USD(10'000)));
128 env.close();
129 MPT const btc = MPTTester(
130 {.env = env,
131 .issuer = gw_,
132 .holders = {alice_},
133 .pay = 30'000,
134 .flags = tfMPTRequireAuth | kMptDexFlags,
135 .authHolder = true});
136 AMM const ammAlice(env, alice_, USD(10'000), btc(10'000));
137 }
138
139 // Cleared global freeze
140 {
141 Env env{*this};
142 env.fund(XRP(30'000), gw_, alice_);
143 MPTTester usd(
144 {.env = env,
145 .issuer = gw_,
146 .holders = {alice_},
147 .pay = 30'000,
148 .flags = tfMPTCanLock | kMptDexFlags});
149 usd.set({.flags = tfMPTLock});
150 AMM const ammAliceFail(env, alice_, XRP(10'000), usd(10'000), Ter(tecLOCKED));
151 usd.set({.flags = tfMPTUnlock});
152 AMM const ammAlice(env, alice_, XRP(10'000), usd(10'000));
153 }
154 }
155
156 void
158 {
159 testcase("Invalid Instance");
160
161 using namespace jtx;
162
163 // Can't have both tokens the same MPT
164 {
165 Env env{*this};
166 env.fund(XRP(30'000), alice_, gw_);
167 env.close();
168 MPT const btc = MPTTester(
169 {.env = env,
170 .issuer = gw_,
171 .holders = {alice_},
172 .transferFee = 1'500,
173 .pay = 40'000});
174 AMM const ammAlice(env, alice_, btc(20'000), btc(10'000), Ter(temBAD_AMM_TOKENS));
175 }
176
177 // MPTCanTrade is not set and AMM creator is not the issuer of MPT
178 {
179 Env env{*this};
180 env.fund(XRP(30'000), alice_, gw_);
181 env.close();
182 MPT const btc = MPTTester(
183 {.env = env,
184 .issuer = gw_,
185 .holders = {alice_},
186 .pay = 40'000,
187 .flags = tfMPTCanLock});
188 AMM const ammAlice(env, alice_, btc(20'000), XRP(10'000), Ter(tecNO_PERMISSION));
189 }
190
191 // MPTokenIssuance doesn't exist
192 {
193 Env env{*this};
194 env.fund(XRP(30'000), alice_, gw_);
195 env.close();
196 AMM const ammAlice(
197 env, alice_, MPT(gw_, 1'000)(20'000), XRP(10'000), Ter(tecOBJECT_NOT_FOUND));
198 }
199
200 // MPToken doesn't exist and amm creator is not the issuer
201 {
202 Env env{*this};
203 env.fund(XRP(30'000), alice_, gw_);
204 env.close();
205 MPT const btc = MPTTester({
206 .env = env,
207 .issuer = gw_,
208 });
209 AMM const ammAlice(env, alice_, btc(20'000), XRP(10'000), Ter(tecNO_AUTH));
210 }
211
212 // Can't have zero or negative amounts
213 {
214 Env env{*this};
215 fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All);
216 env(rate(gw_, 1.25));
217 env.close();
218 MPT const btc = MPTTester(
219 {.env = env,
220 .issuer = gw_,
221 .holders = {alice_},
222 .transferFee = 1'500,
223 .pay = 30'000});
224
225 AMM const ammAlice(env, alice_, XRP(0), btc(10'000), Ter(temBAD_AMOUNT));
226 BEAST_EXPECT(!ammAlice.ammExists());
227 AMM const ammAlice1(env, alice_, XRP(10'000), btc(0), Ter(temBAD_AMOUNT));
228 BEAST_EXPECT(!ammAlice1.ammExists());
229 AMM const ammAlice2(env, alice_, USD(0), btc(10'000), Ter(temBAD_AMOUNT));
230 BEAST_EXPECT(!ammAlice2.ammExists());
231 AMM const ammAlice3(env, alice_, USD(10'000), btc(0), Ter(temBAD_AMOUNT));
232 BEAST_EXPECT(!ammAlice3.ammExists());
233 AMM const ammAlice4(env, alice_, XRP(-10'0000), btc(10'000), Ter(temBAD_AMOUNT));
234 BEAST_EXPECT(!ammAlice4.ammExists());
235 AMM const ammAlice5(env, alice_, XRP(10'000), btc(-10'000), Ter(temBAD_AMOUNT));
236 BEAST_EXPECT(!ammAlice5.ammExists());
237 AMM const ammAlice6(env, alice_, USD(-10'000), btc(10'000), Ter(temBAD_AMOUNT));
238 BEAST_EXPECT(!ammAlice6.ammExists());
239 AMM const ammAlice7(env, alice_, USD(10'000), btc(-10'000), Ter(temBAD_AMOUNT));
240 BEAST_EXPECT(!ammAlice7.ammExists());
241 }
242
243 // Bad MPT
244 {
245 Env env{*this};
246 fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All);
247
248 AMM const ammAlice(env, alice_, XRP(10'000), MPT(badMPT())(100), Ter(temBAD_MPT));
249
250 BEAST_EXPECT(!ammAlice.ammExists());
251 }
252
253 // Insufficient MPT balance
254 {
255 Env env{*this};
256 fund(env, gw_, {alice_}, {USD(30'000)}, Fund::All);
257 MPT const btc = MPTTester(
258 {.env = env,
259 .issuer = gw_,
260 .holders = {alice_},
261 .transferFee = 1'500,
262 .pay = 30'000});
263 AMM const ammAlice(env, alice_, XRP(10'000), btc(40'000), Ter(tecUNFUNDED_AMM));
264 BEAST_EXPECT(!ammAlice.ammExists());
265 }
266
267 // Invalid trading fee
268 {
269 Env env{*this};
270 fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All);
271 MPT const btc = MPTTester(
272 {.env = env,
273 .issuer = gw_,
274 .holders = {alice_},
275 .transferFee = 1'500,
276 .pay = 30'000});
277 AMM const ammAlice(
278 env,
279 alice_,
280 USD(10'000),
281 btc(10'000),
282 false,
283 65'001,
284 10,
285 std::nullopt,
286 std::nullopt,
287 std::nullopt,
288 Ter(temBAD_FEE));
289 BEAST_EXPECT(!ammAlice.ammExists());
290 }
291
292 // AMM already exists XRP/MPT
293 testAMM(
294 [&](AMM& ammAlice, Env& env) {
295 // Account bob1("bob1");
296 env.fund(XRP(30'000), bob_);
297 env.close();
298 AMM const ammBob(env, bob_, XRP(1'000), MPT(ammAlice[1])(1'000), Ter(tecDUPLICATE));
299 },
300 {{XRP(10'000), gAmmmpt(10'000)}});
301
302 // AMM already exists IOU/MPT
303 testAMM(
304 [&](AMM& ammAlice, Env& env) {
305 env.fund(XRP(30'000), bob_);
306 env.close();
307 env.trust(USD(10000), bob_);
308 env(pay(gw_, bob_, USD(100)));
309
310 AMM const ammBob(env, bob_, USD(1'000), MPT(ammAlice[1])(1'000), Ter(tecDUPLICATE));
311 },
312 {{USD(10'000), gAmmmpt(10'000)}});
313
314 // AMM already exists MPT/MPT
315 testAMM(
316 [&](AMM& ammAlice, Env& env) {
317 AMM const ammCarol(
318 env,
319 carol_,
320 MPT(ammAlice[0])(1'000),
321 MPT(ammAlice[1])(2'000),
323 },
324 {{gAmmmpt(20'000), gAmmmpt(10'000)}});
325
326 // MPTRequireAuth flag is set and AMM creator is not authorized
327 {
328 Env env{*this};
329 env.fund(XRP(30'000), gw_, alice_);
330 env.close();
331 env(fset(gw_, asfRequireAuth));
332 env(trust(alice_, gw_["USD"](30'000), 0));
333 env(trust(gw_, alice_["USD"](0), tfSetfAuth));
334 env.close();
335 env(pay(gw_, alice_, USD(10'000)));
336 env.close();
337 MPT const btc = MPTTester(
338 {.env = env, .issuer = gw_, .holders = {alice_}, .flags = tfMPTRequireAuth});
339 AMM const ammAlice(env, alice_, USD(10'000), btc(10'000), Ter(tecNO_AUTH));
340 BEAST_EXPECT(!ammAlice.ammExists());
341 }
342
343 // MPTLocked flag is set
344 {
345 Env env{*this};
346 fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All);
347 MPTTester btc(
348 {.env = env,
349 .issuer = gw_,
350 .holders = {alice_},
351 .pay = 30'000,
352 .flags = tfMPTCanLock | kMptDexFlags});
353 btc.set({.flags = tfMPTLock});
354 AMM const ammAlice(env, alice_, USD(10'000), btc(10'000), Ter(tecLOCKED));
355 BEAST_EXPECT(!ammAlice.ammExists());
356 }
357
358 // MPT individually locked
359 {
360 Env env{*this};
361 fund(env, gw_, {alice_, bob_}, {USD(20'000)}, Fund::All);
362 MPTTester btc(
363 {.env = env,
364 .issuer = gw_,
365 .holders = {alice_, bob_},
366 .pay = 30'000,
367 .flags = tfMPTCanLock | kMptDexFlags});
368 btc.set({.holder = alice_, .flags = tfMPTLock});
369
370 // alice's token is locked
371 AMM const ammAlice(env, alice_, USD(10'000), btc(10'000), Ter(tecLOCKED));
372 BEAST_EXPECT(!ammAlice.ammExists());
373
374 // bob can create
375 AMM const ammBob(env, bob_, USD(10'000), btc(10'000));
376 BEAST_EXPECT(ammBob.ammExists());
377 }
378
379 // OutstandingAmount > MaximumAmount
380 {
381 Env env{*this};
382 env.fund(XRP(1'000), gw_, alice_);
383
384 MPTTester const btc({.env = env, .issuer = gw_, .holders = {alice_}, .maxAmt = 100});
385
386 // OutstandingAmount is 0, issuer issues 10 over MaximumAmount
387 AMM const amm(env, gw_, XRP(100), btc(110), Ter(tecUNFUNDED_AMM));
388
389 env(pay(gw_, alice_, btc(100)));
390
391 // OutstandingAmount is 100, issuer issues 100 over MaximumAmount
392 AMM const amm1(env, gw_, XRP(100), btc(100), Ter(tecUNFUNDED_AMM));
393 // This is fine - alice transfers 100 to AMM. OutstandingAmount
394 // is 100.
395 AMM const ammAlice(env, alice_, XRP(100), btc(100));
396 }
397 }
398
399 void
401 {
402 testcase("Invalid Deposit");
403
404 using namespace jtx;
405
406 testAMM(
407 [&](AMM& ammAlice, Env& env) {
408 // LPTokenOut can not be zero
409 ammAlice.deposit(alice_, 0, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
410
411 // LPTokenOut can not be negative
412 ammAlice.deposit(
413 alice_, IOUAmount{-1}, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
414
415 // LPTokenOut can not be MPT
416 {
418 jv[jss::Account] = alice_.human();
419 jv[jss::TransactionType] = jss::AMMDeposit;
420 jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::Values::None);
421 jv[jss::Asset2] =
422 STIssue(sfAsset, MPT(ammAlice[1])).getJson(JsonOptions::Values::None);
423 jv[jss::LPTokenOut] =
424 MPT(ammAlice[1])(100).value().getJson(JsonOptions::Values::None);
425 jv[jss::Flags] = tfLPToken;
426 env(jv, Ter(telENV_RPC_FAILED));
427 }
428
429 // Provided LPTokenOut does not match AMM pool's LPToken
430 // asset
431 {
433 jv[jss::Account] = alice_.human();
434 jv[jss::TransactionType] = jss::AMMDeposit;
435 jv[jss::Asset] = STIssue(sfAsset, XRP).getJson(JsonOptions::Values::None);
436 jv[jss::Asset2] =
437 STIssue(sfAsset, MPT(ammAlice[1])).getJson(JsonOptions::Values::None);
438 jv[jss::LPTokenOut] = USD(100).value().getJson(JsonOptions::Values::None);
439 jv[jss::Flags] = tfLPToken;
440 env(jv, Ter(temBAD_AMM_TOKENS));
441 }
442
443 // Invalid trading fee
444 ammAlice.deposit(
445 carol_,
446 std::nullopt,
447 XRP(200),
448 MPT(ammAlice[1])(200),
449 std::nullopt,
450 tfTwoAssetIfEmpty,
451 std::nullopt,
452 std::nullopt,
453 10'000,
454 Ter(temBAD_FEE));
455
456 // Invalid tokens
457 {
458 auto const mpt1 = MPTIssue{MPTID(0xabc)};
459 auto const mpt2 = MPTIssue{MPTID(0xdef)};
460 ammAlice.deposit(
461 alice_,
462 1'000,
463 std::nullopt,
464 std::nullopt,
465 std::nullopt,
466 std::nullopt,
467 {{mpt1, mpt2}},
468 std::nullopt,
469 std::nullopt,
470 Ter(terNO_AMM));
471 }
472
473 // invalid MPT
474 ammAlice.deposit(
475 alice_, badMPT(), std::nullopt, std::nullopt, std::nullopt, Ter(temBAD_MPT));
476 ammAlice.deposit(
477 alice_, XRP(100), badMPT(), std::nullopt, std::nullopt, Ter(temBAD_MPT));
478
479 // MPTokenIssuance object doesn't exist
480 ammAlice.deposit(
481 alice_,
482 XRP(100),
483 MPT(gw_, 1'000)(100),
484 std::nullopt,
485 tfTwoAsset,
487
488 // MPToken object doesn't exist
489 env.fund(XRP(1'000), bob_);
490 ammAlice.deposit(
491 bob_,
492 XRP(100),
493 MPT(ammAlice[1])(200),
494 std::nullopt,
495 tfTwoAsset,
496 Ter(tecNO_AUTH));
497
498 MPTTester const btc(
499 {.env = env,
500 .issuer = gw_,
501 .holders = {alice_, carol_},
502 .flags = tfMPTCanLock | kMptDexFlags,
503 .authHolder = true});
504
505 // Depositing mismatched token, invalid Asset1In.issue
506 ammAlice.deposit(
507 alice_,
508 btc(100),
509 std::nullopt,
510 std::nullopt,
511 std::nullopt,
513
514 // Depositing mismatched token, invalid Asset2In.issue
515 ammAlice.deposit(
516 alice_, XRP(100), btc(100), std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
517
518 // Assets can not be the same
519 ammAlice.deposit(
520 alice_,
521 MPT(ammAlice[1])(100),
522 MPT(ammAlice[1])(200),
523 std::nullopt,
524 tfTwoAsset,
526
527 // Invalid amount value
528 ammAlice.deposit(
529 alice_,
530 MPT(ammAlice[1])(0),
531 std::nullopt,
532 std::nullopt,
533 std::nullopt,
535 ammAlice.deposit(
536 alice_,
537 MPT(ammAlice[1])(-1'000),
538 std::nullopt,
539 std::nullopt,
540 std::nullopt,
542
543 // Invalid Account
544 {
545 Account bad("bad");
546 env.memoize(bad);
547 ammAlice.deposit(
548 bad,
549 std::nullopt,
550 MPT(ammAlice[1])(1'000),
551 std::nullopt,
552 std::nullopt,
553 std::nullopt,
554 std::nullopt,
555 Seq(1),
556 std::nullopt,
558 }
559
560 // Invalid AMM
561 ammAlice.deposit(
562 alice_,
563 1'000,
564 std::nullopt,
565 std::nullopt,
566 std::nullopt,
567 std::nullopt,
568 {{MPT(btc), MPT(ammAlice[1])}},
569 std::nullopt,
570 std::nullopt,
571 Ter(terNO_AMM));
572
573 // Single deposit: 100000 tokens worth of MPT
574 // Amount to deposit exceeds Max
575 ammAlice.deposit(
576 carol_,
577 100'000,
578 MPT(ammAlice[1])(200),
579 std::nullopt,
580 std::nullopt,
581 std::nullopt,
582 std::nullopt,
583 std::nullopt,
584 std::nullopt,
586
587 // Single deposit: 100000 tokens worth of XRP
588 // Amount to deposit exceeds Max
589 ammAlice.deposit(
590 carol_,
591 100'000,
592 XRP(200),
593 std::nullopt,
594 std::nullopt,
595 std::nullopt,
596 std::nullopt,
597 std::nullopt,
598 std::nullopt,
600
601 // Deposit amount is invalid
602 ammAlice.deposit(
603 alice_,
604 MPT(ammAlice[1])(0),
605 std::nullopt,
606 STAmount{ammAlice.lptIssue(), 1, -1},
607 std::nullopt,
609 // Calculated amount is 0
610 ammAlice.deposit(
611 alice_,
612 MPT(ammAlice[1])(0),
613 std::nullopt,
614 STAmount{ammAlice.lptIssue(), 2'000, -6},
615 std::nullopt,
617
618 // Tiny deposit
619 ammAlice.deposit(
620 carol_,
621 IOUAmount{1, -10},
622 std::nullopt,
623 std::nullopt,
625
626 // Deposit non-empty AMM
627 ammAlice.deposit(
628 carol_,
629 XRP(100),
630 MPT(ammAlice[1])(100),
631 std::nullopt,
632 tfTwoAssetIfEmpty,
634 },
635 {{XRP(10'000), gAmmmpt(10'000)}});
636
637 // Tiny deposit
638 testAMM(
639 [&](AMM& ammAlice, Env& env) {
640 auto const enabledV13 = env.current()->rules().enabled(fixAMMv1_3);
641 auto const err = !enabledV13 ? Ter(temBAD_AMOUNT) : Ter(tesSUCCESS);
642 // Pre-amendment XRP deposit side is rounded to 0
643 // and deposit fails.
644 ammAlice.deposit(carol_, IOUAmount{1, -1}, std::nullopt, std::nullopt, err);
645 },
646 {{XRP(10'000), gAmmmpt(10'000)}},
647 0,
648 std::nullopt,
649 {features, features - fixAMMv1_3});
650
651 // Invalid AMM
652 testAMM(
653 [&](AMM& ammAlice, Env& env) {
654 ammAlice.withdrawAll(alice_);
655 ammAlice.deposit(alice_, 10'000, std::nullopt, std::nullopt, Ter(terNO_AMM));
656 },
657 {{XRP(10'000), gAmmmpt(10'000)}});
658
659 // Single deposit MPT with eprice
660 // the calculated amount to deposit is negative.
661 testAMM(
662 [&](AMM& ammAlice, Env& env) {
663 ammAlice.deposit(
664 carol_,
665 MPT(ammAlice[1])(100),
666 std::nullopt,
667 STAmount{ammAlice.lptIssue(), 1, -1},
668 tfLimitLPToken,
670
671 // although we should use lptoken unit for eprice,
672 // we don't check the currency any more, we just use
673 // the value
674 ammAlice.deposit(
675 carol_,
676 MPT(ammAlice[1])(100),
677 std::nullopt,
678 STAmount{USD, 1, -1},
679 tfLimitLPToken,
681 },
682 {{USD(10'000), gAmmmpt(10'000)}});
683
684 // Globally locked MPT
685 {
686 Env env{*this};
687 fund(env, gw_, {alice_, carol_}, {USD(20'000)}, Fund::All);
688 MPTTester btc(
689 {.env = env,
690 .issuer = gw_,
691 .holders = {alice_, carol_},
692 .pay = 30'000,
693 .flags = tfMPTCanLock | kMptDexFlags});
694
695 AMM ammAlice(env, alice_, USD(10'000), btc(10'000));
696 btc.set({.flags = tfMPTLock});
697
698 ammAlice.deposit(
699 carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
700
701 ammAlice.deposit(
702 carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
703
704 ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
705
706 ammAlice.deposit(
707 carol_, USD(100), btc(100), std::nullopt, std::nullopt, Ter(tecLOCKED));
708 }
709
710 // Individually lock MPT or freeze IOU (AMM) with IOU/MPT AMM
711 {
712 Env env{*this, features};
713 fund(env, gw_, {alice_, carol_}, {USD(20'000)}, Fund::All);
714 MPTTester btc(
715 {.env = env,
716 .issuer = gw_,
717 .holders = {alice_, carol_},
718 .pay = 30'000,
719 .flags = tfMPTCanLock | kMptDexFlags});
720
721 AMM ammAlice(env, alice_, USD(10'000), btc(10'000));
722
723 // Carol's mpt is locked
724 btc.set({.holder = carol_, .flags = tfMPTLock});
725
726 // Carol can not deposit locked mpt
727 ammAlice.deposit(
728 carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
729
730 ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
731
732 // Post-fixCleanup3_3_0 a locked holder cannot deposit the other
733 // (non-locked) token either, matching featureAMMClawback.
734 if (!features[featureAMMClawback] && !features[fixCleanup3_3_0])
735 {
736 ammAlice.deposit(carol_, USD(100), std::nullopt, std::nullopt, std::nullopt);
737 }
738 else
739 {
740 ammAlice.deposit(
741 carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
742 }
743
744 // Alice can deposit because she's not individually locked
745 ammAlice.deposit(alice_, btc(100), std::nullopt, std::nullopt, std::nullopt);
746 ammAlice.deposit(alice_, 1'000, std::nullopt, std::nullopt);
747 ammAlice.deposit(alice_, USD(100), std::nullopt, std::nullopt, std::nullopt);
748
749 // Unlock
750 btc.set({.holder = carol_, .flags = tfMPTUnlock});
751 // Carol can deposit after unlock
752 ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt);
753 ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt);
754
755 // Individually frozen AMM
756 env(trust(
757 gw_, STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
758 env.close();
759
760 // Post-fixCleanup3_3_0 the deposit checks both pool assets, so the
761 // non-frozen token cannot be deposited while the AMM's USD is frozen.
762 ammAlice.deposit(
763 carol_,
764 btc(100),
765 std::nullopt,
766 std::nullopt,
767 std::nullopt,
768 features[fixCleanup3_3_0] ? Ter(tecFROZEN) : Ter(tesSUCCESS));
769
770 // Cannot deposit frozen token
771 ammAlice.deposit(carol_, 1'000'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
772 ammAlice.deposit(
773 carol_, USD(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecFROZEN));
774
775 // unfreeze IOU
776 env(trust(
777 gw_,
778 STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0},
779 tfClearFreeze));
780 env.close();
781 // Can deposit
782 ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt);
783
784 // Individually lock AMM
785 btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock});
786
787 // Post-fixCleanup3_3_0 the non-locked token cannot be deposited
788 // while the AMM's BTC is locked.
789 ammAlice.deposit(
790 carol_,
791 USD(100),
792 std::nullopt,
793 std::nullopt,
794 std::nullopt,
795 features[fixCleanup3_3_0] ? Ter(tecLOCKED) : Ter(tesSUCCESS));
796
797 // Can not deposit locked token
798 ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
799 ammAlice.deposit(
800 carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
801
802 // Unlock AMM MPT
803 btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock});
804 // can deposit
805 ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt);
806 ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt);
807 }
808
809 // Individually lock MPT (AMM) account with MPT/MPT AMM.
810 // This block always runs with all amendments (incl. fixCleanup3_3_0),
811 // so the deposit checks both pool assets unconditionally.
812 {
813 Env env{*this};
814 env.fund(XRP(10'000), gw_, alice_, carol_);
815 env.close();
816 MPTTester btc(
817 {.env = env,
818 .issuer = gw_,
819 .holders = {alice_, carol_},
820 .pay = 30'000,
821 .flags = tfMPTCanLock | kMptDexFlags});
822 MPTTester usd(
823 {.env = env,
824 .issuer = gw_,
825 .holders = {alice_, carol_},
826 .pay = 40'000,
827 .flags = tfMPTCanLock | kMptDexFlags});
828
829 AMM ammAlice(env, alice_, usd(10'000), btc(10'000));
830
831 // Carol's BTC is locked
832 btc.set({.holder = carol_, .flags = tfMPTLock});
833 ammAlice.deposit(
834 carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
835
836 ammAlice.deposit(
837 carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
838
839 // Unlock carol's BTC
840 btc.set({.holder = carol_, .flags = tfMPTUnlock});
841
842 // Can deposit
843 ammAlice.deposit(carol_, usd(100), std::nullopt, std::nullopt, std::nullopt);
844 ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt);
845
846 // Individually lock MPT BTC (AMM) account
847 btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock});
848
849 // Post-fixCleanup3_3_0 the non-locked token USD cannot be deposited
850 // while the AMM's BTC is locked.
851 ammAlice.deposit(
852 carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
853
854 // Can not deposit locked token BTC
855 ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
856 ammAlice.deposit(
857 carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
858
859 // Unlock AMM MPT BTC
860 btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock});
861 // Can deposit BTC
862 ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt);
863 ammAlice.deposit(carol_, btc(100), std::nullopt, std::nullopt, std::nullopt);
864
865 // Individually Lock MPT USD (AMM) account
866 usd.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock});
867
868 // Post-fixCleanup3_3_0 the non-locked token BTC cannot be deposited
869 // while the AMM's USD is locked.
870 ammAlice.deposit(
871 carol_, btc(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
872
873 // Can not deposit locked token USD
874 ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
875 ammAlice.deposit(
876 carol_, usd(100), std::nullopt, std::nullopt, std::nullopt, Ter(tecLOCKED));
877
878 // Unlock AMM MPT USD
879 usd.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock});
880 // Can deposit USD
881 ammAlice.deposit(carol_, 1'000, std::nullopt, std::nullopt);
882 ammAlice.deposit(carol_, usd(100), std::nullopt, std::nullopt, std::nullopt);
883 }
884
885 // Deposit unauthorized token
886 {
887 Env env{*this, features};
888 Account const gw("gateway"), alice{"alice"}, carol{"carol"};
889 env.fund(XRP(30'000), alice, carol, gw);
890 env.close();
891
892 MPTTester btc(env, gw, {.holders = {alice, carol}, .fund = false});
893 btc.create(
894 {.maxAmt = 1'000'000,
895 .authorize = {{alice}},
896 .pay = {{{alice}, 10'000}},
897 .flags = tfMPTRequireAuth | kMptDexFlags,
898 .authHolder = true});
899
900 AMM amm(env, alice, XRP(10'000), btc(10'000));
901 env.close();
902
903 // Post-fixCleanup3_3_0 the deposit requires authorization for both
904 // pool assets, so the unauthorized MPT blocks the XRP deposit too.
905 if (!features[featureAMMClawback] && !features[fixCleanup3_3_0])
906 {
907 amm.deposit(carol, XRP(10), std::nullopt, std::nullopt, std::nullopt);
908 }
909 else
910 {
911 amm.deposit(
912 carol, XRP(10), std::nullopt, std::nullopt, std::nullopt, Ter(tecNO_AUTH));
913 }
914 }
915
916 // MPTCanTransfer is not set and the account is not the issuer of MPT
917 {
918 Env env{*this, features};
919 Account const gw("gateway"), alice{"alice"}, carol{"carol"};
920 env.fund(XRP(30'000), alice, carol, gw);
921 env.close();
922
923 MPTTester const btc(
924 {.env = env,
925 .issuer = gw,
926 .holders = {alice},
927 .pay = 1'000,
928 .flags = tfMPTCanTrade});
929
930 AMM amm(env, gw, XRP(10'000), btc(10'000));
931
932 amm.deposit({.account = alice, .asset1In = btc(10), .err = Ter(tecNO_AUTH)});
933 }
934
935 // Insufficient XRP balance
936 testAMM(
937 [&](AMM& ammAlice, Env& env) {
938 env.fund(XRP(1'000), bob_);
939 env.close();
940 ammAlice.deposit(bob_, XRP(10));
941 ammAlice.deposit(
942 bob_,
943 XRP(1'000),
944 std::nullopt,
945 std::nullopt,
946 std::nullopt,
948 },
949 {{XRP(10'000), gAmmmpt(10'000)}});
950
951 // Insufficient MPT balance
952 testAMM(
953 [&](AMM& ammAlice, Env& env) {
954 ammAlice.deposit(
955 carol_,
956 MPT(ammAlice[1])(450'000),
957 std::nullopt,
958 std::nullopt,
959 std::nullopt,
961 },
962 {{XRP(10'000), gAmmmpt(10'000)}});
963
964 // Insufficient IOU balance
965 testAMM(
966 [&](AMM& ammAlice, Env& env) {
967 fund(env, gw_, {bob_}, {USD(1'000)}, Fund::Acct);
968 ammAlice.deposit(
969 bob_,
970 USD(1'001),
971 std::nullopt,
972 std::nullopt,
973 std::nullopt,
975 },
976 {{USD(1000), gAmmmpt(1000)}});
977
978 // Insufficient MPT balance by tokens
979 {
980 Env env{*this};
981 env.fund(XRP(30'000), alice_, bob_, gw_);
982 env.close();
983 MPT const btc = MPTTester(
984 {.env = env,
985 .issuer = gw_,
986 .holders = {alice_, bob_},
987 .transferFee = 1'500,
988 .pay = 1000});
989 AMM ammAlice(env, alice_, XRP(20'000), btc(1000));
990 ammAlice.deposit(
991 bob_,
992 10'000'000,
993 std::nullopt,
994 std::nullopt,
995 std::nullopt,
996 std::nullopt,
997 std::nullopt,
998 std::nullopt,
999 std::nullopt,
1001 }
1002
1003 // Insufficient reserve, XRP/MPT
1004 {
1005 Env env(*this);
1006 auto const startingXrp = reserve(env, 4) + env.current()->fees().base * 4;
1007 env.fund(XRP(10'000), gw_);
1008 env.fund(XRP(10'000), alice_);
1009 env.fund(startingXrp, carol_);
1010
1011 MPT const btc = MPTTester(
1012 {.env = env,
1013 .issuer = gw_,
1014 .holders = {alice_, carol_},
1015 .transferFee = 1'500,
1016 .pay = 40'000});
1017
1018 env(offer(carol_, XRP(100), btc(101)));
1019 AMM ammAlice(env, alice_, XRP(1000), btc(1000));
1020 ammAlice.deposit(
1021 carol_,
1022 XRP(100),
1023 std::nullopt,
1024 std::nullopt,
1025 std::nullopt,
1027
1028 env(offer(carol_, XRP(100), btc(102)));
1029 ammAlice.deposit(
1030 carol_,
1031 btc(100),
1032 std::nullopt,
1033 std::nullopt,
1034 std::nullopt,
1036 }
1037
1038 // Invalid min
1039 testAMM(
1040 [&](AMM& ammAlice, Env& env) {
1041 // min tokens can't be <= zero
1042 ammAlice.deposit(carol_, 0, XRP(100), tfSingleAsset, Ter(temBAD_AMM_TOKENS));
1043 ammAlice.deposit(carol_, -1, XRP(100), tfSingleAsset, Ter(temBAD_AMM_TOKENS));
1044 ammAlice.deposit(
1045 carol_,
1046 0,
1047 XRP(100),
1048 MPT(ammAlice[1])(100),
1049 std::nullopt,
1050 tfTwoAsset,
1051 std::nullopt,
1052 std::nullopt,
1053 std::nullopt,
1055
1056 // min amounts can't be <= zero
1057 ammAlice.deposit(
1058 carol_,
1059 1'000,
1060 XRP(0),
1061 MPT(ammAlice[1])(100),
1062 std::nullopt,
1063 tfTwoAsset,
1064 std::nullopt,
1065 std::nullopt,
1066 std::nullopt,
1068 ammAlice.deposit(
1069 carol_,
1070 1'000,
1071 XRP(100),
1072 MPT(ammAlice[1])(-1),
1073 std::nullopt,
1074 tfTwoAsset,
1075 std::nullopt,
1076 std::nullopt,
1077 std::nullopt,
1079 ammAlice.deposit(
1080 carol_,
1081 1'000,
1082 XRP(100),
1083 badMPT(),
1084 std::nullopt,
1085 tfTwoAsset,
1086 std::nullopt,
1087 std::nullopt,
1088 std::nullopt,
1089 Ter(temBAD_MPT));
1090 },
1091 {{XRP(10'000), gAmmmpt(10'000)}});
1092
1093 // Min deposit
1094 testAMM(
1095 [&](AMM& ammAlice, Env& env) {
1096 // Equal deposit by tokens
1097 ammAlice.deposit(
1098 carol_,
1099 1'000'000,
1100 XRP(1'000),
1101 MPT(ammAlice[1])(1'001),
1102 std::nullopt,
1103 tfLPToken,
1104 std::nullopt,
1105 std::nullopt,
1106 std::nullopt,
1108 ammAlice.deposit(
1109 carol_,
1110 1'000'000,
1111 XRP(1'001),
1112 MPT(ammAlice[1])(1'000),
1113 std::nullopt,
1114 tfLPToken,
1115 std::nullopt,
1116 std::nullopt,
1117 std::nullopt,
1119 // Equal deposit by asset
1120 ammAlice.deposit(
1121 carol_,
1122 100'001,
1123 XRP(100),
1124 MPT(ammAlice[1])(100),
1125 std::nullopt,
1126 tfTwoAsset,
1127 std::nullopt,
1128 std::nullopt,
1129 std::nullopt,
1131 // Single deposit by asset
1132 ammAlice.deposit(
1133 carol_,
1134 488'090,
1135 XRP(1'000),
1136 std::nullopt,
1137 std::nullopt,
1138 tfSingleAsset,
1139 std::nullopt,
1140 std::nullopt,
1141 std::nullopt,
1143 },
1144 {{XRP(1000), gAmmmpt(1000)}});
1145
1146 // OutstandingAmount > MaximumAmount
1147 {
1148 Env env{*this};
1149 env.fund(XRP(1'000), gw_, alice_);
1150
1151 MPTTester const btc({.env = env, .issuer = gw_, .holders = {alice_}, .maxAmt = 100});
1152
1153 AMM amm(env, gw_, XRP(100), btc(90));
1154 // OutstandingAmount is 90, issuer issues 1 over MaximumAmount
1155 amm.deposit(
1156 DepositArg{.account = gw_, .asset1In = btc(11), .err = Ter(tecUNFUNDED_AMM)});
1157
1158 env(pay(gw_, alice_, btc(10)));
1159
1160 // OutstandingAmount is 100, issuer issues 10 over MaximumAmount
1161 amm.deposit(
1162 DepositArg{.account = gw_, .asset1In = btc(10), .err = Ter(tecUNFUNDED_AMM)});
1163 // This is fine - alice transfers 10 to AMM. OutstandingAmount
1164 // is 100.
1165 amm.deposit(DepositArg{.account = alice_, .asset1In = btc(10)});
1166 }
1167 }
1168
1169 void
1171 {
1172 testcase("Deposit");
1173
1174 using namespace jtx;
1175
1176 // Equal deposit: 1000000 tokens. XRP/MPT AMM.
1177 testAMM(
1178 [&](AMM& ammAlice, Env& env) {
1179 XRPAmount const baseFee{env.current()->fees().base};
1180 auto carolXRP = env.balance(carol_, XRP);
1181 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
1182
1183 ammAlice.deposit(carol_, 1'000'000);
1184 BEAST_EXPECT(ammAlice.expectBalances(
1185 XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0}));
1186
1187 // carol deposited 1000 XRP and pays the transaction fee
1188 env.require(Balance(carol_, carolXRP - XRP(1000) - drops(baseFee)));
1189 // carol deposited 1000 MPT
1190 env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(1000)));
1191 },
1192 {{XRP(10'000), gAmmmpt(10'000)}});
1193
1194 // single deposit MPT with eprice
1195 testAMM(
1196 [&](AMM& ammAlice, Env& env) {
1197 ammAlice.deposit(
1198 carol_,
1199 MPT(ammAlice[1])(100),
1200 std::nullopt,
1201 STAmount{ammAlice.lptIssue(), 1, -1},
1202 tfLimitLPToken);
1203
1204 // although we should use lptoken unit for eprice,
1205 // we don't check the currency any more, we just use
1206 // the value
1207 ammAlice.deposit(
1208 carol_,
1209 MPT(ammAlice[1])(100),
1210 std::nullopt,
1211 STAmount{USD, 1, -1},
1212 tfLimitLPToken);
1213 },
1214 {{USD(10'000'000), gAmmmpt(10'000)}});
1215
1216 // Equal deposit: 1000000 tokens. IOU/MPT combination
1217 {
1218 auto test = [&](auto&& issue1, auto&& issue2) {
1219 Env env(*this);
1220 env.fund(XRP(30'000), alice_, carol_, gw_);
1221 env.close();
1222 auto const usd = issue1(
1223 {.env = env,
1224 .token = "USD",
1225 .issuer = gw_,
1226 .holders = {alice_, carol_},
1227 .limit = 1'000'000});
1228 auto const btc = issue2(
1229 {.env = env,
1230 .token = "BTC",
1231 .issuer = gw_,
1232 .holders = {alice_, carol_},
1233 .limit = 1'000'000});
1234 env(pay(gw_, alice_, btc(50000)));
1235 env(pay(gw_, carol_, btc(50000)));
1236 env(pay(gw_, alice_, usd(50000)));
1237 env(pay(gw_, carol_, usd(50000)));
1238 env.close();
1239
1240 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
1241 auto carolBTC = env.balance(carol_, btc);
1242 auto carolUSD = env.balance(carol_, usd);
1243
1244 ammAlice.deposit(carol_, 1'000);
1245 BEAST_EXPECT(ammAlice.expectBalances(btc(11'000), usd(11'000), IOUAmount(11'000)));
1246
1247 env.require(Balance(carol_, carolBTC - btc(1000)));
1248 env.require(Balance(carol_, carolUSD - usd(1000)));
1249 };
1251 }
1252
1253 // Deposit 100MPT/100XRP. XRP/MPT AMM.
1254 testAMM(
1255 [&](AMM& ammAlice, Env& env) {
1256 XRPAmount const baseFee{env.current()->fees().base};
1257 auto carolXRP = env.balance(carol_, XRP);
1258 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
1259
1260 ammAlice.deposit(carol_, MPT(ammAlice[1])(100), XRP(100));
1261 BEAST_EXPECT(ammAlice.expectBalances(
1262 XRP(10'100), MPT(ammAlice[1])(10'100), IOUAmount{10'100'000, 0}));
1263
1264 env.require(Balance(carol_, carolXRP - XRP(100) - drops(baseFee)));
1265 env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(100)));
1266 },
1267 {{XRP(10'000), gAmmmpt(10'000)}});
1268
1269 // Deposit MPT/IOU combination
1270 {
1271 auto test = [&](auto&& issue1, auto&& issue2) {
1272 Env env(*this);
1273 env.fund(XRP(30'000), alice_, carol_, gw_);
1274 env.close();
1275 auto const usd = issue1(
1276 {.env = env,
1277 .token = "USD",
1278 .issuer = gw_,
1279 .holders = {alice_, carol_},
1280 .limit = 1'000'000});
1281 auto const btc = issue2(
1282 {.env = env,
1283 .token = "BTC",
1284 .issuer = gw_,
1285 .holders = {alice_, carol_},
1286 .limit = 1'000'000});
1287 env(pay(gw_, alice_, btc(50000)));
1288 env(pay(gw_, carol_, btc(50000)));
1289 env(pay(gw_, alice_, usd(50000)));
1290 env(pay(gw_, carol_, usd(50000)));
1291 env.close();
1292
1293 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
1294 auto carolBTC = env.balance(carol_, btc);
1295 auto carolUSD = env.balance(carol_, usd);
1296 ammAlice.deposit(carol_, btc(100), usd(100));
1297
1298 BEAST_EXPECT(ammAlice.expectBalances(btc(10'100), usd(10'100), IOUAmount(10'100)));
1299
1300 env.require(Balance(carol_, carolBTC - btc(100)));
1301 env.require(Balance(carol_, carolUSD - usd(100)));
1302 };
1304 }
1305
1306 // Equal limit deposit.
1307 // Try to deposit 200MPT/100XRP. Is truncated to 100MPT/100XRP.
1308 testAMM(
1309 [&](AMM& ammAlice, Env& env) {
1310 XRPAmount const baseFee{env.current()->fees().base};
1311 auto carolXRP = env.balance(carol_, XRP);
1312 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
1313
1314 ammAlice.deposit(carol_, MPT(ammAlice[1])(200), XRP(100));
1315 BEAST_EXPECT(ammAlice.expectBalances(
1316 XRP(10'100), MPT(ammAlice[1])(10'100), IOUAmount{10'100'000, 0}));
1317
1318 env.require(Balance(carol_, carolXRP - XRP(100) - drops(baseFee)));
1319 env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(100)));
1320 },
1321 {{XRP(10'000), gAmmmpt(10'000)}});
1322
1323 // Equal limit deposit. MPT/IOU combination.
1324 {
1325 auto test = [&](auto&& issue1, auto&& issue2) {
1326 Env env(*this);
1327 env.fund(XRP(30'000), alice_, carol_, gw_);
1328 env.close();
1329 auto const usd = issue1(
1330 {.env = env,
1331 .token = "USD",
1332 .issuer = gw_,
1333 .holders = {alice_, carol_},
1334 .limit = 1'000'000});
1335 auto const btc = issue2(
1336 {.env = env,
1337 .token = "BTC",
1338 .issuer = gw_,
1339 .holders = {alice_, carol_},
1340 .limit = 1'000'000});
1341 env(pay(gw_, alice_, btc(50000)));
1342 env(pay(gw_, carol_, btc(50000)));
1343 env(pay(gw_, alice_, usd(50000)));
1344 env(pay(gw_, carol_, usd(50000)));
1345 env.close();
1346
1347 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
1348 auto carolBTC = env.balance(carol_, btc);
1349 auto carolUSD = env.balance(carol_, usd);
1350 ammAlice.deposit(carol_, btc(200), usd(100));
1351 BEAST_EXPECT(ammAlice.expectBalances(btc(10'100), usd(10'100), IOUAmount(10'100)));
1352
1353 env.require(Balance(carol_, carolBTC - btc(100)));
1354 env.require(Balance(carol_, carolUSD - usd(100)));
1355 };
1357 }
1358
1359 // Single deposit: 1000 MPT into MPT/XRP
1360 testAMM(
1361 [&](AMM& ammAlice, Env& env) {
1362 XRPAmount const baseFee{env.current()->fees().base};
1363 auto carolXRP = env.balance(carol_, XRP);
1364 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
1365
1366 ammAlice.deposit(carol_, MPT(ammAlice[1])(1000));
1367 BEAST_EXPECT(ammAlice.expectBalances(
1368 XRP(10'000), MPT(ammAlice[1])(11'000), IOUAmount{10'488'088'48170151, -8}));
1369
1370 env.require(Balance(carol_, carolXRP - drops(baseFee)));
1371 env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(1000)));
1372 },
1373 {{XRP(10'000), gAmmmpt(10'000)}});
1374
1375 // Single deposit: 1000 XRP into MPT/XRP
1376 testAMM(
1377 [&](AMM& ammAlice, Env& env) {
1378 XRPAmount const baseFee{env.current()->fees().base};
1379 auto carolXRP = env.balance(carol_, XRP);
1380 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
1381
1382 ammAlice.deposit(carol_, XRP(1000));
1383 BEAST_EXPECT(ammAlice.expectBalances(
1384 XRP(11'000), MPT(ammAlice[1])(10'000), IOUAmount{10'488'088'48170151, -8}));
1385
1386 env.require(Balance(carol_, carolXRP - XRP(1000) - drops(baseFee)));
1387 env.require(Balance(carol_, carolMPT));
1388 },
1389 {{XRP(10'000), gAmmmpt(10'000)}});
1390
1391 // Single deposit: 1000 MPT0 into MPT/MPT
1392 testAMM(
1393 [&](AMM& ammAlice, Env& env) {
1394 auto carolMPT0 = env.balance(carol_, MPT(ammAlice[0]));
1395 auto carolMPT1 = env.balance(carol_, MPT(ammAlice[1]));
1396
1397 ammAlice.deposit(carol_, MPT(ammAlice[0])(1000));
1398 BEAST_EXPECT(ammAlice.expectBalances(
1399 MPT(ammAlice[0])(11'000),
1400 MPT(ammAlice[1])(10'000),
1401 IOUAmount{10'488'08848170151, -11}));
1402
1403 env.require(Balance(carol_, carolMPT0 - MPT(ammAlice[0])(1000)));
1404 env.require(Balance(carol_, carolMPT1));
1405 },
1406 {{gAmmmpt(10'000), gAmmmpt(10'000)}});
1407
1408 // Single deposit: 1000 MPT into MPT/IOU
1409 testAMM(
1410 [&](AMM& ammAlice, Env& env) {
1411 auto carolMPT = env.balance(carol_, MPT(ammAlice[0]));
1412 auto carolUSD = env.balance(carol_, USD);
1413
1414 ammAlice.deposit(carol_, MPT(ammAlice[0])(1000));
1415 BEAST_EXPECT(ammAlice.expectBalances(
1416 MPT(ammAlice[0])(11'000), USD(10'000), IOUAmount{10'488'08848170151, -11}));
1417
1418 env.require(Balance(carol_, carolMPT - MPT(ammAlice[0])(1000)));
1419 env.require(Balance(carol_, carolUSD));
1420 },
1421 {{gAmmmpt(10'000), USD(10'000)}});
1422
1423 // Single deposit: 1000 IOU into MPT/IOU
1424 testAMM(
1425 [&](AMM& ammAlice, Env& env) {
1426 auto carolMPT = env.balance(carol_, MPT(ammAlice[0]));
1427 auto carolUSD = env.balance(carol_, USD);
1428
1429 ammAlice.deposit(carol_, USD(1000));
1430 BEAST_EXPECT(ammAlice.expectBalances(
1431 MPT(ammAlice[0])(10'000),
1432 STAmount{USD, UINT64_C(10999'99999999999), -11},
1433 IOUAmount{10'488'08848170151, -11}));
1434
1435 env.require(Balance(carol_, carolMPT));
1436 env.require(
1437 Balance(carol_, carolUSD - STAmount{USD, UINT64_C(999'99999999999), -11}));
1438 },
1439 {{gAmmmpt(10'000), USD(10'000)}});
1440
1441 // Single deposit: 100000 tokens worth of MPT into XRP/MPT
1442 testAMM(
1443 [&](AMM& ammAlice, Env& env) {
1444 XRPAmount const baseFee{env.current()->fees().base};
1445 auto carolXRP = env.balance(carol_, XRP);
1446 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
1447
1448 ammAlice.deposit(carol_, 100'000, MPT(ammAlice[1])(205));
1449 BEAST_EXPECT(ammAlice.expectBalances(
1450 XRP(10'000), MPT(ammAlice[1])(10'201), IOUAmount{10'100'000, 0}));
1451
1452 env.require(Balance(carol_, carolXRP - drops(baseFee)));
1453 env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(201)));
1454 },
1455 {{XRP(10'000), gAmmmpt(10'000)}});
1456
1457 // Single deposit: 100000 tokens worth of XRP into XRP/MPT
1458 testAMM(
1459 [&](AMM& ammAlice, Env& env) {
1460 XRPAmount const baseFee{env.current()->fees().base};
1461 auto carolXRP = env.balance(carol_, XRP);
1462 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
1463
1464 ammAlice.deposit(carol_, 100'000, XRP(205));
1465 BEAST_EXPECT(ammAlice.expectBalances(
1466 XRP(10'201), MPT(ammAlice[1])(10'000), IOUAmount{10'100'000, 0}));
1467
1468 env.require(Balance(carol_, carolXRP - XRP(201) - drops(baseFee)));
1469 env.require(Balance(carol_, carolMPT));
1470 },
1471 {{XRP(10'000), gAmmmpt(10'000)}});
1472
1473 // Single deposit: 100 tokens worth of MPT/IOU into pool of MPT/IOU
1474 // combination
1475 {
1476 auto test = [&](auto&& issue1, auto&& issue2) {
1477 Env env(*this);
1478 env.fund(XRP(30'000), alice_, carol_, gw_);
1479 env.close();
1480 auto const usd = issue1(
1481 {.env = env,
1482 .token = "USD",
1483 .issuer = gw_,
1484 .holders = {alice_, carol_},
1485 .limit = 1'000'000});
1486 auto const btc = issue2(
1487 {.env = env,
1488 .token = "BTC",
1489 .issuer = gw_,
1490 .holders = {alice_, carol_},
1491 .limit = 1'000'000});
1492 env(pay(gw_, alice_, btc(50000)));
1493 env(pay(gw_, carol_, btc(50000)));
1494 env(pay(gw_, alice_, usd(50000)));
1495 env(pay(gw_, carol_, usd(50000)));
1496 env.close();
1497
1498 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
1499 auto carolBTC = env.balance(carol_, btc);
1500 auto carolUSD = env.balance(carol_, usd);
1501
1502 ammAlice.deposit(carol_, 100, usd(205));
1503 auto deltaUSD = [&]() {
1504 if constexpr (std::is_same_v<MPT, std::decay_t<decltype(usd)>>)
1505 return usd(202);
1506 return usd(201);
1507 }();
1508 BEAST_EXPECT(ammAlice.expectBalances(
1509 btc(10'000), usd(10'000) + deltaUSD, IOUAmount{10'100, 0}));
1510
1511 env.require(Balance(carol_, carolBTC));
1512 env.require(Balance(carol_, carolUSD - deltaUSD));
1513 };
1515 }
1516
1517 // Single deposit with EP not exceeding specified:
1518 // 100 MPT with EP not to exceed 0.1 (AssetIn/TokensOut)
1519 // for XRP/MPT
1520 testAMM(
1521 [&](AMM& ammAlice, Env& env) {
1522 ammAlice.deposit(
1523 carol_,
1524 MPT(ammAlice[1])(100),
1525 std::nullopt,
1526 STAmount{ammAlice.lptIssue(), 1, -1});
1527
1528 BEAST_EXPECT(ammAlice.expectBalances(
1529 XRP(10'000), MPT(ammAlice[1])(10100), IOUAmount{10'049'875'62112089, -8}));
1530 },
1531 {{XRP(10'000), gAmmmpt(10'000)}});
1532
1533 // Single deposit with EP not exceeding specified:
1534 // 100 MPT with EP not to exceed 0.002004 (AssetIn/TokensOut)
1535 testAMM(
1536 [&](AMM& ammAlice, Env& env) {
1537 XRPAmount const baseFee{env.current()->fees().base};
1538 auto carolXRP = env.balance(carol_, XRP);
1539 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
1540
1541 ammAlice.deposit(
1542 carol_,
1543 MPT(ammAlice[1])(100),
1544 std::nullopt,
1545 STAmount{ammAlice.lptIssue(), 2004, -6});
1546
1547 BEAST_EXPECT(ammAlice.expectBalances(
1548 XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8}));
1549
1550 env.require(Balance(carol_, carolXRP - drops(baseFee)));
1551 env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81)));
1552 },
1553 {{XRP(10'000), gAmmmpt(10'000)}});
1554
1555 // Single deposit with EP not exceeding specified:
1556 // 0 MPT with EP not to exceed 0.002004 (AssetIn/TokensOut)
1557 testAMM(
1558 [&](AMM& ammAlice, Env& env) {
1559 XRPAmount const baseFee{env.current()->fees().base};
1560 auto carolXRP = env.balance(carol_, XRP);
1561 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
1562
1563 ammAlice.deposit(
1564 carol_,
1565 MPT(ammAlice[1])(0),
1566 std::nullopt,
1567 STAmount{ammAlice.lptIssue(), 2004, -6});
1568
1569 BEAST_EXPECT(ammAlice.expectBalances(
1570 XRP(10'000), MPT(ammAlice[1])(10'081), IOUAmount{10'039'920'31840891, -8}));
1571
1572 env.require(Balance(carol_, carolXRP - drops(baseFee)));
1573 env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(81)));
1574 },
1575 {{XRP(10'000), gAmmmpt(10'000)}});
1576
1577 // Single deposit with EP not exceeding specified:
1578 // 100 MPT with EP not to exceed 0.1 (AssetIn/TokensOut)
1579 // for IOU/MPT
1580 testAMM(
1581 [&](AMM& ammAlice, Env& env) {
1582 ammAlice.deposit(
1583 carol_,
1584 MPT(ammAlice[1])(100),
1585 std::nullopt,
1586 STAmount{ammAlice.lptIssue(), 1, -1});
1587
1588 BEAST_EXPECT(ammAlice.expectBalances(
1589 USD(10'000'000'000),
1590 MPT(ammAlice[1])(10100),
1591 IOUAmount{10'049'875'62112089, -8}));
1592 },
1593 {{USD(10'000'000'000), gAmmmpt(10'000)}});
1594
1595 // Single deposit with EP not exceeding specified:
1596 // 100 IOU with EP not to exceed 0.1 (AssetIn/TokensOut)
1597 // for IOU/MPT
1598 testAMM(
1599 [&](AMM& ammAlice, Env& env) {
1600 ammAlice.deposit(carol_, USD(100), std::nullopt, STAmount{USD, 1, -1});
1601
1602 BEAST_EXPECT(ammAlice.expectBalances(
1603 MPT(ammAlice[1])(10'000'000'000),
1604 USD(10100),
1605 IOUAmount{10'049'875'62112089, -8}));
1606 },
1607 {{USD(10'000), gAmmmpt(10'000'000'000)}});
1608
1609 // Single deposit with EP not exceeding specified:
1610 // 100 IOU with EP not to exceed 0.1 (AssetIn/TokensOut)
1611 // for MPT/MPT
1612 testAMM(
1613 [&](AMM& ammAlice, Env& env) {
1614 ammAlice.deposit(
1615 carol_,
1616 MPT(ammAlice[0])(100),
1617 std::nullopt,
1618 STAmount{ammAlice.lptIssue(), 1, -1});
1619
1620 BEAST_EXPECT(ammAlice.expectBalances(
1621 MPT(ammAlice[1])(10'000'000'000),
1622 MPT(ammAlice[0])(10100),
1623 IOUAmount{10'049'875'62112089, -8}));
1624 },
1625 {{gAmmmpt(10'000), gAmmmpt(10'000'000'000)}});
1626
1627 // MPT/MPT with transfer fee
1628 {
1629 Env env(*this);
1630 env.fund(XRP(30'000), gw_, alice_, carol_);
1631 env.close();
1632
1633 MPT const btc = MPTTester(
1634 {.env = env,
1635 .issuer = gw_,
1636 .holders = {alice_, carol_},
1637 .transferFee = 25'000,
1638 .pay = 400'000});
1639
1640 MPT const usd = MPTTester(
1641 {.env = env,
1642 .issuer = gw_,
1643 .holders = {alice_, carol_},
1644 .transferFee = 25'000,
1645 .pay = 400'000});
1646
1647 AMM ammAlice(env, alice_, usd(200'000), btc(5));
1648 BEAST_EXPECT(ammAlice.expectBalances(usd(200'000), btc(5), IOUAmount{1000, 0}));
1649
1650 ammAlice.deposit(carol_, 100, std::nullopt, std::nullopt);
1651 BEAST_EXPECT(ammAlice.expectBalances(usd(220'000), btc(6), IOUAmount{1100, 0}));
1652 }
1653
1654 // IOU/MPT with transfer fee
1655 {
1656 Env env(*this);
1657 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
1658 env.close();
1659
1660 env(rate(gw_, 1.25));
1661 env.close();
1662
1663 MPT const btc = MPTTester(
1664 {.env = env,
1665 .issuer = gw_,
1666 .holders = {alice_, bob_, carol_},
1667 .transferFee = 25'000,
1668 .pay = 400'000});
1669
1670 auto const usd = gw_["USD"];
1671 env.trust(usd(1000000), alice_);
1672 env(pay(gw_, alice_, usd(1000000)));
1673 env.trust(usd(1000000), bob_);
1674 env(pay(gw_, bob_, usd(1000000)));
1675 env.trust(usd(1000000), carol_);
1676 env(pay(gw_, carol_, usd(1000000)));
1677 env.close();
1678
1679 // IOU/MPT
1680 AMM ammAlice(env, alice_, usd(200'000), btc(5));
1681 BEAST_EXPECT(ammAlice.expectBalances(usd(200'000), btc(5), IOUAmount{1000, 0}));
1682 ammAlice.deposit(carol_, 100, std::nullopt, std::nullopt);
1683 BEAST_EXPECT(ammAlice.expectBalances(usd(220'000), btc(6), IOUAmount{1100, 0}));
1684
1685 MPT const eth = MPTTester(
1686 {.env = env,
1687 .issuer = gw_,
1688 .holders = {alice_, bob_, carol_},
1689 .transferFee = 25'000,
1690 .pay = 400'000});
1691
1692 // MPT/IOU
1693 AMM ammBob(env, bob_, eth(20'000), usd(0.5));
1694 BEAST_EXPECT(ammBob.expectBalances(eth(20'000), usd(0.5), IOUAmount{100, 0}));
1695 ammBob.deposit(carol_, 10);
1696 BEAST_EXPECT(ammBob.expectBalances(eth(22'000), usd(0.55), IOUAmount{110, 0}));
1697 }
1698
1699 // Tiny deposits for IOU/MPT
1700 testAMM(
1701 [&](AMM& ammAlice, Env& env) {
1702 // tiny amount causes MPT to deposit rounded to 0
1703 ammAlice.deposit(carol_, IOUAmount{1, -3}, std::nullopt, std::nullopt);
1704 BEAST_EXPECT(ammAlice.expectBalances(
1705 STAmount{USD, UINT64_C(10'000'001), -3},
1706 MPT(ammAlice[1])(10'001),
1707 IOUAmount{10'000'001, -3}));
1708
1709 ammAlice.deposit(carol_, IOUAmount{1});
1710 BEAST_EXPECT(ammAlice.expectBalances(
1711 STAmount{USD, UINT64_C(10'001'001), -3},
1712 MPT(ammAlice[1])(10'003),
1713 IOUAmount{10'001'001, -3}));
1714 },
1715 {{USD(10'000), gAmmmpt(10'000)}});
1716 testAMM(
1717 [&](AMM& ammAlice, Env& env) {
1718 ammAlice.deposit(carol_, STAmount{USD, 1, -10});
1719 BEAST_EXPECT(ammAlice.expectBalances(
1720 STAmount{USD, UINT64_C(10'000'00000000008), -11},
1721 MPT(ammAlice[1])(10'000),
1722 IOUAmount{1'000'000'000000004, -11}));
1723
1724 ammAlice.deposit(carol_, MPT(ammAlice[1])(1));
1725 BEAST_EXPECT(ammAlice.expectBalances(
1726 STAmount{USD, UINT64_C(10'000'00000000008), -11},
1727 MPT(ammAlice[1])(10'001),
1728 IOUAmount{10'000'49998750066, -11}));
1729 },
1730 {{USD(10'000), gAmmmpt(10'000)}});
1731
1732 // Tiny deposits for XRP/MPT
1733 testAMM(
1734 [&](AMM& ammAlice, Env& env) {
1735 ammAlice.deposit(carol_, XRPAmount{1});
1736 BEAST_EXPECT(ammAlice.expectBalances(
1737 XRPAmount{10'000'000'001},
1738 MPT(ammAlice[1])(10'000),
1739 IOUAmount{1'000'000'000049999, -8}));
1740 },
1741 {{XRP(10'000), gAmmmpt(10'000)}});
1742 testAMM(
1743 [&](AMM& ammAlice, Env& env) {
1744 ammAlice.deposit(carol_, MPT(ammAlice[1])(1));
1745 BEAST_EXPECT(ammAlice.expectBalances(
1746 XRP(10'000), MPT(ammAlice[1])(10'001), IOUAmount{10'000'499'98750062, -8}));
1747 },
1748 {{XRP(10'000), gAmmmpt(10'000)}});
1749
1750 // Tiny deposits for MPT/MPT
1751 testAMM(
1752 [&](AMM& ammAlice, Env& env) {
1753 ammAlice.deposit(carol_, MPT(ammAlice[1])(1));
1754
1755 BEAST_EXPECT(ammAlice.expectBalances(
1756 MPT(ammAlice[0])(10'000),
1757 MPT(ammAlice[1])(10'001),
1758 IOUAmount{1'000'049'998750062, -11}));
1759 },
1760 {{gAmmmpt(10'000), gAmmmpt(10'000)}});
1761
1762 // MPT Issuer create/deposit
1763 {
1764 Env env(*this);
1765 env.fund(XRP(30'000), gw_);
1766 env.close();
1767
1768 MPT const btc = MPTTester({.env = env, .issuer = gw_, .holders = {}});
1769
1770 AMM ammGw(env, gw_, XRP(10'000), btc(10'000'000'000));
1771 BEAST_EXPECT(
1772 ammGw.expectBalances(XRP(10'000), btc(10'000'000'000), IOUAmount{10'000'000'000}));
1773
1774 ammGw.deposit(gw_, 1'000'000);
1775 BEAST_EXPECT(
1776 ammGw.expectBalances(XRP(10'001), btc(10'001000000), IOUAmount{10'001000000}));
1777
1778 ammGw.deposit(gw_, btc(1'000000000));
1779 BEAST_EXPECT(ammGw.expectBalances(
1780 XRP(10'001), btc(11'001000000), IOUAmount{1048'908'961731188, -5}));
1781 }
1782
1783 // Issuer deposit in MPT/MPT pool
1784 testAMM(
1785 [&](AMM& ammAlice, Env& env) {
1786 ammAlice.deposit(gw_, 1'000'000);
1787 BEAST_EXPECT(ammAlice.expectBalances(
1788 MPT(ammAlice[0])(1'010'000),
1789 MPT(ammAlice[1])(1'010'000),
1790 IOUAmount{1'010'000}));
1791
1792 ammAlice.deposit(gw_, MPT(ammAlice[0])(1000));
1793 BEAST_EXPECT(ammAlice.expectBalances(
1794 MPT(ammAlice[0])(1'010'999),
1795 MPT(ammAlice[1])(1'010'000),
1796 IOUAmount{1'010'499'376546071, -9}));
1797 },
1798 {{gAmmmpt(10'000), gAmmmpt(10'000)}});
1799
1800 // Issuer deposit in MPT/XRP pool
1801 testAMM(
1802 [&](AMM& ammAlice, Env& env) {
1803 ammAlice.deposit(gw_, 1'000'000);
1804 BEAST_EXPECT(ammAlice.expectBalances(
1805 XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000}));
1806 ammAlice.deposit(gw_, MPT(ammAlice[1])(1'000));
1807 BEAST_EXPECT(ammAlice.expectBalances(
1808 XRP(11'000), MPT(ammAlice[1])(12'000), IOUAmount{11'489'125'29307605, -8}));
1809 },
1810 {{XRP(10'000), gAmmmpt(10'000)}});
1811
1812 // Equal deposit by tokens MPT/XRP
1813 testAMM(
1814 [&](AMM& ammAlice, Env& env) {
1815 ammAlice.deposit(
1816 carol_,
1817 1'000'000,
1818 XRP(1'000),
1819 MPT(ammAlice[1])(1'000),
1820 std::nullopt,
1821 tfLPToken,
1822 std::nullopt,
1823 std::nullopt);
1824 BEAST_EXPECT(ammAlice.expectBalances(
1825 XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0}));
1826 },
1827 {{XRP(10'000), gAmmmpt(10'000)}});
1828
1829 // Equal deposit by tokens MPT/IOU combination
1830 {
1831 auto test = [&](auto&& issue1, auto&& issue2) {
1832 Env env(*this);
1833 env.fund(XRP(30'000), alice_, carol_, gw_);
1834 env.close();
1835 auto const usd = issue1(
1836 {.env = env,
1837 .token = "USD",
1838 .issuer = gw_,
1839 .holders = {alice_, carol_},
1840 .limit = 1'000'000});
1841 auto const btc = issue2(
1842 {.env = env,
1843 .token = "BTC",
1844 .issuer = gw_,
1845 .holders = {alice_, carol_},
1846 .limit = 1'000'000});
1847 env(pay(gw_, alice_, btc(50000)));
1848 env(pay(gw_, carol_, btc(50000)));
1849 env(pay(gw_, alice_, usd(50000)));
1850 env(pay(gw_, carol_, usd(50000)));
1851 env.close();
1852
1853 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
1854 auto carolBTC = env.balance(carol_, btc);
1855 auto carolUSD = env.balance(carol_, usd);
1856
1857 ammAlice.deposit(
1858 carol_,
1859 1'000,
1860 usd(1'000),
1861 btc(1'000),
1862 std::nullopt,
1863 tfLPToken,
1864 std::nullopt,
1865 std::nullopt);
1866 BEAST_EXPECT(ammAlice.expectBalances(usd(11'000), btc(11'000), IOUAmount{11'000}));
1867
1868 env.require(Balance(carol_, carolBTC - btc(1000)));
1869 env.require(Balance(carol_, carolUSD - usd(1000)));
1870 };
1872 }
1873
1874 // Equal deposit by asset XRP/MPT
1875 testAMM(
1876 [&](AMM& ammAlice, Env& env) {
1877 ammAlice.deposit(
1878 carol_,
1879 1'000'000,
1880 XRP(1'000),
1881 MPT(ammAlice[1])(1'000),
1882 std::nullopt,
1883 tfTwoAsset,
1884 std::nullopt,
1885 std::nullopt);
1886 BEAST_EXPECT(ammAlice.expectBalances(
1887 XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0}));
1888 },
1889 {{XRP(10'000), gAmmmpt(10'000)}});
1890
1891 // Equal deposit by asset IOU/MPT combination
1892 {
1893 auto test = [&](auto&& issue1, auto&& issue2) {
1894 Env env(*this);
1895 env.fund(XRP(30'000), alice_, carol_, gw_);
1896 env.close();
1897 auto const usd = issue1(
1898 {.env = env,
1899 .token = "USD",
1900 .issuer = gw_,
1901 .holders = {alice_, carol_},
1902 .limit = 1'000'000});
1903 auto const btc = issue2(
1904 {.env = env,
1905 .token = "BTC",
1906 .issuer = gw_,
1907 .holders = {alice_, carol_},
1908 .limit = 1'000'000});
1909 env(pay(gw_, alice_, btc(50000)));
1910 env(pay(gw_, carol_, btc(50000)));
1911 env(pay(gw_, alice_, usd(50000)));
1912 env(pay(gw_, carol_, usd(50000)));
1913 env.close();
1914 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
1915 auto carolBTC = env.balance(carol_, btc);
1916 auto carolUSD = env.balance(carol_, usd);
1917
1918 ammAlice.deposit(
1919 carol_,
1920 1'000,
1921 usd(1'000),
1922 btc(1'000),
1923 std::nullopt,
1924 tfTwoAsset,
1925 std::nullopt,
1926 std::nullopt);
1927 BEAST_EXPECT(
1928 ammAlice.expectBalances(usd(11'000), btc(11'000), IOUAmount{11'000, 0}));
1929
1930 env.require(Balance(carol_, carolBTC - btc(1000)));
1931 env.require(Balance(carol_, carolUSD - usd(1000)));
1932 };
1934 }
1935
1936 // Single deposit XRP by asset MPT/XRP
1937 testAMM(
1938 [&](AMM& ammAlice, Env& env) {
1939 ammAlice.deposit(
1940 carol_,
1941 488'088,
1942 XRP(1'000),
1943 std::nullopt,
1944 std::nullopt,
1945 tfSingleAsset,
1946 std::nullopt,
1947 std::nullopt);
1948 BEAST_EXPECT(ammAlice.expectBalances(
1949 XRP(11'000), MPT(ammAlice[1])(10'000), IOUAmount{10'488'088'48170151, -8}));
1950 },
1951 {{XRP(10'000), gAmmmpt(10'000)}});
1952
1953 // Single deposit MPT by asset MPT/XRP
1954 testAMM(
1955 [&](AMM& ammAlice, Env& env) {
1956 ammAlice.deposit(
1957 carol_,
1958 488'088,
1959 MPT(ammAlice[1])(1'000),
1960 std::nullopt,
1961 std::nullopt,
1962 tfSingleAsset,
1963 std::nullopt,
1964 std::nullopt);
1965 BEAST_EXPECT(ammAlice.expectBalances(
1966 XRP(10'000), MPT(ammAlice[1])(11'000), IOUAmount{10'488'088'48170151, -8}));
1967 },
1968 {{XRP(10'000), gAmmmpt(10'000)}});
1969
1970 // Single deposit IOU by asset MPT/IOU
1971 testAMM(
1972 [&](AMM& ammAlice, Env& env) {
1973 ammAlice.deposit(
1974 carol_,
1975 488,
1976 USD(1'000),
1977 std::nullopt,
1978 std::nullopt,
1979 tfSingleAsset,
1980 std::nullopt,
1981 std::nullopt);
1982 BEAST_EXPECT(ammAlice.expectBalances(
1983 STAmount{USD, UINT64_C(10'999'99999999999), -11},
1984 MPT(ammAlice[1])(10'000),
1985 IOUAmount{10'488'08848170151, -11}));
1986 },
1987 {{USD(10'000), gAmmmpt(10'000)}});
1988
1989 // Single deposit MPT by asset MPT/IOU
1990 testAMM(
1991 [&](AMM& ammAlice, Env& env) {
1992 ammAlice.deposit(
1993 carol_,
1994 488,
1995 MPT(ammAlice[1])(1'000),
1996 std::nullopt,
1997 std::nullopt,
1998 tfSingleAsset,
1999 std::nullopt,
2000 std::nullopt);
2001 BEAST_EXPECT(ammAlice.expectBalances(
2002 USD(10'000), MPT(ammAlice[1])(11'000), IOUAmount{10'488'088'48170151, -11}));
2003 },
2004 {{USD(10'000), gAmmmpt(10'000)}});
2005
2006 // Single deposit MPT by asset MPT/MPT
2007 testAMM(
2008 [&](AMM& ammAlice, Env& env) {
2009 ammAlice.deposit(
2010 carol_,
2011 488,
2012 MPT(ammAlice[1])(1'000),
2013 std::nullopt,
2014 std::nullopt,
2015 tfSingleAsset,
2016 std::nullopt,
2017 std::nullopt);
2018 BEAST_EXPECT(ammAlice.expectBalances(
2019 MPT(ammAlice[0])(10'000),
2020 MPT(ammAlice[1])(11'000),
2021 IOUAmount{10'488'088'48170151, -11}));
2022 },
2023 {{gAmmmpt(10'000), gAmmmpt(10'000)}});
2024 }
2025
2026 void
2028 {
2029 testcase("Invalid AMMWithdraw");
2030
2031 using namespace jtx;
2032 auto const all = testableAmendments();
2033
2034 testAMM(
2035 [&](AMM& ammAlice, Env& env) {
2036 WithdrawArg const args{
2037 .asset1Out = XRP(100),
2038 .err = Ter(tecAMM_BALANCE),
2039 };
2040 ammAlice.withdraw(args);
2041 },
2042 {{XRP(99), gAmmmpt(99)}});
2043
2044 testAMM(
2045 [&](AMM& ammAlice, Env& env) {
2046 WithdrawArg const args{
2047 .asset1Out = MPT(ammAlice[1])(100),
2048 .err = Ter(tecAMM_BALANCE),
2049 };
2050 ammAlice.withdraw(args);
2051 },
2052 {{XRP(99), gAmmmpt(99)}});
2053
2054 {
2055 Env env{*this};
2056 env.fund(XRP(30'000), gw_, alice_, bob_);
2057 env.close();
2058 // alice is authorized to hold gw MPT, bob is not authorized
2059 MPTTester const btc(
2060 {.env = env,
2061 .issuer = gw_,
2062 .holders = {alice_},
2063 .pay = 30'000,
2064 .flags = tfMPTRequireAuth | kMptDexFlags,
2065 .authHolder = true});
2066 AMM ammAlice(env, alice_, XRP(10'000), btc(10'000));
2067 WithdrawArg const args{
2068 .account = bob_,
2069 .asset1Out = btc(100),
2070 .err = Ter(tecNO_AUTH),
2071 };
2072 ammAlice.withdraw(args);
2073 }
2074
2075 testAMM(
2076 [&](AMM& ammAlice, Env& env) {
2077 MPTTester const btc(
2078 {.env = env,
2079 .issuer = gw_,
2080 .holders = {alice_, carol_},
2081 .pay = 2'000,
2082 .flags = tfMPTCanLock | kMptDexFlags});
2083
2084 // Invalid tokens
2085 ammAlice.withdraw(alice_, 0, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
2086 ammAlice.withdraw(
2087 alice_, IOUAmount{-1}, std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
2088
2089 // Mismatched token, invalid Asset1Out issue
2090 ammAlice.withdraw(
2091 alice_, btc(100), std::nullopt, std::nullopt, Ter(temBAD_AMM_TOKENS));
2092 ammAlice.withdraw(
2093 alice_, MPT(badMPT())(100), std::nullopt, std::nullopt, Ter(temBAD_MPT));
2094
2095 // Mismatched token, invalid Asset2Out issue
2096 ammAlice.withdraw(alice_, XRP(100), btc(100), std::nullopt, Ter(temBAD_AMM_TOKENS));
2097
2098 // Mismatched token, Asset1Out.issue == Asset2Out.issue
2099 ammAlice.withdraw(
2100 alice_,
2101 MPT(ammAlice[1])(100),
2102 MPT(ammAlice[1])(100),
2103 std::nullopt,
2105
2106 // Invalid amount value
2107 ammAlice.withdraw(
2108 alice_, MPT(ammAlice[1])(0), std::nullopt, std::nullopt, Ter(temBAD_AMOUNT));
2109 ammAlice.withdraw(
2110 alice_, MPT(ammAlice[1])(-100), std::nullopt, std::nullopt, Ter(temBAD_AMOUNT));
2111 ammAlice.withdraw(
2112 alice_, MPT(ammAlice[1])(10), std::nullopt, IOUAmount{-1}, Ter(temBAD_AMOUNT));
2113
2114 // Invalid amount/token value, withdraw all tokens from one side
2115 // of the pool.
2116 ammAlice.withdraw(
2117 alice_,
2118 MPT(ammAlice[1])(10'000),
2119 std::nullopt,
2120 std::nullopt,
2122 ammAlice.withdraw(
2123 alice_, XRP(10'000), std::nullopt, std::nullopt, Ter(tecAMM_BALANCE));
2124 ammAlice.withdraw(
2125 alice_,
2126 std::nullopt,
2127 MPT(ammAlice[1])(0),
2128 std::nullopt,
2129 std::nullopt,
2130 tfOneAssetWithdrawAll,
2131 std::nullopt,
2132 std::nullopt,
2134
2135 // Bad MPT
2136 ammAlice.withdraw(
2137 alice_, XRP(100), MPT(badMPT())(100), std::nullopt, Ter(temBAD_MPT));
2138
2139 // Specified MPToken doesn't match the pool assets
2140 ammAlice.withdraw(
2141 alice_, XRP(100), MPT(noMPT())(100), std::nullopt, Ter(temBAD_AMM_TOKENS));
2142
2143 // Invalid Account
2144 Account bad("bad");
2145 env.memoize(bad);
2146 ammAlice.withdraw(
2147 bad,
2148 1'000'000,
2149 std::nullopt,
2150 std::nullopt,
2151 std::nullopt,
2152 std::nullopt,
2153 std::nullopt,
2154 Seq(1),
2156
2157 // Invalid AMM
2158 ammAlice.withdraw(
2159 alice_,
2160 1'000,
2161 std::nullopt,
2162 std::nullopt,
2163 std::nullopt,
2164 std::nullopt,
2165 {{MPT(ammAlice[1]), GBP}},
2166 std::nullopt,
2167 Ter(terNO_AMM));
2168
2169 // Carol is not a Liquidity Provider
2170 ammAlice.withdraw(carol_, 10'000, std::nullopt, std::nullopt, Ter(tecAMM_BALANCE));
2171 },
2172 {{XRP(10'000), gAmmmpt(10'000)}});
2173
2174 testAMM(
2175 [&](AMM& ammAlice, Env& env) {
2176 // Withdraw entire one side of the pool.
2177 // Pre-fixAMMv1_3:
2178 // Equal withdraw but due to MPT precision limit,
2179 // this results in full withdraw of MPT pool only,
2180 // while leaving a tiny amount in USD pool.
2181 // Post-fixAMMv1_3:
2182 // Most of the pool is withdrawn with remaining tiny amounts
2183 auto err = env.enabled(fixAMMv1_3) ? Ter(tesSUCCESS) : Ter(tecAMM_BALANCE);
2184 ammAlice.withdraw(
2185 alice_, IOUAmount{9'999'999'9999, -4}, std::nullopt, std::nullopt, err);
2186 if (env.enabled(fixAMMv1_3))
2187 {
2188 BEAST_EXPECT(ammAlice.expectBalances(
2189 MPT(ammAlice[0])(1), STAmount{USD, 1, -7}, IOUAmount{1, -4}));
2190 }
2191 },
2192 {{gAmmmpt(10'000'000'000), USD(10'000)}},
2193 0,
2194 std::nullopt,
2195 {all, all - fixAMMv1_3});
2196
2197 testAMM(
2198 [&](AMM& ammAlice, Env& env) {
2199 // Similar to above with even smaller remaining amount
2200 // Pre-fixAMMv1_3: results in full withdraw of MPT pool only,
2201 // returning tecAMM_BALANCE. Post-fixAMMv1_3: most of the pool
2202 // is withdrawn with remaining tiny amounts
2203 auto err = env.enabled(fixAMMv1_3) ? Ter(tesSUCCESS) : Ter(tecAMM_BALANCE);
2204 ammAlice.withdraw(
2205 alice_, IOUAmount{9'999'999'999999999, -9}, std::nullopt, std::nullopt, err);
2206 if (env.enabled(fixAMMv1_3))
2207 {
2208 BEAST_EXPECT(ammAlice.expectBalances(
2209 MPT(ammAlice[0])(1), STAmount{USD, 1, -11}, IOUAmount{1, -8}));
2210 }
2211 },
2212 {{gAmmmpt(10'000'000'000), USD(10'000)}},
2213 0,
2214 std::nullopt,
2215 {all, all - fixAMMv1_3});
2216
2217 // Invalid AMM
2218 testAMM(
2219 [&](AMM& ammAlice, Env& env) {
2220 ammAlice.withdrawAll(alice_);
2221 ammAlice.withdraw(alice_, 10'000, std::nullopt, std::nullopt, Ter(terNO_AMM));
2222 },
2223 {{XRP(10'000), gAmmmpt(10'000)}});
2224
2225 // MPTokenIssuance object doesn't exist
2226 {
2227 Env env{*this};
2228 env.fund(XRP(30'000), gw_, alice_);
2229 env.close();
2230 MPT const btc =
2231 MPTTester({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 30'000});
2232
2233 AMM ammAlice(env, alice_, XRP(10'000), btc(10'000));
2234
2235 ammAlice.withdraw(
2237 .account = alice_,
2238 .asset1Out = MPT(gw_, 1'000)(10),
2239 .assets = {{XRP, MPT(gw_, 1'000)}},
2240 .err = Ter(terNO_AMM)});
2241 }
2242
2243 // MPTRequireAuth flag is set and the account is not authorized
2244 {
2245 Env env{*this};
2246 env.fund(XRP(30'000), gw_, alice_);
2247 env.close();
2248 auto btcm = MPTTester(
2249 {.env = env,
2250 .issuer = gw_,
2251 .holders = {alice_},
2252 .pay = 30'000,
2253 .flags = tfMPTRequireAuth | kMptDexFlags,
2254 .authHolder = true});
2255 MPT const btc = btcm;
2256
2257 AMM amm(env, alice_, XRP(10'000), btc(10'000));
2258
2259 btcm.authorize({.account = gw_, .holder = alice_, .flags = tfMPTUnauthorize});
2260
2261 amm.withdraw(
2263 .account = alice_,
2264 .asset1Out = btc(100),
2265 .assets = {{XRP, btc}},
2266 .err = Ter(tecNO_AUTH)});
2267 }
2268
2269 // MPTCanTransfer is not set and the account is not the issuer of MPT.
2270 // The issuer can create the AMM, and an existing LP token holder can
2271 // still withdraw.
2272 {
2273 Env env{*this};
2274 env.fund(XRP(30'000), gw_, alice_);
2275 env.close();
2276 auto btc = MPTTester(
2277 {.env = env,
2278 .issuer = gw_,
2279 .holders = {alice_},
2280 .pay = 30'000,
2281 .flags = tfMPTCanTrade,
2282 .authHolder = true});
2283
2284 AMM amm(env, gw_, XRP(10'000), btc(10'000));
2285
2286 auto const lpIssue = amm.lptIssue();
2287 env.trust(STAmount{lpIssue, 20'000'000}, alice_);
2288 env.close();
2289 env(pay(gw_, alice_, LPToken(1'000'000).tokens(lpIssue)));
2290 env.close();
2291
2292 amm.withdraw(
2293 WithdrawArg{.account = alice_, .asset1Out = btc(100), .assets = {{XRP, btc}}});
2294 }
2295
2296 // Globally locked MPT
2297 // MPTLocked flag is set and the account is not the issuer of MPT
2298 {
2299 Env env{*this};
2300 env.fund(XRP(30'000), gw_, alice_);
2301 env.close();
2302 MPTTester btc(
2303 {.env = env,
2304 .issuer = gw_,
2305 .holders = {alice_},
2306 .pay = 30'000,
2307 .flags = tfMPTCanLock | kMptDexFlags,
2308 .authHolder = true});
2309
2310 AMM ammAlice(env, alice_, XRP(10'000), btc(10'000));
2311 btc.set({.flags = tfMPTLock});
2312
2313 ammAlice.withdraw(
2314 alice_, MPT(ammAlice[1])(100), std::nullopt, std::nullopt, Ter(tecLOCKED));
2315 ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
2316
2317 // can single withdraw the other asset
2318 ammAlice.withdraw({.account = alice_, .asset1Out = XRP(100)});
2319 }
2320
2321 // Individually frozen (AMM) account with MPT/MPT AMM
2322 {
2323 Env env{*this};
2324 env.fund(XRP(10'000), gw_, alice_);
2325 env.close();
2326 MPTTester btc(
2327 {.env = env,
2328 .issuer = gw_,
2329 .holders = {alice_},
2330 .pay = 30'000,
2331 .flags = tfMPTCanLock | kMptDexFlags});
2332 MPTTester const usd(
2333 {.env = env,
2334 .issuer = gw_,
2335 .holders = {alice_},
2336 .pay = 40'000,
2337 .flags = tfMPTCanLock | kMptDexFlags});
2338
2339 AMM ammAlice(env, alice_, usd(10'000), btc(10'000));
2340
2341 // Alice's BTC is locked
2342 btc.set({.holder = alice_, .flags = tfMPTLock});
2343 ammAlice.withdraw(alice_, 1000, std::nullopt, std::nullopt, Ter(tecLOCKED));
2344 ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecLOCKED));
2345
2346 // can withdraw the other asset
2347 ammAlice.withdraw(alice_, usd(100), std::nullopt, std::nullopt);
2348
2349 // Unlock and then alice can withdraw
2350 btc.set({.holder = alice_, .flags = tfMPTUnlock});
2351 ammAlice.withdraw(alice_, 1000, std::nullopt, std::nullopt);
2352 ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt);
2353 ammAlice.withdraw(alice_, usd(100), std::nullopt, std::nullopt);
2354 }
2355
2356 // Individually lock MPT or freeze IOU (AMM)
2357 {
2358 Env env{*this};
2359 fund(env, gw_, {alice_}, {USD(20'000)}, Fund::All);
2360 MPTTester btc(
2361 {.env = env,
2362 .issuer = gw_,
2363 .holders = {alice_},
2364 .pay = 30'000,
2365 .flags = tfMPTCanLock | kMptDexFlags});
2366
2367 AMM ammAlice(env, alice_, USD(10'000), btc(10'000));
2368
2369 // Alice's BTC is locked
2370 btc.set({.holder = alice_, .flags = tfMPTLock});
2371
2372 ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
2373 ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecLOCKED));
2374 // can still single withdraw the unlocked other asset
2375 ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt);
2376
2377 // Unlock alice's BTC
2378 btc.set({.holder = alice_, .flags = tfMPTUnlock});
2379
2380 // Now alice can withdraw
2381 ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt);
2382 ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt);
2383 ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt);
2384
2385 // Individually lock MPT BTC (AMM) account
2386 btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock});
2387
2388 // Can withdraw non-frozen token USD
2389 ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt);
2390
2391 // Can not withdraw locked token BTC
2392 ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecLOCKED));
2393 ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt, Ter(tecLOCKED));
2394
2395 // Unlock AMM MPT
2396 btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTUnlock});
2397 // Can withdraw
2398 ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt);
2399 ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt);
2400
2401 // Individually frozen AMM
2402 env(trust(
2403 gw_, STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0}, tfSetFreeze));
2404 env.close();
2405
2406 // Can withdraw non-locked token BTC
2407 ammAlice.withdraw(alice_, btc(100), std::nullopt, std::nullopt);
2408
2409 // Can not withdraw frozen token USD
2410 ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt, Ter(tecFROZEN));
2411 ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt, Ter(tecFROZEN));
2412
2413 // Unfreeze
2414 env(trust(
2415 gw_,
2416 STAmount{Issue{gw_["USD"].currency, ammAlice.ammAccount()}, 0},
2417 tfClearFreeze));
2418 env.close();
2419
2420 // Can withdraw
2421 ammAlice.withdraw(alice_, 1'000, std::nullopt, std::nullopt);
2422 ammAlice.withdraw(alice_, USD(100), std::nullopt, std::nullopt);
2423 }
2424
2425 // Carol withdraws more than she owns
2426 testAMM(
2427 [&](AMM& ammAlice, Env&) {
2428 // Single deposit of 100000 worth of tokens
2429 // which is 10% of the pool. Carol is LP now.
2430 ammAlice.deposit(carol_, 1'000'000);
2431 BEAST_EXPECT(ammAlice.expectBalances(
2432 XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0}));
2433
2434 ammAlice.withdraw(
2435 carol_, 2'000'000, std::nullopt, std::nullopt, Ter(tecAMM_INVALID_TOKENS));
2436 BEAST_EXPECT(ammAlice.expectBalances(
2437 XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0}));
2438 },
2439 {{XRP(10'000), gAmmmpt(10'000)}});
2440
2441 // Withdraw with EPrice limit. Fails to withdraw, calculated tokens
2442 // to withdraw are 0.
2443 testAMM(
2444 [&](AMM& ammAlice, Env& env) {
2445 ammAlice.deposit(carol_, 1'000'000);
2446 auto const err =
2447 env.enabled(fixAMMv1_3) ? Ter(tecAMM_INVALID_TOKENS) : Ter(tecAMM_FAILED);
2448 ammAlice.withdraw(
2449 carol_, MPT(ammAlice[1])(100), std::nullopt, IOUAmount{500, 0}, err);
2450 },
2451 {{XRP(10'000), gAmmmpt(10'000)}},
2452 0,
2453 std::nullopt,
2454 {all, all - fixAMMv1_3});
2455
2456 // Withdraw with EPrice limit. Fails to withdraw, calculated tokens
2457 // to withdraw are greater than the LP shares.
2458 testAMM(
2459 [&](AMM& ammAlice, Env&) {
2460 ammAlice.deposit(carol_, 1'000'000);
2461 ammAlice.withdraw(
2462 carol_,
2463 MPT(ammAlice[1])(100),
2464 std::nullopt,
2465 IOUAmount{600, 0},
2467 },
2468 {{XRP(10'000), gAmmmpt(10'000)}});
2469
2470 // Withdraw with EPrice limit. Fails to withdraw, amount1
2471 // to withdraw is less than 1700 MPT.
2472 testAMM(
2473 [&](AMM& ammAlice, Env&) {
2474 ammAlice.deposit(carol_, 1'000'000);
2475 ammAlice.withdraw(
2476 carol_,
2477 MPT(ammAlice[1])(1'700),
2478 std::nullopt,
2479 IOUAmount{520, 0},
2481 },
2482 {{XRP(10'000), gAmmmpt(10'000)}});
2483
2484 // Deposit/Withdraw the same amount with the trading fee
2485 testAMM(
2486 [&](AMM& ammAlice, Env&) {
2487 ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000));
2488 ammAlice.withdraw(
2489 carol_,
2490 MPT(ammAlice[1])(1'000),
2491 std::nullopt,
2492 std::nullopt,
2494 },
2495 {{XRP(10'000), gAmmmpt(10'000)}},
2496 1'000);
2497 testAMM(
2498 [&](AMM& ammAlice, Env&) {
2499 ammAlice.deposit(carol_, XRP(1'000));
2500 ammAlice.withdraw(
2501 carol_, XRP(1'000), std::nullopt, std::nullopt, Ter(tecAMM_INVALID_TOKENS));
2502 },
2503 {{XRP(10'000), gAmmmpt(10'000)}},
2504 1'000);
2505
2506 // Deposit/Withdraw the same amount fails due to the tokens adjustment
2507 testAMM(
2508 [&](AMM& ammAlice, Env&) {
2509 ammAlice.deposit(carol_, STAmount{USD, 1, -6});
2510 ammAlice.withdraw(
2511 carol_,
2512 STAmount{USD, 1, -6},
2513 std::nullopt,
2514 std::nullopt,
2516 },
2517 {{gAmmmpt(10'000'000'000), USD(10'000)}});
2518
2519 // Withdraw close to one side of the pool. Account's LP tokens
2520 // are rounded to all LP tokens.
2521 testAMM(
2522 [&](AMM& ammAlice, Env&) {
2523 ammAlice.withdraw(
2524 alice_,
2525 STAmount{MPT(ammAlice[1]), UINT64_C(9'999'999999999999), -12},
2526 std::nullopt,
2527 std::nullopt,
2529 },
2530 {{XRP(10'000), gAmmmpt(10'000)}});
2531
2532 // Tiny withdraw
2533 testAMM(
2534 [&](AMM& ammAlice, Env&) {
2535 // XRP amount to withdraw is 0
2536 ammAlice.withdraw(
2537 alice_, IOUAmount{1, -5}, std::nullopt, std::nullopt, Ter(tecAMM_FAILED));
2538 // Calculated tokens to withdraw are 0
2539 ammAlice.withdraw(
2540 alice_,
2541 std::nullopt,
2542 STAmount{USD, 1, -11},
2543 std::nullopt,
2545 ammAlice.deposit(carol_, STAmount{USD, 1, -10});
2546 ammAlice.withdraw(
2547 carol_,
2548 std::nullopt,
2549 STAmount{USD, 1, -9},
2550 std::nullopt,
2552 ammAlice.withdraw(
2553 carol_,
2554 std::nullopt,
2555 MPT(ammAlice[0])(1),
2556 std::nullopt,
2558 },
2559 {{gAmmmpt(10'000'000'000), USD(10'000)}});
2560 }
2561
2562 void
2564 {
2565 testcase("Withdraw");
2566
2567 using namespace jtx;
2568
2569 // Equal withdrawal by Carol: 1'000'000 of tokens, 10% of the current
2570 // pool
2571 testAMM(
2572 [&](AMM& ammAlice, Env& env) {
2573 // XRP/MPT
2574 XRPAmount const baseFee{env.current()->fees().base};
2575 auto carolXRP = env.balance(carol_, XRP);
2576 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
2577
2578 // Single deposit of 1'000'000 worth of tokens,
2579 // which is 10% of the pool. Carol is LP now.
2580 ammAlice.deposit(carol_, 1'000'000);
2581 BEAST_EXPECT(ammAlice.expectBalances(
2582 XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{11'000'000, 0}));
2583 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{1'000'000, 0}));
2584 env.require(Balance(carol_, carolMPT - MPT(ammAlice[1])(1'000)));
2585 env.require(Balance(carol_, carolXRP - XRP(1'000) - drops(baseFee)));
2586
2587 // Carol withdraws all tokens
2588 ammAlice.withdraw(carol_, 1'000'000);
2589 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero())));
2590 env.require(Balance(carol_, carolMPT));
2591 env.require(Balance(carol_, carolXRP - drops(2 * baseFee)));
2592 },
2593 {{XRP(10'000), gAmmmpt(10'000)}});
2594
2595 // Equal withdrawal by tokens 1000000, 10%
2596 // of the current pool, XRP/MPT
2597 testAMM(
2598 [&](AMM& ammAlice, Env&) {
2599 // XRP/MPT
2600 ammAlice.withdraw(alice_, 1'000'000);
2601 BEAST_EXPECT(ammAlice.expectBalances(
2602 XRP(9'000), MPT(ammAlice[1])(9'000), IOUAmount{9'000'000, 0}));
2603 },
2604 {{XRP(10'000), gAmmmpt(10'000)}});
2605
2606 // Equal withdrawal by tokens, 10% of the current pool, IOU/MPT
2607 // combination
2608 {
2609 auto test = [&](auto&& issue1, auto&& issue2) {
2610 Env env(*this);
2611 env.fund(XRP(30'000), alice_, gw_);
2612 env.close();
2613 auto const usd = issue1(
2614 {.env = env,
2615 .token = "USD",
2616 .issuer = gw_,
2617 .holders = {alice_},
2618 .limit = 1'000'000});
2619 auto const btc = issue2(
2620 {.env = env,
2621 .token = "BTC",
2622 .issuer = gw_,
2623 .holders = {alice_},
2624 .limit = 1'000'000});
2625 env(pay(gw_, alice_, btc(50000)));
2626 env(pay(gw_, alice_, usd(50000)));
2627 env.close();
2628 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
2629 auto aliceBTC = env.balance(alice_, btc);
2630 auto aliceUSD = env.balance(alice_, usd);
2631 ammAlice.withdraw(alice_, 1'000);
2632 BEAST_EXPECT(ammAlice.expectBalances(btc(9'000), usd(9'000), IOUAmount(9'000)));
2633 env.require(Balance(alice_, aliceBTC + btc(1000)));
2634 env.require(Balance(alice_, aliceUSD + usd(1000)));
2635 };
2637 }
2638
2639 // Equal withdrawal with a limit. Withdraw XRP200.
2640 // If proportional withdraw of MPT is less than 100
2641 // then withdraw that amount, otherwise withdraw MPT100
2642 // and proportionally withdraw XRP. It's the latter
2643 // in this case - XRP100/MPT100.
2644 testAMM(
2645 [&](AMM& ammAlice, Env&) {
2646 // XRP/MPT
2647 ammAlice.withdraw(alice_, XRP(200), MPT(ammAlice[1])(100));
2648 BEAST_EXPECT(ammAlice.expectBalances(
2649 XRP(9'900), MPT(ammAlice[1])(9'900), IOUAmount{9'900'000, 0}));
2650 },
2651 {{XRP(10'000), gAmmmpt(10'000)}});
2652
2653 // Equal withdrawal with a limit. XRP100/MPT200 truncated to
2654 // XRP100/MPT100
2655 testAMM(
2656 [&](AMM& ammAlice, Env&) {
2657 // XRP/MPT
2658 ammAlice.withdraw(alice_, XRP(100), MPT(ammAlice[1])(200));
2659 BEAST_EXPECT(ammAlice.expectBalances(
2660 XRP(9'900), MPT(ammAlice[1])(9'900), IOUAmount{9'900'000, 0}));
2661 },
2662 {{XRP(10'000), gAmmmpt(10'000)}});
2663
2664 // Equal withdrawal with a limit. IOU/MPT combination.
2665 {
2666 auto test = [&](auto&& issue1, auto&& issue2) {
2667 Env env(*this);
2668 env.fund(XRP(30'000), alice_, gw_);
2669 env.close();
2670 auto const usd = issue1(
2671 {.env = env,
2672 .token = "USD",
2673 .issuer = gw_,
2674 .holders = {alice_},
2675 .limit = 1'000'000});
2676 auto const btc = issue2(
2677 {.env = env,
2678 .token = "BTC",
2679 .issuer = gw_,
2680 .holders = {alice_},
2681 .limit = 1'000'000});
2682 env(pay(gw_, alice_, btc(50000)));
2683 env(pay(gw_, alice_, usd(50000)));
2684 env.close();
2685 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
2686 auto aliceBTC = env.balance(alice_, btc);
2687 auto aliceUSD = env.balance(alice_, usd);
2688 ammAlice.withdraw(alice_, btc(200), usd(100));
2689 BEAST_EXPECT(ammAlice.expectBalances(btc(9'900), usd(9'900), IOUAmount(9'900)));
2690 env.require(Balance(alice_, aliceBTC + btc(100)));
2691 env.require(Balance(alice_, aliceUSD + usd(100)));
2692 };
2694 }
2695
2696 // Single withdrawal by amount
2697 testAMM(
2698 [&](AMM& ammAlice, Env&) {
2699 // single withdraw XRP from XRP/MPT
2700 ammAlice.withdraw(alice_, XRP(1'000));
2701 BEAST_EXPECT(ammAlice.expectBalances(
2702 XRPAmount(9000'000001),
2703 MPT(ammAlice[1])(10'000),
2704 IOUAmount{9'486'832'98050514, -8}));
2705 },
2706 {{XRP(10'000), gAmmmpt(10'000)}});
2707 testAMM(
2708 [&](AMM& ammAlice, Env&) {
2709 // single withdraw MPT from XRP/MPT
2710 ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000));
2711 BEAST_EXPECT(ammAlice.expectBalances(
2712 XRP(10000), MPT(ammAlice[1])(9001), IOUAmount{9'486'832'98050514, -8}));
2713 },
2714 {{XRP(10'000), gAmmmpt(10'000)}});
2715 testAMM(
2716 [&](AMM& ammAlice, Env&) {
2717 // single withdraw IOU from IOU/MPT
2718 ammAlice.withdraw(alice_, USD(1'000));
2719 BEAST_EXPECT(ammAlice.expectBalances(
2720 STAmount{USD, UINT64_C(9000'000000000004), -12},
2721 MPT(ammAlice[1])(10'000),
2722 IOUAmount{9486'83298050514, -11}));
2723 },
2724 {{USD(10'000), gAmmmpt(10'000)}});
2725 testAMM(
2726 [&](AMM& ammAlice, Env&) {
2727 // single withdraw MPT from IOU/MPT
2728 ammAlice.withdraw(alice_, MPT(ammAlice[1])(1'000));
2729 BEAST_EXPECT(ammAlice.expectBalances(
2730 USD(10'000), MPT(ammAlice[1])(9001), IOUAmount{9486'83298050514, -11}));
2731 },
2732 {{USD(10'000), gAmmmpt(10'000)}});
2733 testAMM(
2734 [&](AMM& ammAlice, Env&) {
2735 // single withdraw MPT from MPT/MPT
2736 ammAlice.withdraw(alice_, MPT(ammAlice[0])(1'000));
2737 BEAST_EXPECT(ammAlice.expectBalances(
2738 MPT(ammAlice[0])(9001),
2739 MPT(ammAlice[1])(10'000),
2740 IOUAmount{9486'83298050514, -11}));
2741 },
2742 {{gAmmmpt(10'000), gAmmmpt(10'000)}});
2743
2744 // Single withdrawal MPT by tokens 10000. XRP/MPT
2745 testAMM(
2746 [&](AMM& ammAlice, Env&) {
2747 ammAlice.withdraw(alice_, 10'000, MPT(ammAlice[1])(0));
2748 BEAST_EXPECT(ammAlice.expectBalances(
2749 XRP(10'000), MPT(ammAlice[1])(9981), IOUAmount{9'990'000, 0}));
2750 },
2751 {{XRP(10'000), gAmmmpt(10'000)}});
2752
2753 // Single withdrawal XRP by tokens 10000. XRP/MPT
2754 testAMM(
2755 [&](AMM& ammAlice, Env&) {
2756 ammAlice.withdraw(alice_, 10'000, XRP(0));
2757 BEAST_EXPECT(ammAlice.expectBalances(
2758 XRPAmount(9980010000), MPT(ammAlice[1])(10'000), IOUAmount{9'990'000, 0}));
2759 },
2760 {{XRP(10'000), gAmmmpt(10'000)}});
2761
2762 // Single withdrawal by tokens 10000. MPT/IOU combination.
2763 {
2764 auto test = [&](auto&& issue1, auto&& issue2) {
2765 Env env(*this);
2766 env.fund(XRP(30'000), alice_, gw_);
2767 env.close();
2768 auto const usd = issue1(
2769 {.env = env,
2770 .token = "USD",
2771 .issuer = gw_,
2772 .holders = {alice_},
2773 .limit = 1'000'000});
2774 auto const btc = issue2(
2775 {.env = env,
2776 .token = "BTC",
2777 .issuer = gw_,
2778 .holders = {alice_},
2779 .limit = 1'000'000});
2780 env(pay(gw_, alice_, btc(50000)));
2781 env(pay(gw_, alice_, usd(50000)));
2782 env.close();
2783 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
2784 auto aliceBTC = env.balance(alice_, btc);
2785 auto aliceUSD = env.balance(alice_, usd);
2786 ammAlice.withdraw(alice_, 1000, btc(0));
2787 BEAST_EXPECT(ammAlice.expectBalances(usd(10'000), btc(8100), IOUAmount{9000, 0}));
2788 env.require(Balance(alice_, aliceBTC + btc(1900)));
2789 env.require(Balance(alice_, aliceUSD));
2790 };
2792 }
2793
2794 // Withdraw all tokens.
2795 testAMM(
2796 [&](AMM& ammAlice, Env& env) {
2797 env(trust(carol_, STAmount{ammAlice.lptIssue(), 10'000}));
2798 // Can SetTrust only for AMM LP tokens
2799 env(trust(carol_, STAmount{Issue{EUR.currency, ammAlice.ammAccount()}, 10'000}),
2801 env.close();
2802 ammAlice.withdrawAll(alice_);
2803 BEAST_EXPECT(!ammAlice.ammExists());
2804
2805 BEAST_EXPECT(!env.le(keylet::ownerDir(ammAlice.ammAccount())));
2806
2807 // Can create AMM for the XRP/MPT pair
2808 AMM const ammCarol(env, carol_, XRP(10'000), MPT(ammAlice[1])(10'000));
2809 BEAST_EXPECT(ammCarol.expectBalances(
2810 XRP(10'000), MPT(ammAlice[1])(10'000), IOUAmount{10'000'000, 0}));
2811 },
2812 {{XRP(10'000), gAmmmpt(10'000)}});
2813
2814 // Single deposit 1000MPT, withdraw all tokens in MPT from XRP/MPT
2815 testAMM(
2816 [&](AMM& ammAlice, Env& env) {
2817 ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000));
2818 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
2819 BEAST_EXPECT(ammAlice.expectBalances(
2820 XRP(10'000), MPT(ammAlice[1])(10'001), IOUAmount{10'000'000, 0}));
2821 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero())));
2822 },
2823 {{XRP(10'000), gAmmmpt(10'000)}});
2824
2825 // Single deposit 1000MPT, withdraw all tokens in XRP from XRP/MPT
2826 testAMM(
2827 [&](AMM& ammAlice, Env&) {
2828 ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000));
2829 ammAlice.withdrawAll(carol_, XRP(0));
2830 BEAST_EXPECT(ammAlice.expectBalances(
2831 XRPAmount(9'090'909'091), MPT(ammAlice[1])(11000), IOUAmount{10'000'000, 0}));
2832 },
2833 {{XRP(10'000), gAmmmpt(10'000)}});
2834
2835 // Single deposit 1000MPT, withdraw all tokens in MPT from USD/MPT
2836 testAMM(
2837 [&](AMM& ammAlice, Env& env) {
2838 // USD/MPT
2839 ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000));
2840 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
2841 BEAST_EXPECT(ammAlice.expectBalances(
2842 USD(10'000), MPT(ammAlice[1])(10'001), IOUAmount{10'000, 0}));
2843 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero())));
2844 },
2845 {{USD(10'000), gAmmmpt(10'000)}});
2846
2847 // Single deposit 1000USD, withdraw all tokens in USD from USD/MPT
2848 testAMM(
2849 [&](AMM& ammAlice, Env& env) {
2850 // USD/MPT
2851 ammAlice.deposit(carol_, USD(1'000));
2852 ammAlice.withdrawAll(carol_, USD(0));
2853 BEAST_EXPECT(ammAlice.expectBalances(
2854 USD(10'000), MPT(ammAlice[1])(10'000), IOUAmount{10'000, 0}));
2855 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero())));
2856 },
2857 {{USD(10'000), gAmmmpt(10'000)}});
2858
2859 // Single deposit 1000MPT, withdraw all tokens in MPT from MPT/MPT
2860 testAMM(
2861 [&](AMM& ammAlice, Env& env) {
2862 // MPT/MPT
2863 ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000));
2864 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
2865 BEAST_EXPECT(ammAlice.expectBalances(
2866 MPT(ammAlice[0])(10'000), MPT(ammAlice[1])(10'001), IOUAmount{10'000, 0}));
2867 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount(beast::Zero())));
2868 },
2869 {{gAmmmpt(10'000), gAmmmpt(10'000)}});
2870
2871 // Single deposit 1000MPT, withdraw all tokens in MPT
2872 testAMM(
2873 [&](AMM& ammAlice, Env&) {
2874 ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000));
2875 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
2876 BEAST_EXPECT(ammAlice.expectBalances(
2877 XRP(10'000), MPT(ammAlice[1])(10001), IOUAmount{10'000'000, 0}));
2878 },
2879 {{XRP(10'000), gAmmmpt(10'000)}});
2880
2881 // Single deposit 1000MPT, withdraw all tokens in USD
2882 testAMM(
2883 [&](AMM& ammAlice, Env&) {
2884 ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000));
2885 ammAlice.withdrawAll(carol_, USD(0));
2886 BEAST_EXPECT(ammAlice.expectBalances(
2887 STAmount{USD, UINT64_C(9'090'9090909091), -10},
2888 MPT(ammAlice[1])(11000),
2889 IOUAmount{10'000, 0}));
2890 },
2891 {{USD(10'000), gAmmmpt(10'000)}});
2892
2893 // Single deposit 1000USD, withdraw all tokens in MPT
2894 testAMM(
2895 [&](AMM& ammAlice, Env&) {
2896 ammAlice.deposit(carol_, USD(1'000));
2897 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
2898 BEAST_EXPECT(ammAlice.expectBalances(
2899 STAmount{USD, UINT64_C(10'999'99999999999), -11},
2900 MPT(ammAlice[1])(9091),
2901 IOUAmount{10'000, 0}));
2902 },
2903 {{USD(10'000), gAmmmpt(10'000)}});
2904
2905 // Single deposit/withdraw by the same account
2906 testAMM(
2907 [&](AMM& ammAlice, Env&) {
2908 auto lpTokens = ammAlice.deposit(carol_, USD(1'000));
2909 ammAlice.withdraw(carol_, lpTokens, USD(0));
2910 lpTokens = ammAlice.deposit(carol_, STAmount(USD, 1, -6));
2911 ammAlice.withdraw(carol_, lpTokens, USD(0));
2912 lpTokens = ammAlice.deposit(carol_, MPT(ammAlice[0])(1));
2913 ammAlice.withdraw(carol_, lpTokens, MPT(ammAlice[0])(0));
2914 BEAST_EXPECT(ammAlice.expectBalances(
2915 MPT(ammAlice[0])(10'000'000'001), USD(10'000), ammAlice.tokens()));
2916 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
2917 },
2918 {{gAmmmpt(10'000'000'000), USD(10'000)}});
2919 testAMM(
2920 [&](AMM& ammAlice, Env&) {
2921 auto const& btc = MPT(ammAlice[1]);
2922 auto lpTokens = ammAlice.deposit(carol_, btc(1'000));
2923 ammAlice.withdraw(carol_, lpTokens, btc(0));
2924 lpTokens = ammAlice.deposit(carol_, btc(1));
2925 ammAlice.withdraw(carol_, lpTokens, btc(0));
2926 lpTokens = ammAlice.deposit(carol_, btc(1));
2927 ammAlice.withdraw(carol_, lpTokens, btc(0));
2928 BEAST_EXPECT(ammAlice.expectBalances(btc(10'003), XRP(10'000), ammAlice.tokens()));
2929 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
2930 },
2931 {{XRP(10'000), gAmmmpt(10'000)}});
2932
2933 // Single deposit by different accounts and then withdraw
2934 // in reverse.
2935 testAMM(
2936 [&](AMM& ammAlice, Env&) {
2937 auto const carolTokens = ammAlice.deposit(carol_, MPT(ammAlice[1])(1'000));
2938 auto const aliceTokens = ammAlice.deposit(alice_, MPT(ammAlice[1])(1'000));
2939 ammAlice.withdraw(alice_, aliceTokens, MPT(ammAlice[1])(0));
2940 ammAlice.withdraw(carol_, carolTokens, MPT(ammAlice[1])(0));
2941 BEAST_EXPECT(ammAlice.expectBalances(
2942 XRP(10'000), MPT(ammAlice[1])(10'001), ammAlice.tokens()));
2943 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
2944 BEAST_EXPECT(ammAlice.expectLPTokens(alice_, ammAlice.tokens()));
2945 },
2946 {{XRP(10'000), gAmmmpt(10'000)}});
2947
2948 // Equal deposit 10%, withdraw all tokens. XRP/MPT
2949 testAMM(
2950 [&](AMM& ammAlice, Env&) {
2951 ammAlice.deposit(carol_, 1'000'000);
2952 ammAlice.withdrawAll(carol_);
2953 BEAST_EXPECT(ammAlice.expectBalances(
2954 XRP(10'000), MPT(ammAlice[1])(10'000), IOUAmount{10'000'000, 0}));
2955 },
2956 {{XRP(10'000), gAmmmpt(10'000)}});
2957 // Equal deposit 10%, withdraw all tokens. IOU/MPT combination.
2958 {
2959 auto test = [&](auto&& issue1, auto&& issue2) {
2960 Env env(*this);
2961 env.fund(XRP(30'000), alice_, carol_, gw_);
2962 env.close();
2963 auto const usd = issue1(
2964 {.env = env,
2965 .token = "USD",
2966 .issuer = gw_,
2967 .holders = {alice_, carol_},
2968 .limit = 1'000'000});
2969 auto const btc = issue2(
2970 {.env = env,
2971 .token = "BTC",
2972 .issuer = gw_,
2973 .holders = {alice_, carol_},
2974 .limit = 1'000'000});
2975 env(pay(gw_, alice_, btc(50000)));
2976 env(pay(gw_, carol_, btc(50000)));
2977 env(pay(gw_, alice_, usd(50000)));
2978 env(pay(gw_, carol_, usd(50000)));
2979 env.close();
2980 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
2981 auto carolBTC = env.balance(carol_, btc);
2982 auto carolUSD = env.balance(carol_, usd);
2983 ammAlice.deposit(carol_, 1'000);
2984 ammAlice.withdrawAll(carol_);
2985 BEAST_EXPECT(
2986 ammAlice.expectBalances(usd(10'000), btc(10'000), IOUAmount{10'000, 0}));
2987 env.require(Balance(carol_, carolBTC));
2988 env.require(Balance(carol_, carolUSD));
2989 };
2991 }
2992
2993 // Equal deposit 10%, withdraw all tokens in MPT from XRP/MPT
2994 testAMM(
2995 [&](AMM& ammAlice, Env&) {
2996 ammAlice.deposit(carol_, 1'000'000);
2997 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
2998 BEAST_EXPECT(ammAlice.expectBalances(
2999 XRP(11'000),
3000 STAmount{MPT(ammAlice[1]), UINT64_C(9'090'909090909092), -12},
3001 IOUAmount{10'000'000, 0}));
3002 },
3003 {{XRP(10'000), gAmmmpt(10'000)}});
3004
3005 // Equal deposit 10%, withdraw all tokens in XRP from XRP/MPT
3006 testAMM(
3007 [&](AMM& ammAlice, Env&) {
3008 ammAlice.deposit(carol_, 1'000'000);
3009 ammAlice.withdrawAll(carol_, XRP(0));
3010 BEAST_EXPECT(ammAlice.expectBalances(
3011 XRPAmount(9'090'909'091), MPT(ammAlice[1])(11'000), IOUAmount{10'000'000, 0}));
3012 },
3013 {{XRP(10'000), gAmmmpt(10'000)}});
3014
3015 // Equal deposit 10%, withdraw all tokens in USD from USD/MPT
3016 testAMM(
3017 [&](AMM& ammAlice, Env&) {
3018 ammAlice.deposit(carol_, 1'000);
3019 ammAlice.withdrawAll(carol_, USD(0));
3020 BEAST_EXPECT(ammAlice.expectBalances(
3021 STAmount{USD, UINT64_C(9'090'909090909092), -12},
3022 MPT(ammAlice[1])(11'000),
3023 IOUAmount{10'000}));
3024 },
3025 {{USD(10'000), gAmmmpt(10'000)}});
3026 // Equal deposit 10%, withdraw all tokens in MPT from MPT/MPT
3027 testAMM(
3028 [&](AMM& ammAlice, Env&) {
3029 ammAlice.deposit(carol_, 1'000);
3030 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
3031 BEAST_EXPECT(ammAlice.expectBalances(
3032 MPT(ammAlice[0])(11'000), MPT(ammAlice[1])(9'091), IOUAmount{10'000}));
3033 },
3034 {{gAmmmpt(10'000), gAmmmpt(10'000)}});
3035
3036 auto const all = testableAmendments();
3037
3038 // Withdraw with EPrice limit.
3039 testAMM(
3040 [&](AMM& ammAlice, Env& env) {
3041 ammAlice.deposit(carol_, 1'000'000'000'000);
3042 ammAlice.withdraw(
3043 carol_, MPT(ammAlice[1])(100'000000), std::nullopt, IOUAmount{520, 0});
3044 if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
3045 {
3046 BEAST_EXPECT(
3047 ammAlice.expectBalances(
3048 XRP(11'000'000000),
3049 MPT(ammAlice[1])(9372781065),
3050 IOUAmount{10'153'846'15384616, -2}) &&
3051 ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2}));
3052 }
3053 else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
3054 {
3055 BEAST_EXPECT(
3056 ammAlice.expectBalances(
3057 XRP(11'000'000000),
3058 MPT(ammAlice[1])(9372781065),
3059 IOUAmount{10'153'846'15384616, -2}) &&
3060 ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2}));
3061 }
3062 else if (env.enabled(fixAMMv1_3))
3063 {
3064 BEAST_EXPECT(
3065 ammAlice.expectBalances(
3066 XRP(11'000'000000),
3067 MPT(ammAlice[1])(9372781066),
3068 IOUAmount{10'153'846'15384616, -2}) &&
3069 ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2}));
3070 }
3071 ammAlice.withdrawAll(carol_);
3072 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
3073 },
3074 {{XRP(10'000'000'000), gAmmmpt(10'000'000'000)}},
3075 0,
3076 std::nullopt,
3077 {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3});
3078
3079 // Withdraw with EPrice limit. AssetOut is 0.
3080 testAMM(
3081 [&](AMM& ammAlice, Env& env) {
3082 ammAlice.deposit(carol_, 1'000'000'000'000);
3083 ammAlice.withdraw(carol_, MPT(ammAlice[1])(0), std::nullopt, IOUAmount{520, 0});
3084 if (!env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
3085 {
3086 BEAST_EXPECT(
3087 ammAlice.expectBalances(
3088 XRP(11'000'000000),
3089 MPT(ammAlice[1])(9372781065),
3090 IOUAmount{10'153'846'15384616, -2}) &&
3091 ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2}));
3092 }
3093 else if (env.enabled(fixAMMv1_1) && !env.enabled(fixAMMv1_3))
3094 {
3095 BEAST_EXPECT(
3096 ammAlice.expectBalances(
3097 XRP(11'000'000000),
3098 MPT(ammAlice[1])(9372781065),
3099 IOUAmount{10'153'846'15384616, -2}) &&
3100 ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2}));
3101 }
3102 else if (env.enabled(fixAMMv1_3))
3103 {
3104 BEAST_EXPECT(
3105 ammAlice.expectBalances(
3106 XRP(11'000'000000),
3107 MPT(ammAlice[1])(9372781066),
3108 IOUAmount{10'153'846'15384616, -2}) &&
3109 ammAlice.expectLPTokens(carol_, IOUAmount{153'846'15384616, -2}));
3110 }
3111 ammAlice.withdrawAll(carol_);
3112 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
3113 },
3114 {{XRP(10'000'000'000), gAmmmpt(10'000'000'000)}},
3115 0,
3116 std::nullopt,
3117 {all, all - fixAMMv1_3, all - fixAMMv1_1 - fixAMMv1_3});
3118
3119 // IOU/MPT combination + transfer fee
3120 {
3121 auto test = [&](auto&& issue1, auto&& issue2) {
3122 Env env(*this);
3123 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
3124 env.close();
3125 auto const usd = issue1(
3126 {.env = env,
3127 .token = "USD",
3128 .issuer = gw_,
3129 .holders = {alice_, bob_, carol_},
3130 .limit = 1'000'000,
3131 .transferFee = 25'000});
3132 auto const btc = issue2(
3133 {.env = env,
3134 .token = "BTC",
3135 .issuer = gw_,
3136 .holders = {alice_, bob_, carol_},
3137 .limit = 1'000'000,
3138 .transferFee = 25'000});
3139 env(pay(gw_, alice_, btc(10000)));
3140 env(pay(gw_, bob_, btc(10000)));
3141 env(pay(gw_, carol_, btc(10000)));
3142 env(pay(gw_, alice_, usd(10000)));
3143 env(pay(gw_, bob_, usd(10000)));
3144 env(pay(gw_, carol_, usd(10000)));
3145 env.close();
3146 // no transfer fee on create
3147 AMM ammAlice(env, alice_, btc(2'000), usd(5));
3148 BEAST_EXPECT(ammAlice.expectBalances(btc(2'000), usd(5), IOUAmount{100, 0}));
3149 env.require(Balance(alice_, btc(8000)));
3150 env.require(Balance(alice_, usd(9995)));
3151
3152 // no transfer fee on deposit
3153 ammAlice.deposit(carol_, 100);
3154 BEAST_EXPECT(ammAlice.expectBalances(btc(4000), usd(10), IOUAmount{200, 0}));
3155 env.require(Balance(carol_, btc(8000)));
3156 env.require(Balance(carol_, usd(9995)));
3157
3158 // no transfer fee on withdraw
3159 ammAlice.withdraw(carol_, 100);
3160 BEAST_EXPECT(ammAlice.expectBalances(btc(2'000), usd(5), IOUAmount{100, 0}));
3161 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0, 0}));
3162 env.require(Balance(carol_, btc(10000)));
3163 env.require(Balance(carol_, usd(10000)));
3164 };
3166 }
3167
3168 // Tiny withdraw
3169 testAMM(
3170 [&](AMM& ammAlice, Env&) {
3171 // By tokens
3172 ammAlice.withdraw(alice_, IOUAmount{1, -3});
3173 BEAST_EXPECT(ammAlice.expectBalances(
3174 MPT(ammAlice[0])(9'999'999'999),
3175 STAmount{USD, UINT64_C(9'999'999999), -6},
3176 IOUAmount{9'999'999'999, -3}));
3177 },
3178 {{gAmmmpt(10'000'000'000), USD(10'000)}});
3179 testAMM(
3180 [&](AMM& ammAlice, Env&) {
3181 // Single withdraw MPT from MPT/IOU
3182 ammAlice.withdraw(alice_, std::nullopt, MPT(ammAlice[0])(1));
3183 BEAST_EXPECT(ammAlice.expectBalances(
3184 MPT(ammAlice[0])(10000'000000), USD(10'000), IOUAmount{9'999'999'9995, -4}));
3185 },
3186 {{gAmmmpt(10'000'000'000), USD(10'000)}});
3187 testAMM(
3188 [&](AMM& ammAlice, Env&) {
3189 // Single withdraw IOU from MPT/IOU
3190 ammAlice.withdraw(alice_, std::nullopt, STAmount{USD, 1, -10});
3191 BEAST_EXPECT(ammAlice.expectBalances(
3192 MPT(ammAlice[0])(10'000'000'000),
3193 STAmount{USD, UINT64_C(9'999'9999999999), -10},
3194 IOUAmount{9'999'999'99999995, -8}));
3195 },
3196 {{gAmmmpt(10'000'000'000), USD(10'000)}});
3197 testAMM(
3198 [&](AMM& ammAlice, Env&) {
3199 // Single withdraw XRP from MPT/XRP
3200 ammAlice.withdraw(alice_, std::nullopt, XRPAmount(1));
3201 BEAST_EXPECT(ammAlice.expectBalances(
3202 MPT(ammAlice[1])(10'000), XRP(10'000), IOUAmount{9999999'9995, -4}));
3203 },
3204 {{XRP(10'000), gAmmmpt(10'000)}});
3205
3206 // Withdraw close to entire pool
3207 // Equal by tokens
3208 testAMM(
3209 [&](AMM& ammAlice, Env&) {
3210 ammAlice.withdraw(alice_, IOUAmount{9'999'999'999, -3});
3211 BEAST_EXPECT(ammAlice.expectBalances(
3212 MPT(ammAlice[0])(1), STAmount{USD, 1, -6}, IOUAmount{1, -3}));
3213 },
3214 {{gAmmmpt(10'000'000'000), USD(10'000)}});
3215 // USD by tokens
3216 testAMM(
3217 [&](AMM& ammAlice, Env&) {
3218 ammAlice.withdraw(alice_, IOUAmount{9'999'999}, USD(0));
3219 BEAST_EXPECT(ammAlice.expectBalances(
3220 MPT(ammAlice[0])(10'000'000'000), STAmount{USD, 1, -10}, IOUAmount{1}));
3221 },
3222 {{gAmmmpt(10'000'000'000), USD(10'000)}});
3223 // MPT by tokens
3224 testAMM(
3225 [&](AMM& ammAlice, Env&) {
3226 ammAlice.withdraw(alice_, IOUAmount{9'999'900}, MPT(ammAlice[0])(0));
3227 BEAST_EXPECT(
3228 ammAlice.expectBalances(MPT(ammAlice[0])(1), USD(10'000), IOUAmount{100}));
3229 },
3230 {{gAmmmpt(10'000'000'000), USD(10'000)}});
3231 // XRP by tokens
3232 testAMM(
3233 [&](AMM& ammAlice, Env&) {
3234 ammAlice.withdraw(alice_, IOUAmount{9'999'900}, XRP(0));
3235 BEAST_EXPECT(
3236 ammAlice.expectBalances(MPT(ammAlice[1])(10000), XRPAmount(1), IOUAmount{100}));
3237 },
3238 {{XRP(10'000), gAmmmpt(10'000)}});
3239 // USD
3240 testAMM(
3241 [&](AMM& ammAlice, Env&) {
3242 ammAlice.withdraw(alice_, STAmount{USD, UINT64_C(9'999'99999999999), -11});
3243 BEAST_EXPECT(ammAlice.expectBalances(
3244 MPT(ammAlice[0])(10'000'000'000),
3245 STAmount{USD, 1, -11},
3246 IOUAmount{316227765, -9}));
3247 },
3248 {{gAmmmpt(10'000'000'000), USD(10'000)}});
3249 // XRP
3250 testAMM(
3251 [&](AMM& ammAlice, Env&) {
3252 ammAlice.withdraw(alice_, XRPAmount{9'999'999'999});
3253 BEAST_EXPECT(
3254 ammAlice.expectBalances(MPT(ammAlice[1])(10000), XRPAmount(1), IOUAmount{100}));
3255 },
3256 {{XRP(10'000), gAmmmpt(10'000)}});
3257 // MPT
3258 testAMM(
3259 [&](AMM& ammAlice, Env&) {
3260 ammAlice.withdraw(alice_, MPT(ammAlice[0])(9'999'999'999));
3261 BEAST_EXPECT(
3262 ammAlice.expectBalances(MPT(ammAlice[0])(1), USD(10'000), IOUAmount{100}));
3263 },
3264 {{gAmmmpt(10'000'000'000), USD(10'000)}});
3265 testAMM(
3266 [&](AMM& ammAlice, Env&) {
3267 ammAlice.withdraw(alice_, MPT(ammAlice[1])(9'999));
3268 BEAST_EXPECT(
3269 ammAlice.expectBalances(MPT(ammAlice[1])(1), XRP(10'000), IOUAmount{100000}));
3270 },
3271 {{XRP(10'000), gAmmmpt(10'000)}});
3272 }
3273
3274 void
3276 {
3277 testcase("Invalid Fee Vote");
3278 using namespace jtx;
3279
3280 testAMM(
3281 [&](AMM& ammAlice, Env& env) {
3282 // Invalid Account
3283 Account bad("bad");
3284 env.memoize(bad);
3285 ammAlice.vote(bad, 1'000, std::nullopt, Seq(1), std::nullopt, Ter(terNO_ACCOUNT));
3286
3287 // Invalid AMM
3288 ammAlice.vote(
3289 alice_,
3290 1'000,
3291 std::nullopt,
3292 std::nullopt,
3293 {{MPT(ammAlice[1]), GBP}},
3294 Ter(terNO_AMM));
3295
3296 // Account is not LP
3297 ammAlice.vote(
3298 carol_,
3299 1'000,
3300 std::nullopt,
3301 std::nullopt,
3302 std::nullopt,
3304
3305 // Invalid asset pair
3306 ammAlice.vote(
3307 alice_,
3308 1'000,
3309 std::nullopt,
3310 std::nullopt,
3311 {{MPT(ammAlice[1]), MPT(ammAlice[1])}},
3313 },
3314 {{XRP(10'000), gAmmmpt(10'000)}});
3315
3316 // Invalid AMM
3317 testAMM(
3318 [&](AMM& ammAlice, Env& env) {
3319 ammAlice.withdrawAll(alice_);
3320 ammAlice.vote(
3321 alice_, 1'000, std::nullopt, std::nullopt, std::nullopt, Ter(terNO_AMM));
3322 },
3323 {{XRP(10'000), gAmmmpt(10'000)}});
3324
3325 // MPTokenInstance object doesn't exist
3326 {
3327 Env env(*this);
3328 env.fund(XRP(1'000), alice_);
3329 env(AMM::voteJv({.account = alice_, .tfee = 1'000, .assets = {{XRP, MPT(alice_, 0)}}}),
3330 Ter(terNO_AMM));
3331 }
3332 }
3333
3334 void
3336 {
3337 testcase("Fee Vote");
3338 using namespace jtx;
3339
3340 // One vote sets fee to 1%.
3341 testAMM(
3342 [&](AMM& ammAlice, Env& env) {
3343 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{0}));
3344 ammAlice.vote({}, 1'000);
3345 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
3346 // Discounted fee is 1/10 of trading fee.
3347 BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{0}));
3348 },
3349 {{XRP(10'000), gAmmmpt(10'000)}});
3350
3351 auto vote = [&](AMM& ammAlice,
3352 Env& env,
3353 int i,
3354 std::uint32_t tokens = 10'000'000,
3355 std::vector<Account>* accounts = nullptr) {
3356 Account a(std::to_string(i));
3357 ammAlice.deposit(a, tokens);
3358 ammAlice.vote(a, 50 * (i + 1));
3359 if (accounts)
3360 accounts->push_back(std::move(a));
3361 };
3362
3363 {
3364 // Eight votes fill all voting slots, set fee 0.175%.
3365 // New vote, same account, sets fee 0.225%
3366 Env env{*this};
3367 env.fund(XRP(30'000), gw_, alice_);
3368 std::vector<Account> holders = {alice_};
3369 for (int i = 0; i <= 7; ++i)
3370 {
3371 Account const a(std::to_string(i));
3372 holders.push_back(a);
3373 env.fund(XRP(30'000), a);
3374 }
3375 env.close();
3376 // create MPT and pay 30'000 to all the accounts
3377 MPTTester const btc({.env = env, .issuer = gw_, .holders = holders, .pay = 30'000});
3378 AMM ammAlice(env, alice_, XRP(10'000), btc(10'000));
3379 for (int i = 0; i < 7; ++i)
3380 vote(ammAlice, env, i);
3381 BEAST_EXPECT(ammAlice.expectTradingFee(175));
3382 Account const a("0");
3383 ammAlice.vote(a, 450);
3384 BEAST_EXPECT(ammAlice.expectTradingFee(225));
3385 }
3386
3387 {
3388 // Eight votes fill all voting slots, set fee 0.175%.
3389 // New vote, new account, higher vote weight, set higher fee 0.244%
3390 Env env{*this};
3391 env.fund(XRP(30'000), gw_, alice_);
3392 std::vector<Account> holders = {alice_};
3393 for (int i = 0; i < 8; ++i)
3394 {
3395 Account const a(std::to_string(i));
3396 holders.push_back(a);
3397 env.fund(XRP(30'000), a);
3398 }
3399 env.close();
3400 MPTTester const btc({.env = env, .issuer = gw_, .holders = holders, .pay = 30'000});
3401 AMM ammAlice(env, alice_, XRP(10'000), btc(10'000));
3402 for (int i = 0; i < 7; ++i)
3403 vote(ammAlice, env, i);
3404 BEAST_EXPECT(ammAlice.expectTradingFee(175));
3405 vote(ammAlice, env, 7, 20'000'000);
3406 BEAST_EXPECT(ammAlice.expectTradingFee(244));
3407 }
3408
3409 {
3410 // Eight votes fill all voting slots, set fee 0.219%.
3411 // New vote, new account, higher vote weight, set smaller fee 0.206%
3412 Env env{*this};
3413 env.fund(XRP(30'000), gw_, alice_);
3414 std::vector<Account> holders = {alice_};
3415 for (int i = 0; i < 8; ++i)
3416 {
3417 Account const a(std::to_string(i));
3418 holders.push_back(a);
3419 env.fund(XRP(30'000), a);
3420 }
3421 env.close();
3422 MPTTester const btc({.env = env, .issuer = gw_, .holders = holders, .pay = 30'000});
3423 AMM ammAlice(env, alice_, XRP(10'000), btc(10'000));
3424 for (int i = 7; i > 0; --i)
3425 vote(ammAlice, env, i);
3426 BEAST_EXPECT(ammAlice.expectTradingFee(219));
3427 vote(ammAlice, env, 0, 20'000'000);
3428 BEAST_EXPECT(ammAlice.expectTradingFee(206));
3429 }
3430
3431 {
3432 // Eight votes fill all voting slots. The accounts then withdraw all
3433 // tokens. An account sets a new fee and the previous slots are
3434 // deleted.
3435 Env env{*this};
3436 env.fund(XRP(30'000), gw_, alice_, carol_);
3437 std::vector<Account> holders = {alice_, carol_};
3438 for (int i = 0; i < 7; ++i)
3439 {
3440 Account const a(std::to_string(i));
3441 holders.push_back(a);
3442 env.fund(XRP(30'000), a);
3443 }
3444 env.close();
3445 MPTTester const btc({.env = env, .issuer = gw_, .holders = holders, .pay = 30'000});
3446 AMM ammAlice(env, alice_, XRP(10'000), btc(10'000));
3447 std::vector<Account> accounts;
3448 for (int i = 0; i < 7; ++i)
3449 vote(ammAlice, env, i, 10'000'000, &accounts);
3450 BEAST_EXPECT(ammAlice.expectTradingFee(175));
3451 for (int i = 0; i < 7; ++i)
3452 ammAlice.withdrawAll(accounts[i]);
3453 ammAlice.deposit(carol_, 10'000'000);
3454 ammAlice.vote(carol_, 1'000);
3455 // The initial LP set the fee to 1000. Carol gets 50% voting
3456 // power, and the new fee is 500.
3457 BEAST_EXPECT(ammAlice.expectTradingFee(500));
3458 }
3459 {
3460 // Eight votes fill all voting slots. The accounts then withdraw
3461 // some tokens. The new vote doesn't get the voting power but the
3462 // slots are refreshed and the fee is updated.
3463 Env env{*this};
3464 env.fund(XRP(30'000), gw_, alice_, carol_);
3465 std::vector<Account> holders = {alice_, carol_};
3466 for (int i = 0; i < 7; ++i)
3467 {
3468 Account const a(std::to_string(i));
3469 holders.push_back(a);
3470 env.fund(XRP(30'000), a);
3471 }
3472 env.close();
3473 MPTTester const btc({.env = env, .issuer = gw_, .holders = holders, .pay = 30'000});
3474 AMM ammAlice(env, alice_, XRP(10'000), btc(10'000));
3475 std::vector<Account> accounts;
3476 for (int i = 0; i < 7; ++i)
3477 vote(ammAlice, env, i, 10'000'000, &accounts);
3478 BEAST_EXPECT(ammAlice.expectTradingFee(175));
3479 for (int i = 0; i < 7; ++i)
3480 ammAlice.withdraw(accounts[i], 9'000'000);
3481 ammAlice.deposit(carol_, 1'000);
3482 // The vote is not added to the slots
3483 ammAlice.vote(carol_, 1'000);
3484 auto const info = ammAlice.ammRpcInfo()[jss::amm][jss::vote_slots];
3485 for (auto i = 0; i < info.size(); ++i)
3486 BEAST_EXPECT(info[i][jss::account] != carol_.human());
3487 // But the slots are refreshed and the fee is changed
3488 BEAST_EXPECT(ammAlice.expectTradingFee(82));
3489 }
3490 }
3491
3492 void
3494 {
3495 testcase("Invalid Bid");
3496 using namespace jtx;
3497 using namespace std::chrono;
3498
3499 // burn all the LPTokens through a AMMBid transaction
3500 {
3501 Env env(*this);
3502 env.fund(XRP(2'000), gw_, alice_);
3503 MPTTester const btc(
3504 {.env = env,
3505 .issuer = gw_,
3506 .holders = {alice_},
3507 .pay = 2'000,
3508 .flags = kMptDexFlags});
3509 AMM amm(env, gw_, XRP(1'000), btc(1'000), false, 1'000);
3510
3511 // auction slot is owned by the creator of the AMM i.e. gw
3512 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
3513
3514 // gw attempts to burn all her LPTokens through a bid transaction
3515 // this transaction fails because AMMBid transaction can not burn
3516 // all the outstanding LPTokens
3517 env(amm.bid({
3518 .account = gw_,
3519 .bidMin = 1'000'000,
3520 }),
3522 }
3523
3524 // burn all the LPTokens through a AMMBid transaction
3525 {
3526 Env env(*this);
3527 env.fund(XRP(2'000), gw_, alice_);
3528 MPTTester const btc(
3529 {.env = env,
3530 .issuer = gw_,
3531 .holders = {alice_},
3532 .pay = 2'000,
3533 .flags = kMptDexFlags});
3534 AMM amm(env, gw_, XRP(1'000), btc(1'000), false, 1'000);
3535
3536 // auction slot is owned by the creator of the AMM i.e. gw
3537 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
3538
3539 // gw burns all but one of its LPTokens through a bid transaction
3540 // this transaction succeeds because the bid price is less than
3541 // the total outstanding LPToken balance
3542 env(amm.bid({
3543 .account = gw_,
3544 .bidMin = STAmount{amm.lptIssue(), UINT64_C(999'999)},
3545 }),
3546 Ter(tesSUCCESS))
3547 .close();
3548
3549 // gw must own the auction slot
3550 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{999'999}));
3551
3552 // 999'999 tokens are burned, only 1 LPToken is owned by gw
3553 BEAST_EXPECT(amm.expectBalances(XRP(1'000), btc(1'000), IOUAmount{1}));
3554
3555 // gw owns only 1 LPToken in its balance
3556 BEAST_EXPECT(Number{amm.getLPTokensBalance(gw_)} == 1);
3557
3558 // gw attempts to burn the last of its LPTokens in an AMMBid
3559 // transaction. This transaction fails because it would burn all
3560 // the remaining LPTokens
3561 env(amm.bid({
3562 .account = gw_,
3563 .bidMin = 1,
3564 }),
3566 }
3567
3568 testAMM(
3569 [&](AMM& ammAlice, Env& env) {
3570 ammAlice.deposit(carol_, 1'000'000);
3571 // Invlaid Min/Max combination
3572 env(ammAlice.bid({
3573 .account = carol_,
3574 .bidMin = 200,
3575 .bidMax = 100,
3576 }),
3578
3579 // Invalid Account
3580 Account bad("bad");
3581 env.memoize(bad);
3582 env(ammAlice.bid({
3583 .account = bad,
3584 .bidMax = 100,
3585 }),
3586 Seq(1),
3588
3589 // Account is not LP
3590 Account const dan("dan");
3591 env.fund(XRP(1'000), dan);
3592 env(ammAlice.bid({
3593 .account = dan,
3594 .bidMin = 100,
3595 }),
3597 env(ammAlice.bid({
3598 .account = dan,
3599 }),
3601
3602 // Auth account is invalid.
3603 env(ammAlice.bid({
3604 .account = carol_,
3605 .bidMin = 100,
3606 .authAccounts = {bob_},
3607 }),
3608 Ter(terNO_ACCOUNT));
3609
3610 // Invalid Assets
3611 env(ammAlice.bid({
3612 .account = alice_,
3613 .bidMax = 100,
3614 .assets = {{MPT(ammAlice[1]), GBP}},
3615 }),
3616 Ter(terNO_AMM));
3617 },
3618 {{XRP(10'000), gAmmmpt(10'000)}});
3619
3620 // Invalid AMM
3621 testAMM(
3622 [&](AMM& ammAlice, Env& env) {
3623 ammAlice.withdrawAll(alice_);
3624 env(ammAlice.bid({
3625 .account = alice_,
3626 .bidMax = 100,
3627 }),
3628 Ter(terNO_AMM));
3629 },
3630 {{XRP(10'000), gAmmmpt(10'000)}});
3631
3632 // Bid price exceeds LP owned tokens
3633 {
3634 Env env(*this);
3635 fund(env, gw_, {alice_, bob_, carol_}, XRP(1'000), {USD(30'000)}, Fund::All);
3636 MPTTester const btc(
3637 {.env = env,
3638 .issuer = gw_,
3639 .holders = {alice_, carol_, bob_},
3640 .pay = 30'000'000'000,
3641 .flags = kMptDexFlags});
3642
3643 AMM ammAlice(env, alice_, btc(10'000'000'000), USD(10'000));
3644 ammAlice.deposit(carol_, 1'000'000);
3645 ammAlice.deposit(bob_, 10);
3646 env(ammAlice.bid({
3647 .account = carol_,
3648 .bidMin = 1'000'001,
3649 }),
3650 Ter(tecAMM_INVALID_TOKENS));
3651 env(ammAlice.bid({
3652 .account = carol_,
3653 .bidMax = 1'000'001,
3654 }),
3655 Ter(tecAMM_INVALID_TOKENS));
3656 env(ammAlice.bid({
3657 .account = carol_,
3658 .bidMin = 1'000,
3659 }));
3660 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{1'000}));
3661 // Slot purchase price is more than 1000 but bob only has 10 tokens
3662 env(ammAlice.bid({
3663 .account = bob_,
3664 }),
3665 Ter(tecAMM_INVALID_TOKENS));
3666 }
3667
3668 // Bid all tokens, still own the slot
3669 {
3670 Env env(*this);
3671 env.fund(XRP(1'000), gw_, alice_, bob_);
3672 MPTTester const btc(
3673 {.env = env,
3674 .issuer = gw_,
3675 .holders = {alice_, bob_},
3676 .pay = 1'000,
3677 .flags = kMptDexFlags});
3678 AMM amm(env, gw_, XRP(10), btc(1'000));
3679 auto const lpIssue = amm.lptIssue();
3680 env.trust(STAmount{lpIssue, 100}, alice_);
3681 env.trust(STAmount{lpIssue, 50}, bob_);
3682 env(pay(gw_, alice_, STAmount{lpIssue, 100}));
3683 env(pay(gw_, bob_, STAmount{lpIssue, 50}));
3684 env(amm.bid({.account = alice_, .bidMin = 100}));
3685 // Alice doesn't have any more tokens, but
3686 // she still owns the slot.
3687 env(amm.bid({
3688 .account = bob_,
3689 .bidMax = 50,
3690 }),
3691 Ter(tecAMM_FAILED));
3692 }
3693 }
3694
3695 void
3697 {
3698 testcase("Bid");
3699 using namespace jtx;
3700 using namespace std::chrono;
3701
3702 // Auction slot initially is owned by AMM creator, who pays 0 price.
3703
3704 // Bid 110 tokens. Pay bidMin.
3705 testAMM(
3706 [&](AMM& ammAlice, Env& env) {
3707 ammAlice.deposit(carol_, 1'000'000);
3708 env(ammAlice.bid({.account = carol_, .bidMin = 110}));
3709 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
3710 // 110 tokens are burned.
3711 BEAST_EXPECT(ammAlice.expectBalances(
3712 XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{10'999'890, 0}));
3713 },
3714 {{XRP(10'000), gAmmmpt(10'000)}});
3715
3716 // Bid with min/max when the pay price is less than min.
3717 testAMM(
3718 [&](AMM& ammAlice, Env& env) {
3719 ammAlice.deposit(carol_, 1'000'000);
3720 // Bid exactly 110. Pay 110 because the pay price is < 110.
3721 env(ammAlice.bid({.account = carol_, .bidMin = 110, .bidMax = 110}));
3722 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
3723 BEAST_EXPECT(ammAlice.expectBalances(
3724 XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{10'999'890}));
3725 // Bid exactly 180-200. Pay 180 because the pay price is < 180.
3726 env(ammAlice.bid({.account = alice_, .bidMin = 180, .bidMax = 200}));
3727 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{180}));
3728 BEAST_EXPECT(ammAlice.expectBalances(
3729 XRP(11'000), MPT(ammAlice[1])(11'000), IOUAmount{10'999'814'5, -1}));
3730 },
3731 {{XRP(10'000), gAmmmpt(10'000)}});
3732
3733 // Start bid at bidMin 110.
3734 {
3735 Env env(*this);
3736 env.fund(XRP(30'000), gw_, alice_, carol_, bob_);
3737 MPTTester const btc(
3738 {.env = env,
3739 .issuer = gw_,
3740 .holders = {alice_, carol_, bob_},
3741 .pay = 30'000,
3742 .flags = kMptDexFlags});
3743 AMM ammAlice(env, alice_, XRP(10'000), btc(10'000));
3744
3745 ammAlice.deposit(carol_, 1'000'000);
3746 // Bid, pay bidMin.
3747 env(ammAlice.bid({.account = carol_, .bidMin = 110}));
3748 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
3749
3750 ammAlice.deposit(bob_, 1'000'000);
3751 // Bid, pay the computed price.
3752 env(ammAlice.bid({.account = bob_}));
3753 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount(1155, -1)));
3754
3755 // Bid bidMax fails because the computed price is higher.
3756 env(ammAlice.bid({
3757 .account = carol_,
3758 .bidMax = 120,
3759 }),
3761 // Bid MaxSlotPrice succeeds - pay computed price
3762 env(ammAlice.bid({.account = carol_, .bidMax = 600}));
3763 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{121'275, -3}));
3764
3765 // Bid Min/MaxSlotPrice fails because the computed price is not
3766 // in range
3767 env(ammAlice.bid({
3768 .account = carol_,
3769 .bidMin = 10,
3770 .bidMax = 100,
3771 }),
3773 // Bid Min/MaxSlotPrice succeeds - pay computed price
3774 env(ammAlice.bid({.account = carol_, .bidMin = 100, .bidMax = 600}));
3775 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{127'33875, -5}));
3776 }
3777
3778 // Slot states.
3779 {
3780 Env env(*this);
3781 env.fund(XRP(30'000), gw_, alice_, carol_, bob_);
3782 MPTTester const btc(
3783 {.env = env,
3784 .issuer = gw_,
3785 .holders = {alice_, carol_, bob_},
3786 .pay = 30'000,
3787 .flags = kMptDexFlags});
3788 AMM ammAlice(env, alice_, XRP(10'000), btc(10'000));
3789
3790 ammAlice.deposit(carol_, 1'000'000);
3791 ammAlice.deposit(bob_, 1'000'000);
3792 BEAST_EXPECT(ammAlice.expectBalances(
3793 XRPAmount(12'000'000001), btc(12'001), IOUAmount{12'000'000, 0}));
3794
3795 // Initial state. Pay bidMin.
3796 env(ammAlice.bid({.account = carol_, .bidMin = 110})).close();
3797 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{110}));
3798
3799 // 1st Interval after close, price for 0th interval.
3800 env(ammAlice.bid({.account = bob_}));
3802 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 1, IOUAmount{1'155, -1}));
3803
3804 // 10th Interval after close, price for 1st interval.
3805 env(ammAlice.bid({.account = carol_}));
3807 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 10, IOUAmount{121'275, -3}));
3808
3809 // 20th Interval (expired) after close, price for 10th interval.
3810 env(ammAlice.bid({.account = bob_}));
3812 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{127'33875, -5}));
3813
3814 // 0 Interval.
3815 env(ammAlice.bid({.account = carol_, .bidMin = 110})).close();
3816 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, std::nullopt, IOUAmount{110}));
3817 // ~321.09 tokens burnt on bidding fees.
3818 BEAST_EXPECT(ammAlice.expectBalances(
3819 XRPAmount(12'000'000001), btc(12'001), IOUAmount{11'999'678'91000001, -8}));
3820 }
3821
3822 // Pool's fee 1%. Bid bidMin.
3823 // Auction slot owner and auth account trade at discounted fee -
3824 // 1/10 of the trading fee.
3825 // Other accounts trade at 1% fee.
3826 {
3827 Env env(*this);
3828 Account const dan("dan");
3829 Account const ed("ed");
3830 env.fund(XRP(2'000), gw_, alice_, bob_, carol_, dan, ed);
3831 MPTTester const btc(
3832 {.env = env,
3833 .issuer = gw_,
3834 .holders = {alice_, bob_, carol_, dan, ed},
3835 .pay = 30'000'000'000});
3836 fund(env, gw_, {alice_, carol_}, {USD(30'000)}, Fund::TokenOnly);
3837 fund(env, gw_, {bob_, dan, ed}, {USD(20'000)}, Fund::TokenOnly);
3838
3839 AMM ammAlice(env, alice_, btc(10'000'000'000), USD(10'000), CreateArg{.tfee = 1'000});
3840 ammAlice.deposit(bob_, 1'000'000);
3841 ammAlice.deposit(ed, 1'000'000);
3842 ammAlice.deposit(carol_, 500'000);
3843 ammAlice.deposit(dan, 500'000);
3844 auto ammTokens = ammAlice.getLPTokensBalance();
3845 env(ammAlice.bid({
3846 .account = carol_,
3847 .bidMin = 120,
3848 .authAccounts = {bob_, ed},
3849 }));
3850 auto const slotPrice = IOUAmount{5'200};
3851 ammTokens -= slotPrice;
3852 BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice));
3853 BEAST_EXPECT(ammAlice.expectBalances(btc(13'000'000'003), USD(13'000), ammTokens));
3854 // Discounted trade
3855 for (int i = 0; i < 10; ++i)
3856 {
3857 auto tokens = ammAlice.deposit(carol_, USD(100));
3858 ammAlice.withdraw(carol_, tokens, USD(0));
3859 tokens = ammAlice.deposit(bob_, USD(100));
3860 ammAlice.withdraw(bob_, tokens, USD(0));
3861 tokens = ammAlice.deposit(ed, USD(100));
3862 ammAlice.withdraw(ed, tokens, USD(0));
3863 }
3864 // carol, bob, and ed pay ~0.99USD in fees.
3865 BEAST_EXPECT(
3866 env.balance(carol_, USD) == STAmount(USD, UINT64_C(29'499'00572620545), -11));
3867 BEAST_EXPECT(
3868 env.balance(bob_, USD) == STAmount(USD, UINT64_C(18'999'00572616195), -11));
3869 BEAST_EXPECT(env.balance(ed, USD) == STAmount(USD, UINT64_C(18'999'00572611841), -11));
3870 // USD pool is slightly higher because of the fees.
3871 BEAST_EXPECT(ammAlice.expectBalances(
3872 btc(13'000'000'003), STAmount(USD, UINT64_C(13'002'98282151419), -11), ammTokens));
3873
3874 ammTokens = ammAlice.getLPTokensBalance();
3875 // Trade with the fee
3876 for (int i = 0; i < 10; ++i)
3877 {
3878 auto const tokens = ammAlice.deposit(dan, USD(100));
3879 ammAlice.withdraw(dan, tokens, USD(0));
3880 }
3881 // dan pays ~9.94USD, which is ~10 times more in fees than
3882 // carol, bob, ed. the discounted fee is 10 times less
3883 // than the trading fee.
3884
3885 BEAST_EXPECT(env.balance(dan, USD) == STAmount(USD, UINT64_C(19'490'05672274398), -11));
3886 // USD pool gains more in dan's fees.
3887 BEAST_EXPECT(ammAlice.expectBalances(
3888 btc(13'000'000'003), STAmount{USD, UINT64_C(13'012'92609877021), -11}, ammTokens));
3889 // Discounted fee payment
3890 ammAlice.deposit(carol_, USD(100));
3891 ammTokens = ammAlice.getLPTokensBalance();
3892 BEAST_EXPECT(ammAlice.expectBalances(
3893 MPT(ammAlice[0])(13'000'000'003),
3894 STAmount{USD, UINT64_C(13'112'92609877019), -11},
3895 ammTokens));
3896 env(pay(carol_, bob_, USD(100)), Path(~USD), Sendmax(btc(110'000'000)));
3897 env.close();
3898 // carol pays 100000 drops in fees
3899 // 99900668MPT swapped in for 100USD
3900 BEAST_EXPECT(ammAlice.expectBalances(
3901 btc(13'100'000'671), STAmount{USD, UINT64_C(13'012'92609877019), -11}, ammTokens));
3902
3903 // Payment with the trading fee
3904 env(pay(alice_, carol_, btc(100'000'000)), Path(~MPT(ammAlice[0])), Sendmax(USD(110)));
3905 env.close();
3906 // alice pays ~1.011USD in fees, which is ~10 times more
3907 // than carol's fee
3908 // 100.099431529USD swapped in for 100MPT
3909
3910 BEAST_EXPECT(ammAlice.expectBalances(
3911 btc(13'000'000'671), STAmount{USD, UINT64_C(13'114'03663044931), -11}, ammTokens));
3912
3913 // Auction slot expired, no discounted fee
3915 // clock is parent's based
3916 env.close();
3917
3918 BEAST_EXPECT(
3919 env.balance(carol_, USD) == STAmount(USD, UINT64_C(29'399'00572620547), -11));
3920 ammTokens = ammAlice.getLPTokensBalance();
3921 for (int i = 0; i < 10; ++i)
3922 {
3923 auto const tokens = ammAlice.deposit(carol_, USD(100));
3924 ammAlice.withdraw(carol_, tokens, USD(0));
3925 }
3926 // carol pays ~9.94USD in fees, which is ~10 times more in
3927 // trading fees vs discounted fee.
3928
3929 BEAST_EXPECT(
3930 env.balance(carol_, USD) == STAmount(USD, UINT64_C(29'389'06197177122), -11));
3931 BEAST_EXPECT(ammAlice.expectBalances(
3932 btc(13'000'000'671), STAmount{USD, UINT64_C(13'123'98038488356), -11}, ammTokens));
3933
3934 env(pay(carol_, bob_, USD(100)), Path(~USD), Sendmax(btc(110'000'000)));
3935 env.close();
3936 // carol pays ~1.008MPT in trading fee, which is
3937 // ~10 times more than the discounted fee.
3938 // 99.815876MPT is swapped in for 100USD
3939 BEAST_EXPECT(ammAlice.expectBalances(
3940 btc(13'100'824'793), STAmount{USD, UINT64_C(13'023'98038488356), -11}, ammTokens));
3941 }
3942
3943 // Bid tiny amount
3944 testAMM(
3945 [&](AMM& ammAlice, Env& env) {
3946 // Bid a tiny amount
3948 env(ammAlice.bid({.account = alice_, .bidMin = IOUAmount{tiny}}));
3949 // Auction slot purchase price is equal to the tiny amount
3950 // since the minSlotPrice is 0 with no trading fee.
3951 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny}));
3952 // The purchase price is too small to affect the total tokens
3953 BEAST_EXPECT(ammAlice.expectBalances(
3954 MPT(ammAlice[0])(10'000'000'000), USD(10'000), ammAlice.tokens()));
3955 // Bid the tiny amount
3956 env(ammAlice.bid({
3957 .account = alice_,
3958 .bidMin = IOUAmount{STAmount::kMinValue, STAmount::kMinOffset},
3959 }));
3960 // Pay slightly higher price
3961 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny * Number{105, -2}}));
3962 // The purchase price is still too small to affect the total
3963 // tokens
3964 BEAST_EXPECT(ammAlice.expectBalances(
3965 MPT(ammAlice[0])(10'000'000'000), USD(10'000), ammAlice.tokens()));
3966 },
3967 {{gAmmmpt(10'000'000'000), USD(10'000)}});
3968
3969 // Reset auth account
3970 testAMM(
3971 [&](AMM& ammAlice, Env& env) {
3972 env(ammAlice.bid({
3973 .account = alice_,
3974 .bidMin = IOUAmount{100},
3975 .authAccounts = {carol_},
3976 }));
3977 BEAST_EXPECT(ammAlice.expectAuctionSlot({carol_}));
3978 env(ammAlice.bid({.account = alice_, .bidMin = IOUAmount{100}}));
3979 BEAST_EXPECT(ammAlice.expectAuctionSlot({}));
3980 Account bob("bob");
3981 Account dan("dan");
3982 fund(env, {bob, dan}, XRP(1'000));
3983 env(ammAlice.bid({
3984 .account = alice_,
3985 .bidMin = IOUAmount{100},
3986 .authAccounts = {bob, dan},
3987 }));
3988 BEAST_EXPECT(ammAlice.expectAuctionSlot({bob, dan}));
3989 },
3990 {{gAmmmpt(10'000'000'000), USD(10'000)}});
3991
3992 // Bid all tokens, still own the slot and trade at a discount
3993 {
3994 Env env(*this);
3995 env.fund(XRP(2'000), gw_, alice_, bob_);
3996 MPTTester const btc(
3997 {.env = env,
3998 .issuer = gw_,
3999 .holders = {alice_, bob_},
4000 .pay = 2'000'000'000,
4001 .flags = kMptDexFlags});
4002 fund(env, gw_, {alice_, bob_}, {USD(2'000)}, Fund::TokenOnly);
4003 AMM amm(env, gw_, btc(1'000'000'000), USD(1'010), false, 1'000);
4004 auto const lpIssue = amm.lptIssue();
4005 env.trust(STAmount{lpIssue, 500}, alice_);
4006 env.trust(STAmount{lpIssue, 50}, bob_);
4007 env(pay(gw_, alice_, STAmount{lpIssue, 500}));
4008 env(pay(gw_, bob_, STAmount{lpIssue, 50}));
4009 // Alice doesn't have anymore lp tokens
4010 env(amm.bid({.account = alice_, .bidMin = 500}));
4011 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{500}));
4012 BEAST_EXPECT(expectHolding(env, alice_, STAmount{lpIssue, 0}));
4013 // But trades with the discounted fee since she still owns the slot.
4014 // Alice pays ~10011 MPT in fees
4015 env(pay(alice_, bob_, USD(10)), Path(~USD), Sendmax(btc(11'000'000)));
4016 BEAST_EXPECT(amm.expectBalances(
4017 btc(1'010'010'011), USD(1'000), IOUAmount{1'004'487'562112089, -9}));
4018
4019 // Bob pays the full fee ~0.1USD
4020 env(pay(bob_, alice_, btc(10'000'000)), Path(~MPT(btc)), Sendmax(USD(11)));
4021
4022 BEAST_EXPECT(amm.expectBalances(
4023 btc(1'000'010'011),
4024 STAmount{USD, UINT64_C(1'010'10090898081), -11},
4025 IOUAmount{1'004'487'562112089, -9}));
4026 }
4027
4028 // preflight tests
4029 {
4030 Env env(*this, features);
4031 env.fund(XRP(2'000), gw_, alice_, bob_);
4032 MPTTester const btc(
4033 {.env = env,
4034 .issuer = gw_,
4035 .holders = {alice_, bob_},
4036 .pay = 2'000,
4037 .flags = kMptDexFlags});
4038 AMM amm(env, gw_, XRP(1'000), btc(1'010), false, 1'000);
4039 json::Value const tx = amm.bid({.account = alice_, .bidMin = 500});
4040
4041 {
4042 auto jtx = env.jt(tx, Seq(1), Fee(10));
4043 env.app().config().features.erase(featureMPTokensV2);
4044 PreflightContext const pfctx(
4045 env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
4046 auto pf = AMMBid::checkExtraFeatures(pfctx);
4047 BEAST_EXPECT(pf == false);
4048 env.app().config().features.insert(featureMPTokensV2);
4049 }
4050
4051 {
4052 auto jtx = env.jt(tx, Seq(1), Fee(10));
4053 jtx.jv["Asset2"]["currency"] = "XRP";
4054 jtx.jv["Asset2"].removeMember("mpt_issuance_id");
4055 jtx.stx = env.ust(jtx);
4056 PreflightContext const pfctx(
4057 env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
4058 auto pf = AMMBid::preflight(pfctx);
4059 BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
4060 }
4061 }
4062 }
4063
4064 void
4066 {
4067 testcase("Clawback");
4068 using namespace jtx;
4069
4070 Env env(*this);
4071 env.fund(XRP(2'000), gw_, alice_);
4072 MPT const btc = MPTTester(
4073 {.env = env, .issuer = gw_, .holders = {alice_}, .transferFee = 1'500, .pay = 40'000});
4074
4075 // alice creates AMM
4076 AMM const amm(env, alice_, XRP(1'000), btc(1'000));
4077
4078 // gw owns MPTIssuance, not allowed to set asfAllowTrustLineClawback
4079 env(fset(gw_, asfAllowTrustLineClawback), Ter(tecOWNERS));
4080 }
4081
4082 void
4084 {
4085 testcase("test clawback from AMM account");
4086 using namespace jtx;
4087
4088 Env env(*this, features);
4089 env.fund(XRP(1'000), gw_);
4090 env(fset(gw_, asfAllowTrustLineClawback));
4091 fund(env, gw_, {alice_}, XRP(1'000), {USD(1'000)}, Fund::Acct);
4092
4093 MPTTester const btc(
4094 {.env = env,
4095 .issuer = gw_,
4096 .holders = {alice_},
4097 .pay = 30'000,
4098 .flags = tfMPTCanLock | kMptDexFlags});
4099
4100 // to clawback from AMM account, must use AMMClawback instead of
4101 // Clawback
4102 auto const err = features[featureSingleAssetVault] ? tecPSEUDO_ACCOUNT : tecAMM_ACCOUNT;
4103 AMM const amm(env, gw_, XRP(100), btc(100));
4104 auto amount = amountFromString(amm.lptIssue(), "10");
4105 env(claw(gw_, amount), Ter(err));
4106
4107 AMM const amm1(env, alice_, USD(100), btc(200));
4108 auto amount1 = amountFromString(amm1.lptIssue(), "10");
4109 env(claw(gw_, amount1), Ter(err));
4110 }
4111
4112 void
4114 {
4115 testcase("Invalid AMM Payment");
4116 using namespace jtx;
4117 using namespace jtx::paychan;
4118 using namespace std::chrono;
4119 using namespace std::literals::chrono_literals;
4120
4121 // Can't pay into AMM account.
4122 // Can't pay out since there is no keys
4123 for (auto const& acct : {gw_, alice_})
4124 {
4125 {
4126 Env env(*this);
4127 fund(env, gw_, {alice_, carol_}, XRP(1'000));
4128 MPTTester const btc(
4129 {.env = env,
4130 .issuer = gw_,
4131 .holders = {alice_, carol_},
4132 .pay = 100,
4133 .flags = kMptDexFlags});
4134 // XRP balance is below reserve
4135 AMM const ammAlice(env, acct, XRP(10), btc(10));
4136 // Pay below reserve
4137 env(pay(carol_, ammAlice.ammAccount(), XRP(10)), Ter(tecNO_PERMISSION));
4138 // Pay above reserve
4139 env(pay(carol_, ammAlice.ammAccount(), XRP(300)), Ter(tecNO_PERMISSION));
4140 // Pay MPT
4141 env(pay(carol_, ammAlice.ammAccount(), btc(10)), Ter(tecNO_PERMISSION));
4142 }
4143
4144 {
4145 Env env(*this);
4146 fund(env, gw_, {alice_, carol_}, XRP(10'000'000));
4147 MPTTester const btc(
4148 {.env = env,
4149 .issuer = gw_,
4150 .holders = {alice_, carol_},
4151 .pay = 20'000,
4152 .flags = kMptDexFlags});
4153 // XRP balance is above reserve
4154 AMM const ammAlice(env, acct, XRP(1'000'000), btc(10'000));
4155 // Pay below reserve
4156 env(pay(carol_, ammAlice.ammAccount(), XRP(10)), Ter(tecNO_PERMISSION));
4157 // Pay above reserve
4158 env(pay(carol_, ammAlice.ammAccount(), XRP(1'000'000)), Ter(tecNO_PERMISSION));
4159 // Pay MPT
4160 env(pay(carol_, ammAlice.ammAccount(), btc(1'000)), Ter(tecNO_PERMISSION));
4161 }
4162 }
4163
4164 // Can't pay into AMM with escrow.
4165 testAMM(
4166 [&](AMM& ammAlice, Env& env) {
4167 env(escrow::create(carol_, ammAlice.ammAccount(), MPT(ammAlice[1])(1)),
4169 escrow::kFinishTime(env.now() + 1s),
4170 escrow::kCancelTime(env.now() + 2s),
4171 Fee(1'500),
4173
4174 env(escrow::create(carol_, ammAlice.ammAccount(), XRP(1)),
4176 escrow::kFinishTime(env.now() + 1s),
4177 escrow::kCancelTime(env.now() + 2s),
4178 Fee(1'500),
4180 },
4181 {{XRP(10'000), gAmmmpt(10'000)}});
4182
4183 // Can't pay into AMM with paychan.
4184 testAMM(
4185 [&](AMM& ammAlice, Env& env) {
4186 auto const pk = carol_.pk();
4187 auto const settleDelay = 10s;
4188 NetClock::time_point const cancelAfter =
4189 env.current()->header().parentCloseTime + 20s;
4190 env(create(
4191 carol_,
4192 ammAlice.ammAccount(),
4193 MPT(ammAlice[1])(1'000),
4194 settleDelay,
4195 pk,
4196 cancelAfter),
4198
4199 env(create(carol_, ammAlice.ammAccount(), XRP(1'000), settleDelay, pk, cancelAfter),
4201 },
4202 {{XRP(10'000), gAmmmpt(10'000)}});
4203
4204 // Can't pay into AMM with checks.
4205 testAMM(
4206 [&](AMM& ammAlice, Env& env) {
4207 env(check::create(env.master.id(), ammAlice.ammAccount(), XRP(100)),
4209
4210 env(check::create(env.master.id(), ammAlice.ammAccount(), MPT(ammAlice[1])(100)),
4212 },
4213 {{XRP(10'000), gAmmmpt(10'000)}});
4214
4215 // Pay amounts close to one side of the pool
4216 testAMM(
4217 [&](AMM& ammAlice, Env& env) {
4218 auto const& btc = MPT(ammAlice[1]);
4219 // Can't consume whole pool
4220 env(pay(alice_, carol_, USD(100)),
4221 Path(~USD),
4222 Sendmax(btc(1'000'000'000)),
4224 env(pay(alice_, carol_, btc(100'000'000)),
4225 Path(~btc),
4226 Sendmax(USD(1'000'000'000)),
4228 // Overflow
4229 env(pay(alice_, carol_, STAmount{USD, UINT64_C(99'999999999), -9}),
4230 Path(~USD),
4231 Sendmax(btc(1'000'000'000)),
4233 env(pay(alice_, carol_, STAmount{USD, UINT64_C(999'99999999), -8}),
4234 Path(~USD),
4235 Sendmax(btc(1'000'000'000)),
4237 env(pay(alice_, carol_, btc(99'999'999)),
4238 Path(~btc),
4239 Sendmax(USD(1'000'000'000)),
4241 // Sender doesn't have enough funds
4242 env(pay(alice_, carol_, USD(99.99)),
4243 Path(~USD),
4244 Sendmax(btc(1'000'000'000)),
4246 env(pay(alice_, carol_, btc(99'990'000)),
4247 Path(~btc),
4248 Sendmax(USD(1'000'000'000)),
4250 },
4251 {{USD(100), gAmmmpt(100'000'000)}});
4252
4253 // Globally locked MPT.
4254 {
4255 Env env(*this);
4256 env.fund(XRP(30'000), gw_, alice_, carol_);
4257 MPTTester btc(
4258 {.env = env,
4259 .issuer = gw_,
4260 .holders = {alice_, carol_},
4261 .pay = 30'000,
4262 .flags = tfMPTCanLock | kMptDexFlags});
4263
4264 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000));
4265 btc.set({.flags = tfMPTLock});
4266
4267 env(pay(alice_, carol_, btc(1)),
4268 Path(~static_cast<MPT>(btc)),
4269 Txflags(tfPartialPayment | tfNoRippleDirect),
4270 Sendmax(XRP(10)),
4271 Ter(tecPATH_DRY));
4272 env(pay(alice_, carol_, XRP(1)),
4273 Path(~XRP),
4274 Txflags(tfPartialPayment | tfNoRippleDirect),
4275 Sendmax(btc(10)),
4276 Ter(tecPATH_DRY));
4277 }
4278
4279 // Individually locked MPT destination account.
4280 {
4281 Env env(*this);
4282 env.fund(XRP(30'000), gw_, alice_, carol_);
4283 MPTTester btc(
4284 {.env = env,
4285 .issuer = gw_,
4286 .holders = {alice_, carol_},
4287 .pay = 30'000,
4288 .flags = tfMPTCanLock | kMptDexFlags});
4289
4290 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000));
4291 btc.set({.holder = carol_, .flags = tfMPTLock});
4292
4293 env(pay(alice_, carol_, btc(1)),
4294 Path(~static_cast<MPT>(btc)),
4295 Txflags(tfPartialPayment | tfNoRippleDirect),
4296 Sendmax(XRP(10)),
4297 Ter(tecPATH_DRY));
4298 }
4299
4300 // Individually locked MPT source account
4301 {
4302 Env env(*this);
4303 env.fund(XRP(30'000), gw_, alice_, carol_);
4304 MPTTester btc(
4305 {.env = env,
4306 .issuer = gw_,
4307 .holders = {alice_, carol_},
4308 .pay = 30'000,
4309 .flags = tfMPTCanLock | kMptDexFlags});
4310
4311 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000));
4312 btc.set({.holder = alice_, .flags = tfMPTLock});
4313
4314 env(pay(alice_, carol_, XRP(1)),
4315 Path(~XRP),
4316 Txflags(tfPartialPayment | tfNoRippleDirect),
4317 Sendmax(btc(10)),
4318 Ter(tecPATH_DRY));
4319 }
4320
4321 // lock on both sides
4322 {
4323 Env env(*this);
4324 env.fund(XRP(30'000), gw_, alice_, carol_);
4325 MPTTester btc(
4326 {.env = env,
4327 .issuer = gw_,
4328 .holders = {alice_, carol_},
4329 .pay = 30'000,
4330 .flags = tfMPTCanLock | kMptDexFlags});
4331 MPTTester eth(
4332 {.env = env,
4333 .issuer = gw_,
4334 .holders = {alice_, carol_},
4335 .pay = 30'000,
4336 .flags = tfMPTCanLock | kMptDexFlags});
4337
4338 AMM const ammAlice(env, alice_, eth(10'000), btc(10'000));
4339 btc.set({.holder = carol_, .flags = tfMPTLock});
4340 btc.set({.holder = alice_, .flags = tfMPTLock});
4341 eth.set({.holder = carol_, .flags = tfMPTLock});
4342 eth.set({.holder = alice_, .flags = tfMPTLock});
4343
4344 env(pay(alice_, carol_, eth(1)),
4345 Path(~MPT(eth)),
4346 Txflags(tfPartialPayment | tfNoRippleDirect),
4347 Sendmax(btc(10)),
4348 Ter(tecPATH_DRY));
4349
4350 env(pay(alice_, carol_, btc(1)),
4351 Path(~MPT(btc)),
4352 Txflags(tfPartialPayment | tfNoRippleDirect),
4353 Sendmax(eth(10)),
4354 Ter(tecPATH_DRY));
4355 }
4356
4357 // Individually locked AMM MPT
4358 {
4359 Env env(*this);
4360 env.fund(XRP(30'000), gw_, alice_, carol_);
4361 MPTTester btc(
4362 {.env = env,
4363 .issuer = gw_,
4364 .holders = {alice_, carol_},
4365 .pay = 30'000,
4366 .flags = tfMPTCanLock | kMptDexFlags});
4367
4368 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000));
4369 btc.set({.holder = ammAlice.ammAccount(), .flags = tfMPTLock});
4370
4371 env(pay(alice_, carol_, XRP(1)),
4372 Path(~XRP),
4373 Txflags(tfPartialPayment | tfNoRippleDirect),
4374 Sendmax(btc(10)),
4375 Ter(tecPATH_DRY));
4376 }
4377 }
4378
4379 void
4381 {
4382 testcase("Basic Payment");
4383 using namespace jtx;
4384
4385 // Payment 100MPT for 100XRP.
4386 // Force one path with tfNoRippleDirect.
4387 testAMM(
4388 [&](AMM& ammAlice, Env& env) {
4389 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
4390 env.fund(XRP(30'000), bob_);
4391 env.close();
4392 env(pay(bob_, carol_, MPT(ammAlice[1])(100)),
4393 Path(~MPT(ammAlice[1])),
4394 Sendmax(XRP(100)),
4395 Txflags(tfNoRippleDirect));
4396 env.close();
4397 BEAST_EXPECT(ammAlice.expectBalances(
4398 XRP(10'100), MPT(ammAlice[1])(10'000), ammAlice.tokens()));
4399 // Initial balance + 100
4400 env.require(Balance(carol_, carolMPT + MPT(ammAlice[1])(100)));
4401 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
4402 BEAST_EXPECT(
4403 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(100) - txFee(env, 1)));
4404 },
4405 {{XRP(10'000), gAmmmpt(10'100)}});
4406
4407 // Payment 100IOU/MPT for 100IOU/MPT. Test IOU/MPT mix.
4408 // Force one path with tfNoRippleDirect.
4409 {
4410 auto test = [&](auto&& issue1, auto&& issue2) {
4411 Env env(*this);
4412 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
4413 env.close();
4414
4415 auto const usd = issue1(
4416 {.env = env,
4417 .token = "USD",
4418 .issuer = gw_,
4419 .holders = {alice_, bob_, carol_},
4420 .limit = 1'000'000});
4421 auto const btc = issue2(
4422 {.env = env,
4423 .token = "BTC",
4424 .issuer = gw_,
4425 .holders = {alice_, bob_, carol_},
4426 .limit = 1'000'000});
4427
4428 env(pay(gw_, alice_, btc(50000)));
4429 env(pay(gw_, bob_, btc(50000)));
4430 env(pay(gw_, carol_, btc(50000)));
4431 env(pay(gw_, alice_, usd(50000)));
4432 env(pay(gw_, bob_, usd(50000)));
4433 env(pay(gw_, carol_, usd(50000)));
4434 env.close();
4435
4436 auto ammAlice = AMM(env, alice_, usd(10000), btc(10100));
4437 auto carolBTC = env.balance(carol_, btc);
4438 auto bobUSD = env.balance(bob_, usd);
4439 env(pay(bob_, carol_, btc(100)),
4440 Path(~btc),
4441 Sendmax(usd(100)),
4442 Txflags(tfNoRippleDirect | tfPartialPayment));
4443 env.close();
4444 BEAST_EXPECT(ammAlice.expectBalances(usd(10'100), btc(10'000), ammAlice.tokens()));
4445 env.require(Balance(carol_, carolBTC + btc(100)));
4446 env.require(Balance(bob_, bobUSD - usd(100)));
4447 };
4449 }
4450
4451 // Payment 100MPT for 100XRP, use default path.
4452 testAMM(
4453 [&](AMM& ammAlice, Env& env) {
4454 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
4455 env.fund(XRP(30'000), bob_);
4456 env.close();
4457 env(pay(bob_, carol_, MPT(ammAlice[1])(100)), Sendmax(XRP(100)));
4458 env.close();
4459 BEAST_EXPECT(ammAlice.expectBalances(
4460 XRP(10'100), MPT(ammAlice[1])(10'000), ammAlice.tokens()));
4461 // Initial balance + 100
4462 env.require(Balance(carol_, carolMPT + MPT(ammAlice[1])(100)));
4463 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
4464 BEAST_EXPECT(
4465 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(100) - txFee(env, 1)));
4466 },
4467 {{XRP(10'000), gAmmmpt(10'100)}});
4468
4469 // Payment 100IOU/MPT for 100IOU/MPT using default path.
4470 // Test IOU/MPT mix.
4471 {
4472 auto test = [&](auto&& issue1, auto&& issue2) {
4473 Env env(*this);
4474 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
4475 env.close();
4476
4477 auto const usd = issue1(
4478 {.env = env,
4479 .token = "USD",
4480 .issuer = gw_,
4481 .holders = {alice_, bob_, carol_},
4482 .limit = 1'000'000});
4483 auto const btc = issue2(
4484 {.env = env,
4485 .token = "BTC",
4486 .issuer = gw_,
4487 .holders = {alice_, bob_, carol_},
4488 .limit = 1'000'000});
4489
4490 env(pay(gw_, alice_, btc(50000)));
4491 env(pay(gw_, bob_, btc(50000)));
4492 env(pay(gw_, carol_, btc(50000)));
4493 env(pay(gw_, alice_, usd(50000)));
4494 env(pay(gw_, bob_, usd(50000)));
4495 env(pay(gw_, carol_, usd(50000)));
4496 env.close();
4497
4498 auto ammAlice = AMM(env, alice_, usd(10000), btc(10100));
4499 auto carolBTC = env.balance(carol_, btc);
4500 auto bobUSD = env.balance(bob_, usd);
4501 env(pay(bob_, carol_, btc(100)), Sendmax(usd(100)));
4502 env.close();
4503 BEAST_EXPECT(ammAlice.expectBalances(usd(10'100), btc(10'000), ammAlice.tokens()));
4504 env.require(Balance(carol_, carolBTC + btc(100)));
4505 env.require(Balance(bob_, bobUSD - usd(100)));
4506 };
4508 }
4509
4510 // This payment is identical to above. While it has
4511 // both default path and path, activeStrands has one path.
4512 testAMM(
4513 [&](AMM& ammAlice, Env& env) {
4514 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
4515 env.fund(XRP(30'000), bob_);
4516 env.close();
4517 env(pay(bob_, carol_, MPT(ammAlice[1])(100)),
4518 Path(~MPT(ammAlice[1])),
4519 Sendmax(XRP(100)));
4520 env.close();
4521 BEAST_EXPECT(ammAlice.expectBalances(
4522 XRP(10'100), MPT(ammAlice[1])(10'000), ammAlice.tokens()));
4523 // Initial balance + 100
4524 env.require(Balance(carol_, carolMPT + MPT(ammAlice[1])(100)));
4525 // Initial balance 30,000 - 100(sendmax) - 10(tx fee)
4526 BEAST_EXPECT(
4527 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(100) - txFee(env, 1)));
4528 },
4529 {{XRP(10'000), gAmmmpt(10'100)}});
4530
4531 // Test MPT/IOU combination for the case above.
4532 {
4533 auto test = [&](auto&& issue1, auto&& issue2) {
4534 Env env(*this);
4535 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
4536 env.close();
4537
4538 auto const usd = issue1(
4539 {.env = env,
4540 .token = "USD",
4541 .issuer = gw_,
4542 .holders = {alice_, bob_, carol_},
4543 .limit = 1'000'000});
4544 auto const btc = issue2(
4545 {.env = env,
4546 .token = "BTC",
4547 .issuer = gw_,
4548 .holders = {alice_, bob_, carol_},
4549 .limit = 1'000'000});
4550
4551 env(pay(gw_, alice_, btc(50000)));
4552 env(pay(gw_, bob_, btc(50000)));
4553 env(pay(gw_, carol_, btc(50000)));
4554 env(pay(gw_, alice_, usd(50000)));
4555 env(pay(gw_, bob_, usd(50000)));
4556 env(pay(gw_, carol_, usd(50000)));
4557 env.close();
4558
4559 auto ammAlice = AMM(env, alice_, usd(10000), btc(10100));
4560 auto carolBTC = env.balance(carol_, btc);
4561 auto bobUSD = env.balance(bob_, usd);
4562 env(pay(bob_, carol_, btc(100)), Path(~btc), Sendmax(usd(100)));
4563 env.close();
4564 BEAST_EXPECT(ammAlice.expectBalances(usd(10'100), btc(10'000), ammAlice.tokens()));
4565 env.require(Balance(carol_, carolBTC + btc(100)));
4566 env.require(Balance(bob_, bobUSD - usd(100)));
4567 };
4569 }
4570
4571 // Payment with limitQuality set.
4572 testAMM(
4573 [&](AMM& ammAlice, Env& env) {
4574 auto carolMPT = env.balance(carol_, MPT(ammAlice[1]));
4575 env.fund(jtx::XRP(30'000), bob_);
4576 env.close();
4577 // Pays 10MPT for 10XRP. A larger payment of ~99.11MPT/100XRP
4578 // would have been sent has it not been for limitQuality.
4579 env(pay(bob_, carol_, MPT(ammAlice[1])(100)),
4580 Path(~MPT(ammAlice[1])),
4581 Sendmax(XRP(100)),
4582 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
4583 env.close();
4584 BEAST_EXPECT(ammAlice.expectBalances(
4585 XRP(10'010), MPT(ammAlice[1])(10'000), ammAlice.tokens()));
4586 // Initial balance + 10(limited by limitQuality)
4587 env.require(Balance(carol_, carolMPT + MPT(ammAlice[1])(10)));
4588 // Initial balance 30,000 - 10(limited by limitQuality) - 10(tx
4589 // fee)
4590 BEAST_EXPECT(
4591 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(10) - txFee(env, 1)));
4592
4593 // Fails because of limitQuality. Would have sent
4594 // ~98.91MPT/110XRP has it not been for limitQuality.
4595 env(pay(bob_, carol_, MPT(ammAlice[1])(100)),
4596 Path(~MPT(ammAlice[1])),
4597 Sendmax(XRP(100)),
4598 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality),
4599 Ter(tecPATH_DRY));
4600 env.close();
4601 },
4602 {{XRP(10'000), gAmmmpt(10'010)}});
4603
4604 // Payment with limitQuality set. MPT/IOU combination.
4605 {
4606 auto test = [&](auto&& issue1, auto&& issue2) {
4607 Env env(*this);
4608 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
4609 env.close();
4610
4611 auto const usd = issue1(
4612 {.env = env,
4613 .token = "USD",
4614 .issuer = gw_,
4615 .holders = {alice_, bob_, carol_},
4616 .limit = 1'000'000});
4617 auto const btc = issue2(
4618 {.env = env,
4619 .token = "BTC",
4620 .issuer = gw_,
4621 .holders = {alice_, bob_, carol_},
4622 .limit = 1'000'000});
4623
4624 env(pay(gw_, alice_, btc(50000)));
4625 env(pay(gw_, bob_, btc(50000)));
4626 env(pay(gw_, carol_, btc(50000)));
4627 env(pay(gw_, alice_, usd(50000)));
4628 env(pay(gw_, bob_, usd(50000)));
4629 env(pay(gw_, carol_, usd(50000)));
4630 env.close();
4631
4632 auto ammAlice = AMM(env, alice_, usd(10000), btc(10010));
4633 auto carolBTC = env.balance(carol_, btc);
4634 auto bobUSD = env.balance(bob_, usd);
4635 env(pay(bob_, carol_, btc(100)),
4636 Path(~btc),
4637 Sendmax(usd(100)),
4638 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
4639 env.close();
4640 BEAST_EXPECT(ammAlice.expectBalances(usd(10'010), btc(10'000), ammAlice.tokens()));
4641 env.require(Balance(carol_, carolBTC + btc(10)));
4642 env.require(Balance(bob_, bobUSD - usd(10)));
4643
4644 env(pay(bob_, carol_, btc(100)),
4645 Path(~btc),
4646 Sendmax(usd(100)),
4647 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality),
4648 Ter(tecPATH_DRY));
4649 env.close();
4650 };
4652 }
4653
4654 // Payment with limitQuality and transfer fee set.
4655 {
4656 Env env(*this);
4657 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
4658 env.close();
4659 MPTTester const btc(
4660 {.env = env,
4661 .issuer = gw_,
4662 .holders = {alice_, carol_},
4663 .transferFee = 10'000,
4664 .pay = 30'000'000'000'000'000,
4665 .flags = kMptDexFlags});
4666 auto ammAlice = AMM(env, alice_, XRP(10'000), btc(10'010'000'000'000'000));
4667 env.close();
4668 auto carolMPT = env.balance(carol_, MPT(btc));
4669 // Pays 10'000'000'000'000MPT for 10XRP. A larger payment of
4670 // ~99'110'000'000'000MPT/100XRP would have been sent has it not
4671 // been for limitQuality and the transfer fee.
4672 env(pay(bob_, carol_, btc(100'000'000'000'000)),
4673 Path(~MPT(btc)),
4674 Sendmax(XRP(110)),
4675 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
4676 env.close();
4677 BEAST_EXPECT(ammAlice.expectBalances(
4678 XRP(10'010), btc(10'000'000'000'000'000), ammAlice.tokens()));
4679 // 10'000'000'000'000MPT - 10% transfer fee
4680 env.require(Balance(carol_, carolMPT + btc(9'090'909'090'909)));
4681 BEAST_EXPECT(expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(10) - txFee(env, 1)));
4682 }
4683
4684 // Payment with limitQuality and transfer fee set. MPT/IOU combination.
4685 {
4686 auto test = [&](auto&& issue1, auto&& issue2) {
4687 Env env(*this);
4688 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
4689 env.close();
4690
4691 auto const usd = issue1({
4692 .env = env,
4693 .token = "USD",
4694 .issuer = gw_,
4695 .holders = {alice_, bob_, carol_},
4696 .limit = 1'000'000
4697 //.transferFee = 10'000
4698 });
4699 auto const btc = issue2(
4700 {.env = env,
4701 .token = "BTC",
4702 .issuer = gw_,
4703 .holders = {alice_, bob_, carol_},
4704 .limit = 300'000'000'000'000'000,
4705 .transferFee = 10'000});
4706
4707 env(pay(gw_, alice_, btc(30'000'000'000'000'000)));
4708 env(pay(gw_, bob_, btc(30'000'000'000'000'000)));
4709 env(pay(gw_, carol_, btc(30'000'000'000'000'000)));
4710 env(pay(gw_, alice_, usd(50000)));
4711 env(pay(gw_, bob_, usd(50000)));
4712 env(pay(gw_, carol_, usd(50000)));
4713 env.close();
4714
4715 auto ammAlice = AMM(env, alice_, usd(10'000), btc(10'010'000'000'000'000));
4716 auto carolBTC = env.balance(carol_, btc);
4717 auto bobUSD = env.balance(bob_, usd);
4718 env(pay(bob_, carol_, btc(100'000'000'000'000)),
4719 Path(~btc),
4720 Sendmax(usd(110)),
4721 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
4722 env.close();
4723 BEAST_EXPECT(ammAlice.expectBalances(
4724 usd(10'010), btc(10'000'000'000'000'000), ammAlice.tokens()));
4725 env.require(Balance(carol_, carolBTC + btc(9'090'909'090'909)));
4726 env.require(Balance(bob_, bobUSD - usd(10)));
4727 };
4729 }
4730
4731 // Fail when partial payment is not set.
4732 testAMM(
4733 [&](AMM& ammAlice, Env& env) {
4734 env.fund(jtx::XRP(30'000), bob_);
4735 env.close();
4736 env(pay(bob_, carol_, MPT(ammAlice[1])(100)),
4737 Path(~MPT(ammAlice[1])),
4738 Sendmax(XRP(100)),
4739 Txflags(tfNoRippleDirect),
4741 },
4742 {{XRP(10'000), gAmmmpt(10'000)}});
4743 // Fail when partial payment is not set. MPT/IOU combination.
4744 {
4745 auto test = [&](auto&& issue1, auto&& issue2) {
4746 Env env(*this);
4747 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
4748 env.close();
4749
4750 auto const usd = issue1(
4751 {.env = env,
4752 .token = "USD",
4753 .issuer = gw_,
4754 .holders = {alice_, bob_, carol_},
4755 .limit = 1'000'000});
4756 auto const btc = issue2(
4757 {.env = env,
4758 .token = "BTC",
4759 .issuer = gw_,
4760 .holders = {alice_, bob_, carol_},
4761 .limit = 1'000'000});
4762
4763 env(pay(gw_, alice_, btc(50000)));
4764 env(pay(gw_, bob_, btc(50000)));
4765 env(pay(gw_, carol_, btc(50000)));
4766 env(pay(gw_, alice_, usd(50000)));
4767 env(pay(gw_, bob_, usd(50000)));
4768 env(pay(gw_, carol_, usd(50000)));
4769 env.close();
4770
4771 auto ammAlice = AMM(env, alice_, usd(10000), btc(10000));
4772 env(pay(bob_, carol_, btc(100)),
4773 Path(~btc),
4774 Sendmax(usd(100)),
4775 Txflags(tfNoRippleDirect),
4777 };
4779 }
4780
4781 // Non-default path (with AMM) has a better quality than default path.
4782 // The max possible liquidity is taken out of non-default
4783 // path ~29.9e14XRP/29.9e14ETH, ~29.9e14ETH/~29.99e14btc. The rest
4784 // is taken from the offer.
4785 {
4786 Env env(*this);
4787 env.fund(XRP(30'000), gw_, alice_, carol_);
4788 MPTTester const btc(
4789 {.env = env,
4790 .issuer = gw_,
4791 .holders = {alice_, carol_},
4792 .pay = 3'000'000'000'000'000'000,
4793 .flags = kMptDexFlags});
4794 MPTTester const eth(
4795 {.env = env,
4796 .issuer = gw_,
4797 .holders = {alice_, carol_},
4798 .pay = 3'000'000'000'000'000'000,
4799 .flags = kMptDexFlags});
4800 env.fund(XRP(1'000), bob_);
4801 env.close();
4802 auto ammEthXrp = AMM(env, alice_, XRP(10'000), eth(1'000'000'000'000'000'000));
4803 auto ammBtcEth =
4804 AMM(env, alice_, eth(1'000'000'000'000'000'000), btc(1'000'000'000'000'000'000));
4805 env(offer(alice_, XRP(101), btc(10'000'000'000'000'000)), Txflags(tfPassive));
4806 env.close();
4807 env(pay(bob_, carol_, btc(10'000'000'000'000'000)),
4808 Path(~MPT(eth), ~MPT(btc)),
4809 Sendmax(XRP(102)),
4810 Txflags(tfPartialPayment));
4811 env.close();
4812 BEAST_EXPECT(ammEthXrp.expectBalances(
4813 XRPAmount(10'030'082'730), eth(9'970'00749812546872), ammEthXrp.tokens()));
4814
4815 BEAST_EXPECT(ammBtcEth.expectBalances(
4816 btc(9'970'09727766213961), eth(10'029'99250187453128), ammBtcEth.tokens()));
4817
4818 Amounts const expectedAmounts = Amounts{XRPAmount(30'201'749), btc(29'90272233786039)};
4819
4820 BEAST_EXPECT(expectOffers(env, alice_, 1, {{expectedAmounts}}));
4821
4822 // Initial (30,000 + 100)e14
4823 env.require(Balance(carol_, btc(3'010'000'000'000'000'000)));
4824 // Initial 1,000 - 30082730(AMM pool) - 70798251(offer) - 10(tx fee)
4825 BEAST_EXPECT(expectLedgerEntryRoot(
4826 env,
4827 bob_,
4828 XRP(1'000) - XRPAmount{30'082'730} - XRPAmount{70'798'251} - txFee(env, 1)));
4829 }
4830
4831 // Default path (with AMM) has a better quality than a
4832 // non-default path.
4833 // The max possible liquidity is taken out of default
4834 // path ~49XRP/49e14BTC. The rest is taken from the offer.
4835 {
4836 Env env(*this);
4837 env.fund(XRP(30'000), gw_, alice_, carol_);
4838 MPTTester const btc(
4839 {.env = env,
4840 .issuer = gw_,
4841 .holders = {alice_, carol_},
4842 .pay = 3'000'000'000'000'000'000,
4843 .flags = kMptDexFlags});
4844 MPTTester const eth(
4845 {.env = env,
4846 .issuer = gw_,
4847 .holders = {alice_},
4848 .pay = 1'000'000'000'000'000'000,
4849 .flags = kMptDexFlags});
4850 auto ammAlice = AMM(env, alice_, XRP(10'000), btc(1'000'000'000'000'000'000));
4851 env.fund(XRP(1'000), bob_);
4852 env.close();
4853 env(offer(alice_, XRP(101), eth(10'000'000'000'000'000)), Txflags(tfPassive));
4854 env.close();
4855 env(offer(alice_, eth(10'000'000'000'000'000), btc(10'000'000'000'000'000)),
4856 Txflags(tfPassive));
4857 env.close();
4858 env(pay(bob_, carol_, btc(10'000'000'000'000'000)),
4859 Path(~MPT(eth), ~MPT(btc)),
4860 Sendmax(XRP(102)),
4861 Txflags(tfPartialPayment));
4862 env.close();
4863 BEAST_EXPECT(ammAlice.expectBalances(
4864 XRPAmount(10'050'238'637), btc(9'950'01249687578120), ammAlice.tokens()));
4865 BEAST_EXPECT(expectOffers(
4866 env,
4867 alice_,
4868 2,
4869 {{Amounts{XRPAmount(50'487'378), eth(49'98750312421880)},
4870 Amounts{eth(49'98750312421880), btc(49'98750312421880)}}}));
4871 // Initial (30,000 + 100)e14
4872 env.require(Balance(carol_, btc(30'100'00000000000000)));
4873 // Initial 1,000 - 50238637(AMM pool) - 50512622(offer) - 10(tx
4874 // fee)
4875 BEAST_EXPECT(expectLedgerEntryRoot(
4876 env,
4877 bob_,
4878 XRP(1'000) - XRPAmount{50'238'637} - XRPAmount{50'512'622} - txFee(env, 1)));
4879 }
4880
4881 // Default path with AMM and Order Book offer. AMM is consumed first,
4882 // remaining amount is consumed by the offer.
4883 {
4884 Env env(*this);
4885 fund(env, gw_, {alice_, bob_, carol_}, XRP(30'000));
4886 MPTTester const btc(
4887 {.env = env,
4888 .issuer = gw_,
4889 .holders = {alice_, bob_, carol_},
4890 .pay = 30'000'000'000'000'000,
4891 .flags = kMptDexFlags});
4892 AMM ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000'000'000));
4893 env(offer(bob_, XRP(100), MPT(ammAlice[1])(100'000'000'000'000)), Txflags(tfPassive));
4894 env.close();
4895 env(pay(alice_, carol_, MPT(ammAlice[1])(200'000'000'000'000)),
4896 Sendmax(XRP(200)),
4897 Txflags(tfPartialPayment));
4898 env.close();
4899
4900 BEAST_EXPECT(ammAlice.expectBalances(
4901 XRP(10'100), MPT(ammAlice[1])(10'000'000000000001), ammAlice.tokens()));
4902 env.require(Balance(carol_, MPT(ammAlice[1])(30'199'999999999999)));
4903
4904 // Initial 30,000 - 10000(AMM pool LP) - 100(AMMoffer) -
4905 // - 100(offer) - 10(tx fee) - 10(tx fee of MPTTester init as
4906 // holder) - one reserve
4907 BEAST_EXPECT(expectLedgerEntryRoot(
4908 env,
4909 alice_,
4910 XRP(30'000) - XRP(10'000) - XRP(100) - XRP(100) - ammCrtFee(env) -
4911 2 * txFee(env, 1)));
4912 BEAST_EXPECT(expectOffers(env, bob_, 0));
4913 }
4914
4915 // Default path with AMM and Order Book offer.
4916 // Order Book offer is consumed first.
4917 // Remaining amount is consumed by AMM.
4918 {
4919 Env env(*this);
4920 fund(env, gw_, {alice_, bob_, carol_}, XRP(20'000));
4921 MPTTester const btc(
4922 {.env = env,
4923 .issuer = gw_,
4924 .holders = {alice_, bob_, carol_},
4925 .pay = 2'000,
4926 .flags = kMptDexFlags});
4927 env(offer(bob_, XRP(50), btc(150)), Txflags(tfPassive));
4928 env.close();
4929 AMM const ammAlice(env, alice_, XRP(1'000), btc(1'050));
4930 env(pay(alice_, carol_, btc(200)), Sendmax(XRP(200)), Txflags(tfPartialPayment));
4931 BEAST_EXPECT(ammAlice.expectBalances(XRP(1'050), btc(1'000), ammAlice.tokens()));
4932 env.require(Balance(carol_, btc(2'200)));
4933 BEAST_EXPECT(expectOffers(env, bob_, 0));
4934 }
4935
4936 // Offer crossing XRP/MPT
4937 {
4938 Env env(*this);
4939 fund(env, gw_, {alice_, bob_, carol_}, XRP(30'000));
4940 MPTTester const btc(
4941 {.env = env,
4942 .issuer = gw_,
4943 .holders = {alice_, bob_, carol_},
4944 .pay = 30'000,
4945 .flags = kMptDexFlags});
4946 AMM ammAlice(env, alice_, XRP(10'000), btc(10'100));
4947 env(offer(bob_, MPT(ammAlice[1])(100), XRP(100)));
4948 env.close();
4949 BEAST_EXPECT(
4950 ammAlice.expectBalances(XRP(10'100), MPT(ammAlice[1])(10'000), ammAlice.tokens()));
4951 // Initial 30,000 + 100
4952 env.require(Balance(bob_, MPT(ammAlice[1])(30'100)));
4953 // Initial 30,000 - 100(offer) - 10(tx fee) - 1(tx fee for MPTTester
4954 // holder)
4955 BEAST_EXPECT(
4956 expectLedgerEntryRoot(env, bob_, XRP(30'000) - XRP(100) - 2 * txFee(env, 1)));
4957 BEAST_EXPECT(expectOffers(env, bob_, 0));
4958 }
4959
4960 // Offer crossing MPT/MPT and transfer rate
4961 // Single path AMM offer
4962 {
4963 Env env(*this);
4964 env.fund(XRP(30'000), gw_, alice_, carol_);
4965 MPTTester const btc(
4966 {.env = env,
4967 .issuer = gw_,
4968 .holders = {alice_, carol_},
4969 .transferFee = 25'000,
4970 .pay = 30'000'000'000'000'000,
4971 .flags = kMptDexFlags});
4972 MPTTester const eth(
4973 {.env = env,
4974 .issuer = gw_,
4975 .holders = {alice_, carol_},
4976 .transferFee = 25'000,
4977 .pay = 30'000'000'000'000'000,
4978 .flags = kMptDexFlags});
4979 AMM const ammAlice(env, alice_, btc(1'000'000'000'000'000), eth(1'100'000'000'000'000));
4980 // This offer succeeds to cross pre- and post-amendment
4981 // because the strand's out amount is small enough to match
4982 // limitQuality value and limitOut() function in StrandFlow
4983 // doesn't require an adjustment to out value.
4984 env(offer(carol_, eth(100'000'000'000'000), btc(100'000'000'000'000)));
4985 env.close();
4986 // No transfer fee
4987 BEAST_EXPECT(ammAlice.expectBalances(
4988 btc(1'100'000'000'000'000), eth(1'000'000'000'000'000), ammAlice.tokens()));
4989 // Initial 30,000'000'000'000'000 - 100'000'000'000'000(offer)-25 %
4990 // transfer fee
4991 env.require(Balance(carol_, btc(29'875'000'000'000'000)));
4992 // Initial 30,000'000'000'000'000 + 100'000'000'000'000(offer)
4993 env.require(Balance(carol_, eth(30'100'000'000'000'000)));
4994 BEAST_EXPECT(expectOffers(env, carol_, 0));
4995 }
4996
4997 // Single-path AMM offer
4998 {
4999 Env env(*this);
5000 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
5001 MPTTester const btc(
5002 {.env = env,
5003 .issuer = gw_,
5004 .holders = {alice_, carol_},
5005 .transferFee = 100,
5006 .pay = 30'000'000'000'000'000,
5007 .flags = kMptDexFlags});
5008 AMM const amm(env, alice_, XRP(1'000), btc(500'000'000'000'000));
5009 env(offer(carol_, XRP(100), btc(55'000'000'000'000)));
5010 env.close();
5011
5012 BEAST_EXPECT(
5013 amm.expectBalances(XRPAmount(909'090'909), btc(550'000000055001), amm.tokens()));
5014 // Offer ~91XRP/49.99e12BTC
5015 BEAST_EXPECT(expectOffers(
5016 env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, btc(4'999999950000)}}}));
5017 // Carol pays 0.1% fee on 50'000000055000BTC = 50'000000055BTC
5018 env.require(Balance(carol_, btc(29'949'949'999'944'943)));
5019 }
5020
5021 {
5022 Env env(*this);
5023 env.fund(XRP(30'000), gw_, alice_, carol_);
5024 MPTTester const btc(
5025 {.env = env,
5026 .issuer = gw_,
5027 .holders = {alice_, carol_},
5028 .transferFee = 100,
5029 .pay = 3'000'000'000'000'000'000,
5030 .flags = kMptDexFlags});
5031 AMM const amm(env, alice_, XRP(1'000), btc(50'000'000'000'000'000));
5032 env(offer(carol_, XRP(10), btc(5'500'000'000'000'000)));
5033 env.close();
5034
5035 BEAST_EXPECT(amm.expectBalances(XRP(990), btc(505'05050505050506), amm.tokens()));
5036 BEAST_EXPECT(expectOffers(env, carol_, 0));
5037 }
5038
5039 // Multi-path AMM offer
5040 {
5041 Env env(*this);
5042 Account const ed("ed");
5043 env.fund(XRP(30'000), gw_, alice_, bob_, carol_, ed);
5044 MPTTester const btc(
5045 {.env = env,
5046 .issuer = gw_,
5047 .holders = {alice_, bob_, carol_, ed},
5048 .transferFee = 25'000,
5049 .pay = 20'000'000'000'000'000,
5050 .flags = kMptDexFlags});
5051 MPTTester const eth(
5052 {.env = env,
5053 .issuer = gw_,
5054 .holders = {alice_, bob_, carol_, ed},
5055 .transferFee = 25'000,
5056 .pay = 20'000'000'000'000'000,
5057 .flags = kMptDexFlags});
5058 AMM const ammAlice(
5059 env, alice_, btc(10'000'000'000'000'000), eth(11'000'000'000'000'000));
5060
5061 env(offer(bob_, btc(100'000'000'000'000), XRP(10)), Txflags(tfPassive));
5062 env(offer(ed, XRP(10), eth(100'000'000'000'000)), Txflags(tfPassive));
5063 env.close();
5064 env(offer(carol_, eth(1'000'000'000'000'000), btc(1'000'000'000'000'000)));
5065 env.close();
5066
5067 BEAST_EXPECT(ammAlice.expectBalances(
5068 btc(1'060'6848287928033), eth(1'037'0658372213574), ammAlice.tokens()));
5069 // Consumed offer ~72.93e13ETH/72.93e13BTC
5070 BEAST_EXPECT(expectOffers(
5071 env, carol_, 1, {Amounts{eth(27'0658372213574), btc(27'0658372213575)}}));
5072 BEAST_EXPECT(expectOffers(env, bob_, 0));
5073 BEAST_EXPECT(expectOffers(env, ed, 0));
5074
5075 env.require(Balance(carol_, btc(19'116'439'640'089'955)));
5076 env.require(Balance(carol_, eth(20'729'341'627'786'426)));
5077 env.require(Balance(bob_, btc(20'100'000'000'000'000)));
5078 env.require(Balance(ed, eth(19'875'000'000'000'000)));
5079 }
5080
5081 // Payment and transfer fee
5082 // Scenario:
5083 // Bob sends 125BTC to pay 80EUR to Carol
5084 // Payment execution:
5085 // bob's 125BTC/1.25 = 100BTC
5086 // 100BTC/100EUR AMM offer
5087 // 100EUR/1.25 = 80EUR paid to carol
5088 {
5089 Env env(*this);
5090 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
5091 MPTTester const btc(
5092 {.env = env,
5093 .issuer = gw_,
5094 .holders = {alice_, bob_, carol_},
5095 .transferFee = 25'000,
5096 .pay = 30'000,
5097 .flags = kMptDexFlags});
5098 MPTTester const eth(
5099 {.env = env,
5100 .issuer = gw_,
5101 .holders = {alice_, bob_, carol_},
5102 .transferFee = 25'000,
5103 .pay = 30'000,
5104 .flags = kMptDexFlags});
5105 AMM const ammAlice(env, alice_, btc(1'000), eth(1'100));
5106 env(rate(gw_, 1.25));
5107 env.close();
5108 env(pay(bob_, carol_, eth(100)),
5109 Path(~MPT(eth)),
5110 Sendmax(btc(125)),
5111 Txflags(tfPartialPayment));
5112 env.close();
5113 BEAST_EXPECT(ammAlice.expectBalances(btc(1'100), eth(1'000), ammAlice.tokens()));
5114 env.require(Balance(bob_, btc(29'875)));
5115 env.require(Balance(carol_, eth(30'080)));
5116 }
5117
5118 // Payment and transfer fee, multiple steps
5119 // Scenario:
5120 // Dan's offer 200CAN/200GBP
5121 // AMM 1000GBP/10125ETH
5122 // Ed's offer 200ETH/BTC
5123 // Bob sends 195.3125CAN to pay 100BTC to Carol
5124 // Payment execution:
5125 // bob's 195.3125CAN/1.25 = 156.25CAN -> dan's offer
5126 // 156.25CAN/156.25GBP 156.25GBP/1.25 = 125GBP -> AMM's offer
5127 // 125GBP/125ETH 125ETH/1.25 = 100ETH -> ed's offer
5128 // 100ETH/100BTC 100BTC/1.25 = 80BTC paid to carol
5129 {
5130 Env env(*this);
5131 Account const dan("dan");
5132 Account const ed("ed");
5133 env.fund(XRP(30'000), gw_, alice_, bob_, carol_, dan, ed);
5134 MPTTester const btc(
5135 {.env = env,
5136 .issuer = gw_,
5137 .holders = {alice_, bob_, carol_, dan, ed},
5138 .transferFee = 25'000,
5139 .pay = 30'000,
5140 .flags = kMptDexFlags});
5141 MPTTester const eth(
5142 {.env = env,
5143 .issuer = gw_,
5144 .holders = {alice_, bob_, carol_, dan, ed},
5145 .transferFee = 25'000,
5146 .pay = 30'000,
5147 .flags = kMptDexFlags});
5148 MPTTester const can(
5149 {.env = env,
5150 .issuer = gw_,
5151 .holders = {alice_, bob_, carol_, dan, ed},
5152 .transferFee = 25'000,
5153 .pay = 2'000'000,
5154 .flags = kMptDexFlags});
5155 MPTTester const gbp(
5156 {.env = env,
5157 .issuer = gw_,
5158 .holders = {alice_, bob_, carol_, dan, ed},
5159 .transferFee = 25'000,
5160 .pay = 3'000'000,
5161 .flags = kMptDexFlags});
5162 AMM const ammAlice(env, alice_, gbp(1'000'000), eth(10'125));
5163 env(pay(gw_, bob_, can(1'953'125)));
5164 env.close();
5165 env(offer(dan, can(2'000'000), gbp(20'000)));
5166 env(offer(ed, eth(200), btc(200)));
5167 env.close();
5168 env(pay(bob_, carol_, btc(100)),
5169 Path(~MPT(gbp), ~MPT(eth), ~MPT(btc)),
5170 Sendmax(can(1'953'125)),
5171 Txflags(tfPartialPayment));
5172 env.close();
5173 env.require(Balance(bob_, can(2'000'000)));
5174 env.require(Balance(dan, can(3'562'500)));
5175 env.require(Balance(dan, gbp(2'984'375)));
5176
5177 BEAST_EXPECT(ammAlice.expectBalances(gbp(1'012'500), eth(10'000), ammAlice.tokens()));
5178 env.require(Balance(ed, eth(30'100)));
5179 env.require(Balance(ed, btc(29'900)));
5180 env.require(Balance(carol_, btc(30'080)));
5181 }
5182
5183 // Pay amounts close to one side of the pool
5184 testAMM(
5185 [&](AMM& ammAlice, Env& env) {
5186 auto const& btc = MPT(ammAlice[1]);
5187 env(pay(alice_, carol_, btc(9999)),
5188 Path(~btc),
5189 Sendmax(XRP(1)),
5190 Txflags(tfPartialPayment),
5191 Ter(tesSUCCESS));
5192 env(pay(alice_, carol_, btc(10'000)),
5193 Path(~btc),
5194 Sendmax(XRP(1)),
5195 Txflags(tfPartialPayment),
5196 Ter(tesSUCCESS));
5197 env(pay(alice_, carol_, XRP(100)),
5198 Path(~XRP),
5199 Sendmax(btc(100)),
5200 Txflags(tfPartialPayment),
5201 Ter(tesSUCCESS));
5202 env(pay(alice_, carol_, STAmount{xrpIssue(), 99'999'900}),
5203 Path(~XRP),
5204 Sendmax(btc(100)),
5205 Txflags(tfPartialPayment),
5206 Ter(tesSUCCESS));
5207 },
5208 {{XRP(100), gAmmmpt(10'000)}});
5209
5210 // Multiple paths/steps
5211 {
5212 Env env(*this);
5213 env.fund(XRP(100'000), gw_, alice_);
5214 env.fund(XRP(1'000), bob_, carol_);
5215 MPTTester const btc(
5216 {.env = env,
5217 .issuer = gw_,
5218 .holders = {alice_, bob_, carol_},
5219 .pay = 500'000'000'000'000'000,
5220 .flags = kMptDexFlags});
5221 MPTTester const eth(
5222 {.env = env,
5223 .issuer = gw_,
5224 .holders = {alice_, bob_, carol_},
5225 .pay = 500'000'000'000'000'000,
5226 .flags = kMptDexFlags});
5227 MPTTester const usd(
5228 {.env = env,
5229 .issuer = gw_,
5230 .holders = {alice_, bob_, carol_},
5231 .pay = 500'000'000'000'000'000,
5232 .flags = kMptDexFlags});
5233 MPTTester const eur(
5234 {.env = env,
5235 .issuer = gw_,
5236 .holders = {alice_, bob_, carol_},
5237 .pay = 500'000'000'000'000'000,
5238 .flags = kMptDexFlags});
5239 AMM const xrpEur(env, alice_, XRP(10'100), eur(100'000'000'000'000'000));
5240 AMM const eurBtc(
5241 env, alice_, eur(100'000'000'000'000'000), btc(102'000'000'000'000'000));
5242 AMM const btcUsd(
5243 env, alice_, btc(101'000'000'000'000'000), usd(100'000'000'000'000'000));
5244 AMM const xrpUsd(env, alice_, XRP(10'150), usd(102'000'000'000'000'000));
5245 AMM const xrpEth(env, alice_, XRP(10'000), eth(101'000'000'000'000'000));
5246 AMM const ethEur(
5247 env, alice_, eth(109'000'000'000'000'000), eur(110'000'000'000'000'000));
5248 AMM const eurUsd(
5249 env, alice_, eur(101'000'000'000'000'000), usd(100'000'000'000'000'000));
5250 env(pay(bob_, carol_, usd(1'000'000'000'000'000)),
5251 Path(~MPT(eur), ~MPT(btc), ~MPT(usd)),
5252 Path(~MPT(usd)),
5253 Path(~MPT(eth), ~MPT(eur), ~MPT(usd)),
5254 Sendmax(XRP(200)));
5255
5256 BEAST_EXPECT(xrpEth.expectBalances(
5257 XRPAmount(10'026'208'900), eth(10'073'6577924447994), xrpEth.tokens()));
5258 BEAST_EXPECT(ethEur.expectBalances(
5259 eth(10'926'3422075552006), eur(10'973'5423207873690), ethEur.tokens()));
5260 BEAST_EXPECT(eurUsd.expectBalances(
5261 eur(10'126'4576792126310), usd(9'973'9315171207179), eurUsd.tokens()));
5262 // XRP-USD path
5263 // This path provides ~73.9e12USD/74.1XRP
5264 BEAST_EXPECT(xrpUsd.expectBalances(
5265 XRPAmount(10'224'106'246), usd(10'126'0684828792821), xrpUsd.tokens()));
5266
5267 // XRP-EUR-BTC-USD
5268 // This path doesn't provide any liquidity due to how
5269 // offers are generated in multi-path.
5270 // Analytical solution shows a different distribution:
5271 // XRP-EUR-BTC-USD 11.6e12USD/11.64XRP,
5272 // XRP-USD 60.7e12USD/60.8XRP,
5273 // XRP-ETH-EUR-USD 27.6e12USD/27.6XRP
5274 BEAST_EXPECT(
5275 xrpEur.expectBalances(XRP(10'100), eur(100'000'000'000'000'000), xrpEur.tokens()));
5276 BEAST_EXPECT(eurBtc.expectBalances(
5277 eur(100'000'000'000'000'000), btc(102'000'000'000'000'000), eurBtc.tokens()));
5278 BEAST_EXPECT(btcUsd.expectBalances(
5279 btc(101'000'000'000'000'000), usd(100'000'000'000'000'000), btcUsd.tokens()));
5280 env.require(Balance(carol_, usd(501'000'000'000'000'000)));
5281 }
5282
5283 // Dependent AMM
5284 {
5285 Env env(*this);
5286 env.fund(XRP(40'000), gw_, alice_);
5287 env.fund(XRP(1'000), bob_, carol_);
5288 MPTTester const btc(
5289 {.env = env,
5290 .issuer = gw_,
5291 .holders = {alice_, bob_, carol_},
5292 .pay = 50'000'000'000'000'000,
5293 .flags = kMptDexFlags});
5294 MPTTester const eth(
5295 {.env = env,
5296 .issuer = gw_,
5297 .holders = {alice_, bob_, carol_},
5298 .pay = 50'000'000'000'000'000,
5299 .flags = kMptDexFlags});
5300 MPTTester const usd(
5301 {.env = env,
5302 .issuer = gw_,
5303 .holders = {alice_, bob_, carol_},
5304 .pay = 50'000'000'000'000'000,
5305 .flags = kMptDexFlags});
5306 MPTTester const eur(
5307 {.env = env,
5308 .issuer = gw_,
5309 .holders = {alice_, bob_, carol_},
5310 .pay = 50'000'000'000'000'000,
5311 .flags = kMptDexFlags});
5312 AMM const xrpEur(env, alice_, XRP(10'100), eur(10'000'000'000'000'000));
5313 AMM const eurBtc(env, alice_, eur(10'000'000'000'000'000), btc(10'200'000'000'000'000));
5314 AMM const btcUsd(env, alice_, btc(10'100'000'000'000'000), usd(10'000'000'000'000'000));
5315 AMM const xrpEth(env, alice_, XRP(10'000), eth(10'100'000'000'000'000));
5316 AMM const ethEur(env, alice_, eth(10'900'000'000'000'000), eur(11'000'000'000'000'000));
5317 env(pay(bob_, carol_, usd(100'000'000'000'000)),
5318 Path(~MPT(eur), ~MPT(btc), ~MPT(usd)),
5319 Path(~MPT(eth), ~MPT(eur), ~MPT(btc), ~MPT(usd)),
5320 Sendmax(XRP(200)));
5321
5322 BEAST_EXPECT(xrpEur.expectBalances(
5323 XRPAmount(10'118'738'472), eur(9'981'544436337981), xrpEur.tokens()));
5324 BEAST_EXPECT(eurBtc.expectBalances(
5325 eur(10'101'160967851758), btc(10'097'914269680647), eurBtc.tokens()));
5326 BEAST_EXPECT(btcUsd.expectBalances(
5327 btc(10'202'085730319353), usd(9'900'000'000'000'000), btcUsd.tokens()));
5328 BEAST_EXPECT(xrpEth.expectBalances(
5329 XRPAmount(10'082'446'397), eth(10'017'410727780081), xrpEth.tokens()));
5330 BEAST_EXPECT(ethEur.expectBalances(
5331 eth(10'982'589272219919), eur(10'917'294595810261), ethEur.tokens()));
5332 env.require(Balance(carol_, usd(50'100'000'000'000'000)));
5333 }
5334
5335 // AMM offers limit
5336 // Consuming 30 CLOB offers, results in hitting 30 AMM offers limit.
5337 {
5338 Env env(*this);
5339 env.fund(XRP(30'000), gw_, alice_, carol_);
5340 env.fund(XRP(1'000), bob_);
5341 MPTTester const btc(
5342 {.env = env,
5343 .issuer = gw_,
5344 .holders = {alice_, bob_, carol_},
5345 .pay = 30'000'000'000'000'000,
5346 .flags = kMptDexFlags});
5347 MPTTester const eth(
5348 {.env = env,
5349 .issuer = gw_,
5350 .holders = {alice_, bob_, carol_},
5351 .pay = 400'000'000'000'000,
5352 .flags = kMptDexFlags});
5353 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000'000'000'000'000));
5354
5355 for (int i = 0; i < 30; ++i)
5356 env(offer(alice_, eth(1'000'000'000'000 + (10'000'000'000 * i)), XRP(1)));
5357 // This is worse quality offer than 30 offers above.
5358 // It will not be consumed because of AMM offers limit.
5359 env(offer(alice_, eth(140'000'000'000'000), XRP(100)));
5360 env(pay(bob_, carol_, btc(100'000'000'000'000)),
5361 Path(~XRP, ~MPT(btc)),
5362 Sendmax(eth(400'000'000'000'000)),
5363 Txflags(tfPartialPayment | tfNoRippleDirect));
5364
5365 BEAST_EXPECT(
5366 ammAlice.expectBalances(XRP(10'030), btc(9'970'089730807592), ammAlice.tokens()));
5367
5368 env.require(Balance(carol_, btc(30'029'910269192408)));
5369 BEAST_EXPECT(expectOffers(env, alice_, 1, {{{eth(140'000'000'000'000), XRP(100)}}}));
5370 }
5371
5372 // This payment is fulfilled
5373 {
5374 Env env(*this);
5375 env.fund(XRP(30'000), gw_, alice_, carol_);
5376 env.fund(XRP(1'000), bob_);
5377 MPTTester const btc(
5378 {.env = env,
5379 .issuer = gw_,
5380 .holders = {alice_, bob_, carol_},
5381 .pay = 30'000'000'000'000'000,
5382 .flags = kMptDexFlags});
5383 MPTTester const eth(
5384 {.env = env,
5385 .issuer = gw_,
5386 .holders = {alice_, bob_, carol_},
5387 .pay = 400'000'000'000'000,
5388 .flags = kMptDexFlags});
5389 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000'000'000'000'000));
5390
5391 for (int i = 0; i < 29; ++i)
5392 env(offer(alice_, eth(1'000'000'000'000 + (10'000'000'000 * i)), XRP(1)));
5393 // This is worse quality offer than 30 offers above.
5394 // It will not be consumed because of AMM offers limit.
5395 env(offer(alice_, eth(140'000'000'000'000), XRP(100)));
5396 env(pay(bob_, carol_, btc(100'000'000'000'000)),
5397 Path(~XRP, ~MPT(btc)),
5398 Sendmax(eth(400'000'000'000'000)),
5399 Txflags(tfPartialPayment | tfNoRippleDirect));
5400 BEAST_EXPECT(ammAlice.expectBalances(
5401 XRPAmount{10'101'010'102}, btc(9'900'000'000'000'000), ammAlice.tokens()));
5402
5403 env.require(Balance(carol_, btc(30'100'000'000'000'000)));
5404 BEAST_EXPECT(
5405 expectOffers(env, alice_, 1, {{{eth(39'185857200000), XRPAmount{27'989'898}}}}));
5406 }
5407
5408 // Offer crossing with AMM and another offer.
5409 // AMM has a better quality and is consumed first.
5410 {
5411 Env env(*this);
5412 env.fund(XRP(30'000), gw_, alice_, carol_);
5413 env.fund(XRP(1'000), bob_);
5414 MPTTester const btc(
5415 {.env = env,
5416 .issuer = gw_,
5417 .holders = {alice_, bob_, carol_},
5418 .pay = 30'000'000'000'000'000,
5419 .flags = kMptDexFlags});
5420
5421 env(offer(bob_, XRP(100), btc(100'001'000'000'000)));
5422 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000'000'000));
5423 env(offer(carol_, btc(100'000'000'000'000), XRP(100)));
5424
5425 BEAST_EXPECT(ammAlice.expectBalances(
5426 XRPAmount{10'049'825'372}, btc(10'049'925870493027), ammAlice.tokens()));
5427 BEAST_EXPECT(
5428 expectOffers(env, bob_, 1, {{{XRPAmount{50'074'628}, btc(50'075129506973)}}}));
5429
5430 env.require(Balance(carol_, btc(30'100'000'000'000'000)));
5431 }
5432
5433 // Individually locked MPT destination account
5434 {
5435 Env env(*this);
5436 env.fund(XRP(30'000), gw_, alice_, carol_);
5437 MPTTester btc(
5438 {.env = env,
5439 .issuer = gw_,
5440 .holders = {alice_, carol_},
5441 .pay = 30'000,
5442 .flags = tfMPTCanLock | kMptDexFlags});
5443 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000));
5444
5445 btc.set({.holder = carol_, .flags = tfMPTLock});
5446
5447 env(pay(alice_, carol_, XRP(1)),
5448 Path(~XRP),
5449 Sendmax(btc(10)),
5450 Txflags(tfNoRippleDirect | tfPartialPayment),
5451 Ter(tesSUCCESS));
5452 }
5453
5454 // Individually locked MPT source account
5455 {
5456 Env env(*this);
5457 env.fund(XRP(30'000), gw_, alice_, carol_);
5458 MPTTester btc(
5459 {.env = env,
5460 .issuer = gw_,
5461 .holders = {alice_, carol_},
5462 .pay = 30'000,
5463 .flags = tfMPTCanLock | kMptDexFlags});
5464 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000));
5465
5466 btc.set({.holder = alice_, .flags = tfMPTLock});
5467
5468 env(pay(alice_, carol_, btc(1)),
5469 Path(~MPT(btc)),
5470 Sendmax(XRP(10)),
5471 Txflags(tfNoRippleDirect | tfPartialPayment),
5472 Ter(tesSUCCESS));
5473 }
5474 }
5475
5476 void
5478 {
5479 testcase("AMM Tokens");
5480 using namespace jtx;
5481
5482 // Offer crossing with AMM LPTokens and XRP.
5483 // AMM LPTokens come from MPT/XRP pool.
5484 testAMM(
5485 [&](AMM& ammAlice, Env& env) {
5486 auto const token1 = ammAlice.lptIssue();
5487 auto priceXRP = ammAssetOut(
5488 STAmount{XRPAmount{10'000'000'000}},
5489 STAmount{token1, 10'000'000},
5490 STAmount{token1, 5'000'000},
5491 0);
5492 // Carol places an order to buy LPTokens
5493 env(offer(carol_, STAmount{token1, 5'000'000}, priceXRP));
5494 // Alice places an order to sell LPTokens
5495 env(offer(alice_, priceXRP, STAmount{token1, 5'000'000}));
5496 // Pool's LPTokens balance doesn't change
5497 BEAST_EXPECT(ammAlice.expectBalances(
5498 XRP(10'000), MPT(ammAlice[1])(10'000), IOUAmount{10'000'000}));
5499 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{5'000'000}));
5500 BEAST_EXPECT(ammAlice.expectLPTokens(alice_, IOUAmount{5'000'000}));
5501 // Carol votes
5502 ammAlice.vote(carol_, 1'000);
5503 BEAST_EXPECT(ammAlice.expectTradingFee(500));
5504 ammAlice.vote(carol_, 0);
5505 BEAST_EXPECT(ammAlice.expectTradingFee(0));
5506 // Carol bids
5507 auto const baseFee = env.current()->fees().base;
5508 auto const carolXRP = env.balance(carol_);
5509 env(ammAlice.bid({.account = carol_, .bidMin = 100}));
5510 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{4'999'900}));
5511 BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{100}));
5512 BEAST_EXPECT(env.balance(carol_) == (carolXRP - baseFee));
5513 priceXRP = ammAssetOut(
5514 STAmount{XRPAmount{10'000'000'000}},
5515 STAmount{token1, 9'999'900},
5516 STAmount{token1, 4'999'900},
5517 0);
5518 // Carol withdraws
5519 ammAlice.withdrawAll(carol_, XRP(0));
5520 BEAST_EXPECT(env.balance(carol_) == (carolXRP - baseFee * 2 + priceXRP));
5521 BEAST_EXPECT(ammAlice.expectBalances(
5522 XRPAmount{10'000'000'000} - priceXRP,
5523 MPT(ammAlice[1])(10'000),
5524 IOUAmount{5'000'000}));
5525 BEAST_EXPECT(ammAlice.expectLPTokens(alice_, IOUAmount{5'000'000}));
5526 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
5527 },
5528 {{XRP(10000), gAmmmpt(10000)}});
5529
5530 // Offer crossing with two AMM LPTokens.
5531 // token1 comes from MPT/XRP pool.
5532 // token2 comes from XRP/IOU pool.
5533 testAMM(
5534 [&](AMM& ammAlice, Env& env) {
5535 ammAlice.deposit(carol_, 1'000'000);
5536 fund(env, gw_, {alice_, carol_}, {EUR(10'000)}, Fund::TokenOnly);
5537 AMM ammAlice1(env, alice_, XRP(10'000), EUR(10'000));
5538 ammAlice1.deposit(carol_, 1'000'000);
5539 auto const token1 = ammAlice.lptIssue();
5540 auto const token2 = ammAlice1.lptIssue();
5541 env(offer(alice_, STAmount{token1, 100}, STAmount{token2, 100}),
5542 Txflags(tfPassive));
5543 env.close();
5544 BEAST_EXPECT(expectOffers(env, alice_, 1));
5545 env(offer(carol_, STAmount{token2, 100}, STAmount{token1, 100}));
5546 env.close();
5547 BEAST_EXPECT(
5548 expectHolding(env, alice_, STAmount{token1, 10'000'100}) &&
5549 expectHolding(env, alice_, STAmount{token2, 9'999'900}));
5550 BEAST_EXPECT(
5551 expectHolding(env, carol_, STAmount{token2, 1'000'100}) &&
5552 expectHolding(env, carol_, STAmount{token1, 999'900}));
5553 BEAST_EXPECT(expectOffers(env, alice_, 0) && expectOffers(env, carol_, 0));
5554 },
5555 {{XRP(10000), gAmmmpt(10000)}});
5556
5557 // LPs pay LPTokens directly. Must trust set because the trust line
5558 // is checked for the limit, which is 0 in the AMM auto-created
5559 // trust line.
5560 testAMM(
5561 [&](AMM& ammAlice, Env& env) {
5562 auto const token1 = ammAlice.lptIssue();
5563 env.trust(STAmount{token1, 2'000'000}, carol_);
5564 env.close();
5565 ammAlice.deposit(carol_, 1'000'000);
5566 BEAST_EXPECT(
5567 ammAlice.expectLPTokens(alice_, IOUAmount{10'000'000, 0}) &&
5568 ammAlice.expectLPTokens(carol_, IOUAmount{1'000'000, 0}));
5569 // Pool balance doesn't change, only tokens moved from
5570 // one line to another.
5571 env(pay(alice_, carol_, STAmount{token1, 100}));
5572 env.close();
5573 BEAST_EXPECT(
5574 // Alice initial token1 10,000,000 - 100
5575 ammAlice.expectLPTokens(alice_, IOUAmount{9'999'900, 0}) &&
5576 // Carol initial token1 1,000,000 + 100
5577 ammAlice.expectLPTokens(carol_, IOUAmount{1'000'100, 0}));
5578
5579 env.trust(STAmount{token1, 20'000'000}, alice_);
5580 env.close();
5581 env(pay(carol_, alice_, STAmount{token1, 100}));
5582 env.close();
5583 // Back to the original balance
5584 BEAST_EXPECT(
5585 ammAlice.expectLPTokens(alice_, IOUAmount{10'000'000, 0}) &&
5586 ammAlice.expectLPTokens(carol_, IOUAmount{1'000'000, 0}));
5587 },
5588 {{XRP(10000), gAmmmpt(10000)}});
5589 }
5590
5591 void
5593 {
5594 testcase("Amendment");
5595 using namespace jtx;
5596 FeatureBitset const feature{testableAmendments() - featureMPTokensV2};
5597 Env env{*this, feature};
5598
5599 env.fund(XRP(30'000), gw_, alice_);
5600 env.close();
5601 MPT const btc = MPTTester(
5602 {.env = env,
5603 .issuer = gw_,
5604 .holders = {alice_},
5605 .pay = 10'000,
5606 .flags = tfMPTCanClawback | tfMPTCanTransfer});
5607
5608 AMM amm(env, alice_, XRP(1'000), btc(1'000), Ter(temDISABLED));
5609
5610 env(amm.bid({.bidMax = 1000}), Ter(temMALFORMED));
5611 env(amm.bid({}), Ter(temDISABLED));
5612 amm.vote(VoteArg{.tfee = 100, .err = Ter(temDISABLED)});
5613 amm.withdraw(WithdrawArg{.tokens = 100, .err = Ter(temMALFORMED)});
5614 amm.withdraw(WithdrawArg{.err = Ter(temDISABLED)});
5615 amm.deposit(DepositArg{.asset1In = USD(100), .err = Ter(temDISABLED)});
5616 amm.ammDelete(alice_, Ter(temDISABLED));
5617 }
5618
5619 void
5621 {
5622 testcase("AMMAndCLOB, offer quality change");
5623 using namespace jtx;
5624 auto const gw = Account("gw");
5625 auto const lP1 = Account("LP1");
5626 auto const lP2 = Account("LP2");
5627
5628 auto prep = [&](auto const& offerCb, auto const& expectCb) {
5629 Env env(*this, features);
5630 env.fund(XRP(30'000'000'000), gw);
5631 env.fund(XRP(10'000), lP1);
5632 env.fund(XRP(10'000), lP2);
5633 MPTTester const tst(
5634 {.env = env, .issuer = gw, .holders = {lP1, lP2}, .flags = kMptDexFlags});
5635
5636 env(offer(gw, XRP(11'500'000'000), tst(1'000'000'000'000'000)));
5637
5638 env(offer(lP1, tst(25'000'000), XRPAmount(287'500'000)));
5639
5640 // Either AMM or CLOB offer
5641 offerCb(env, tst);
5642
5643 env(offer(lP2, tst(25'000'000), XRPAmount(287'500'000)));
5644
5645 expectCb(env, tst);
5646 };
5647
5648 // If we replace AMM with an equivalent CLOB offer, which AMM generates
5649 // when it is consumed, then the result must be equivalent, too.
5650 STAmount lp2TSTBalance;
5651 std::string lp2TakerGets;
5652 std::string lp2TakerPays;
5653 // Execute with AMM first
5654 prep(
5655 [&](Env& env, MPTTester tst) { AMM const amm(env, lP1, tst(25'000'000), XRP(250)); },
5656 [&](Env& env, MPTTester tst) {
5657 lp2TSTBalance = env.balance(lP2, MPT(tst));
5658 auto const offer = getAccountOffers(env, lP2)["offers"][0u];
5659 lp2TakerGets = offer["taker_gets"].asString();
5660 lp2TakerPays = offer["taker_pays"]["value"].asString();
5661 });
5662 // Execute with CLOB offer
5663 prep(
5664 [&](Env& env, MPTTester tst) {
5665 env(offer(lP1, XRPAmount{18'095'131}, tst(1'687'379)), Txflags(tfPassive));
5666 },
5667 [&](Env& env, MPTTester tst) {
5668 BEAST_EXPECT(lp2TSTBalance == env.balance(lP2, MPT(tst)));
5669 auto const offer = getAccountOffers(env, lP2)["offers"][0u];
5670 BEAST_EXPECT(lp2TakerGets == offer["taker_gets"].asString());
5671 BEAST_EXPECT(lp2TakerPays == offer["taker_pays"]["value"].asString());
5672 });
5673 }
5674
5675 void
5677 {
5678 testcase("Trading Fee");
5679 using namespace jtx;
5680
5681 // Single Deposit, 1% fee
5682 testAMM(
5683 [&](AMM& ammAlice, Env& env) {
5684 // No fee
5685 ammAlice.deposit(carol_, MPT(ammAlice[1])(3'000));
5686 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{1'000}));
5687 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(3'000));
5688 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{0}));
5689 env.require(Balance(carol_, MPT(ammAlice[1])(30'000)));
5690 // Set fee to 1%
5691 ammAlice.vote(alice_, 1'000);
5692 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
5693
5694 // Carol gets fewer LPToken ~994, because of the single deposit
5695 // fee
5696 ammAlice.deposit(carol_, MPT(ammAlice[1])(3'000));
5697 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{994'981155689671, -12}));
5698 env.require(Balance(carol_, MPT(ammAlice[1])(27'000)));
5699
5700 // Set fee to 0
5701 ammAlice.vote(alice_, 0);
5702 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
5703
5704 // Carol gets back less than the original deposit
5705 if (!features[fixAMMv1_3])
5706 {
5707 env.require(Balance(carol_, MPT(ammAlice[1])(29'995)));
5708 }
5709 else
5710 {
5711 env.require(Balance(carol_, MPT(ammAlice[1])(29'994)));
5712 }
5713 },
5714 {{USD(1000), gAmmmpt(1000)}},
5715 0,
5716 std::nullopt,
5717 {features});
5718
5719 // Single deposit with EP not exceeding specified:
5720 // 100MPT with EP not to exceed 0.1 (AssetIn/TokensOut). 1% fee.
5721 testAMM(
5722 [&](AMM& ammAlice, Env& env) {
5723 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
5724 auto const balance = env.balance(carol_, MPT(ammAlice[1]));
5725 auto const tokensFee = ammAlice.deposit(
5726 carol_,
5727 MPT(ammAlice[1])(1000),
5728 std::nullopt,
5729 STAmount{ammAlice.lptIssue(), 1, -1});
5730 auto const deposit = balance - env.balance(carol_, MPT(ammAlice[1]));
5731 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
5732 ammAlice.vote(alice_, 0);
5733 BEAST_EXPECT(ammAlice.expectTradingFee(0));
5734 auto const tokensNoFee = ammAlice.deposit(carol_, deposit);
5735 BEAST_EXPECT(tokensFee == IOUAmount(485636'0611129, -7));
5736 if (!features[fixAMMv1_3])
5737 {
5738 BEAST_EXPECT(tokensNoFee == IOUAmount(487659'8005807, -7));
5739 }
5740 else
5741 {
5742 BEAST_EXPECT(tokensNoFee == IOUAmount(487612'21584827, -8));
5743 }
5744 },
5745 {{XRP(10'000), gAmmmpt(10'000)}},
5746 1'000,
5747 std::nullopt,
5748 {features});
5749
5750 // Single deposit with EP not exceeding specified:
5751 // 200MPT with EP not to exceed 0.002020 (AssetIn/TokensOut). 1% fee
5752 testAMM(
5753 [&](AMM& ammAlice, Env& env) {
5754 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
5755 auto const balance = env.balance(carol_, MPT(ammAlice[1]));
5756 auto const tokensFee = ammAlice.deposit(
5757 carol_,
5758 MPT(ammAlice[1])(200),
5759 std::nullopt,
5760 STAmount{ammAlice.lptIssue(), 2020, -6});
5761 auto const deposit = balance - env.balance(carol_, MPT(ammAlice[1]));
5762 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
5763 ammAlice.vote(alice_, 0);
5764 BEAST_EXPECT(ammAlice.expectTradingFee(0));
5765 auto const tokensNoFee = ammAlice.deposit(carol_, deposit);
5766 if (!features[fixAMMv1_3])
5767 {
5768 BEAST_EXPECT(tokensFee == IOUAmount(98'019'80198019, -8));
5769 BEAST_EXPECT(tokensNoFee == IOUAmount(98'495'13933556, -8));
5770 }
5771 else
5772 {
5773 BEAST_EXPECT(tokensFee == IOUAmount(97527'05893345, -8));
5774 BEAST_EXPECT(tokensNoFee == IOUAmount(98000'10293049, -8));
5775 }
5776 },
5777 {{XRP(10'000), gAmmmpt(10'000)}},
5778 1'000,
5779 std::nullopt,
5780 {features});
5781
5782 // Single Withdrawal, 1% fee
5783 testAMM(
5784 [&](AMM& ammAlice, Env& env) {
5785 // No fee
5786 ammAlice.deposit(carol_, MPT(ammAlice[1])(3'000));
5787 BEAST_EXPECT(ammAlice.expectLPTokens(carol_, IOUAmount{1'000}));
5788 env.require(Balance(carol_, MPT(ammAlice[1])(27'000)));
5789 // Set fee to 1%
5790 ammAlice.vote(alice_, 1'000);
5791 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
5792 // Single withdrawal. Carol gets ~5USD less than deposited.
5793 ammAlice.withdrawAll(carol_, MPT(ammAlice[1])(0));
5794 if (!features[fixAMMv1_3])
5795 {
5796 env.require(Balance(carol_, MPT(ammAlice[1])(29'995)));
5797 }
5798 else
5799 {
5800 env.require(Balance(carol_, MPT(ammAlice[1])(29'994)));
5801 }
5802 },
5803 {{USD(1000), gAmmmpt(1000)}},
5804 0,
5805 std::nullopt,
5806 {features});
5807
5808 // Withdraw with EPrice limit, 1% fee.
5809 testAMM(
5810 [&](AMM& ammAlice, Env& env) {
5811 ammAlice.deposit(carol_, 1'000'000);
5812 auto const tokensFee = ammAlice.withdraw(
5813 carol_, MPT(ammAlice[1])(100), std::nullopt, IOUAmount{520, 0});
5814 env.require(Balance(carol_, MPT(ammAlice[1])(30443)));
5815
5816 // Set to original pool size
5817 auto const deposit =
5818 env.balance(carol_, MPT(ammAlice[1])) - MPT(ammAlice[1])(29'000);
5819 ammAlice.deposit(carol_, deposit);
5820 // fee 0%
5821 ammAlice.vote(alice_, 0);
5822 BEAST_EXPECT(ammAlice.expectTradingFee(0));
5823 auto const tokensNoFee = ammAlice.withdraw(carol_, deposit);
5824 if (!features[fixAMMv1_3])
5825 {
5826 env.require(Balance(carol_, MPT(ammAlice[1])(30443)));
5827 }
5828 else
5829 {
5830 env.require(Balance(carol_, MPT(ammAlice[1])(30442)));
5831 }
5832 BEAST_EXPECT(tokensNoFee == IOUAmount(746'327'46496649, -8));
5833 BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8));
5834 },
5835 {{XRP(10'000), gAmmmpt(10'000)}},
5836 1'000,
5837 std::nullopt,
5838 {features});
5839
5840 // Payment, 1% fee
5841 {
5842 Env env{*this, features};
5843 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
5844 env.close();
5845 MPT const btc = MPTTester(
5846 {.env = env,
5847 .issuer = gw_,
5848 .holders = {alice_, bob_, carol_},
5849 .pay = 30'000,
5850 .flags = kMptDexFlags});
5851
5852 auto const usd = gw_["USD"];
5853 env.trust(usd(30'000), alice_);
5854 env(pay(gw_, alice_, usd(30'000)));
5855 env.trust(usd(30'000), bob_);
5856 env(pay(gw_, bob_, usd(1'000)));
5857 env.trust(usd(30'000), carol_);
5858 env(pay(gw_, carol_, usd(30'000)));
5859 env.close();
5860
5861 AMM amm(env, alice_, btc(1000), usd(1010));
5862
5863 env.require(Balance(alice_, btc(29'000)));
5864 env.require(Balance(alice_, usd(28'990)));
5865 env.require(Balance(carol_, btc(30'000)));
5866
5867 // Carol pays to Alice with no fee
5868 env(pay(carol_, alice_, usd(10)),
5869 Path(~usd),
5870 Sendmax(btc(10)),
5871 Txflags(tfNoRippleDirect));
5872 env.close();
5873
5874 // Alice has 10USD more and Carol has 10BTC less
5875 env.require(Balance(alice_, btc(29'000)));
5876 env.require(Balance(alice_, usd(29'000)));
5877 env.require(Balance(carol_, btc(29'990)));
5878
5879 // Set fee to 1%
5880 amm.vote(alice_, 1'000);
5881 BEAST_EXPECT(amm.expectTradingFee(1'000));
5882 // Bob pays to Carol with 1% fee
5883 env(pay(bob_, carol_, btc(10)),
5884 Path(~btc),
5885 Sendmax(usd(15)),
5886 Txflags(tfNoRippleDirect));
5887 env.close();
5888 // Bob sends 10.1~USD to pay 10BTC
5889 env.require(Balance(bob_, STAmount{usd, UINT64_C(989'8989898989899), -13}));
5890 // Carol got 10BTC
5891 env.require(Balance(carol_, btc(30'000)));
5892 BEAST_EXPECT(amm.expectBalances(
5893 btc(1'000), STAmount{usd, UINT64_C(1'010'10101010101), -11}, amm.tokens()));
5894 }
5895
5896 // Offer crossing, 0.05% fee MPT/XRP
5897 testAMM(
5898 [&](AMM& ammAlice, Env& env) {
5899 auto const btc = MPT(ammAlice[1]);
5900 auto const carolXRP = env.balance(carol_);
5901 auto const baseFee = env.current()->fees().base;
5902 env(offer(carol_, btc(10), XRP(10)));
5903 env.close();
5904 env.require(Balance(carol_, btc(30'010)));
5905 env.require(Balance(carol_, carolXRP - baseFee - XRP(10)));
5906
5907 // Change pool composition back
5908 env(offer(carol_, XRP(10), btc(10)));
5909 env.close();
5910 env.require(Balance(carol_, btc(30'000)));
5911 env.require(Balance(carol_, carolXRP - baseFee * 2));
5912
5913 // set fee to 0.05%
5914 ammAlice.vote(alice_, 50);
5915 BEAST_EXPECT(ammAlice.expectTradingFee(50));
5916 env(offer(carol_, btc(10), XRP(10)));
5917 env.close();
5918 env.require(Balance(carol_, btc(30'009)));
5919 env.require(Balance(carol_, carolXRP - baseFee * 3 - XRPAmount(8'995'507)));
5920 BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{btc(1), XRP(1)}}}));
5921 },
5922 {{XRP(1000), gAmmmpt(1010)}},
5923 0,
5924 std::nullopt,
5925 {features});
5926
5927 // Offer crossing, 0.5% fee MPT/IOU
5928 testAMM(
5929 [&](AMM& ammAlice, Env& env) {
5930 auto const btc = MPT(ammAlice[1]);
5931 env(offer(carol_, btc(10'000'000), USD(10)));
5932 env.close();
5933 env.require(Balance(carol_, btc(1'020'001'000)));
5934 env.require(Balance(carol_, USD(29'990)));
5935
5936 // Change pool composition back
5937 env(offer(carol_, USD(10), btc(10'000'000)));
5938 env.close();
5939 env.require(Balance(carol_, btc(1'010'001'000)));
5940 env.require(Balance(carol_, USD(30'000)));
5941
5942 // set fee to 0.5%
5943 ammAlice.vote(alice_, 500);
5944 BEAST_EXPECT(ammAlice.expectTradingFee(500));
5945 env(offer(carol_, btc(10'000'000), USD(10)));
5946 env.close();
5947 env.require(Balance(carol_, btc(1'014'975'874)));
5948 env.require(Balance(carol_, STAmount{USD, UINT64_C(29'995'02512600184), -11}));
5949 BEAST_EXPECT(expectOffers(
5950 env,
5951 carol_,
5952 1,
5953 {{Amounts{btc(5'025126), STAmount{USD, UINT64_C(5'025126), -6}}}}));
5954 },
5955 {{USD(1000), gAmmmpt(1010000000)}},
5956 0,
5957 std::nullopt,
5958 {features});
5959
5960 // Payment with AMM and CLOB offer, 0 fee
5961 // AMM liquidity is consumed first up to CLOB offer quality
5962 // CLOB offer is fully consumed next
5963 // Remaining amount is consumed via AMM liquidity
5964 {
5965 Env env{*this, features};
5966 Account const ed("ed");
5967 fund(env, gw_, {alice_, bob_, carol_, ed}, XRP(30'000), {USD(2'000)});
5968 MPT const btc = MPTTester(
5969 {.env = env,
5970 .issuer = gw_,
5971 .holders = {alice_, bob_, carol_, ed},
5972 .pay = 30'000'000000,
5973 .flags = kMptDexFlags});
5974
5975 env(offer(carol_, btc(5'000000), USD(5)));
5976 AMM const ammAlice(env, alice_, USD(1'005), btc(1'000'000000));
5977 env(pay(bob_, ed, USD(10)),
5978 Path(~USD),
5979 Sendmax(btc(15'000000)),
5980 Txflags(tfNoRippleDirect));
5981
5982 env.require(Balance(ed, USD(2'010)));
5983 env.require(Balance(bob_, btc(29'989'999999)));
5984 BEAST_EXPECT(ammAlice.expectBalances(
5985 btc(1'005'000001),
5986 STAmount{USD, UINT64_C(999'9999999999999), -13},
5987 ammAlice.tokens()));
5988 BEAST_EXPECT(expectOffers(env, carol_, 0));
5989 }
5990
5991 // Payment with AMM and CLOB offer. Same as above but with 0.25%
5992 // fee.
5993 {
5994 Env env{*this, features};
5995 Account const ed("ed");
5996 fund(env, gw_, {alice_, bob_, carol_, ed}, XRP(30'000), {USD(2'000)});
5997 MPT const btc = MPTTester(
5998 {.env = env,
5999 .issuer = gw_,
6000 .holders = {alice_, bob_, carol_, ed},
6001 .pay = 30'000'000000,
6002 .flags = kMptDexFlags});
6003
6004 env(offer(carol_, btc(5'000000), USD(5)));
6005 // Set 0.25% fee
6006 AMM const ammAlice(env, alice_, USD(1'005), btc(1'000'000000), false, 250);
6007 env(pay(bob_, ed, USD(10)),
6008 Path(~USD),
6009 Sendmax(btc(15'000000)),
6010 Txflags(tfNoRippleDirect));
6011 env.require(Balance(ed, USD(2'010)));
6012 env.require(Balance(bob_, btc(29'989'987453)));
6013 BEAST_EXPECT(ammAlice.expectBalances(btc(1'005'012547), USD(1'000), ammAlice.tokens()));
6014 BEAST_EXPECT(expectOffers(env, carol_, 0));
6015 }
6016
6017 // Payment with AMM and CLOB offer. AMM has a better
6018 // spot price quality, but 1% fee offsets that. As the result
6019 // the entire trade is executed via LOB.
6020 {
6021 Env env{*this, features};
6022 Account const ed("ed");
6023 fund(env, gw_, {alice_, bob_, carol_, ed}, XRP(30'000), {USD(2'000)});
6024 MPT const btc = MPTTester(
6025 {.env = env,
6026 .issuer = gw_,
6027 .holders = {alice_, bob_, carol_, ed},
6028 .pay = 30'000'000000,
6029 .flags = kMptDexFlags});
6030
6031 env(offer(carol_, btc(10'000000), USD(10)));
6032 // Set 1% fee
6033 AMM const ammAlice(env, alice_, USD(1'005), btc(1'000'000000), false, 1'000);
6034 env(pay(bob_, ed, USD(10)),
6035 Path(~USD),
6036 Sendmax(btc(15'000000)),
6037 Txflags(tfNoRippleDirect));
6038 env.require(Balance(ed, USD(2'010)));
6039 env.require(Balance(bob_, btc(29'990'000000)));
6040 BEAST_EXPECT(ammAlice.expectBalances(btc(1'000'000000), USD(1'005), ammAlice.tokens()));
6041 BEAST_EXPECT(expectOffers(env, carol_, 0));
6042 }
6043
6044 // Payment with AMM and CLOB offer. AMM has a better
6045 // spot price quality, but 1% fee offsets that.
6046 // The CLOB offer is consumed first and the remaining
6047 // amount is consumed via AMM liquidity.
6048 {
6049 Env env{*this, features};
6050 Account const ed("ed");
6051 fund(env, gw_, {alice_, bob_, carol_, ed}, XRP(30'000), {USD(2'000)});
6052 MPT const btc = MPTTester(
6053 {.env = env,
6054 .issuer = gw_,
6055 .holders = {alice_, bob_, carol_, ed},
6056 .pay = 30'000'000000,
6057 .flags = kMptDexFlags});
6058
6059 env(offer(carol_, btc(9'000000), USD(9)));
6060 // Set 1% fee
6061 AMM const ammAlice(env, alice_, USD(1'005), btc(1'000'000000), false, 1'000);
6062 env(pay(bob_, ed, USD(10)),
6063 Path(~USD),
6064 Sendmax(btc(15'000000)),
6065 Txflags(tfNoRippleDirect));
6066 env.require(Balance(ed, USD(2'010)));
6067 env.require(Balance(bob_, btc(29'989'993923)));
6068 BEAST_EXPECT(ammAlice.expectBalances(btc(1'001'006077), USD(1'004), ammAlice.tokens()));
6069 BEAST_EXPECT(expectOffers(env, carol_, 0));
6070 }
6071 }
6072
6073 void
6075 {
6076 testcase("Adjusted Deposit/Withdraw Tokens");
6077 using namespace jtx;
6078
6079 // Deposit/Withdraw USD from USD/MPT pool
6080 {
6081 Env env(*this);
6082 Account const gw{"gateway"};
6083 Account const alice{"alice"};
6084 Account const bob("bob");
6085 Account const carol("carol");
6086 Account const ed("ed");
6087 Account const paul("paul");
6088 Account const dan("dan");
6089 Account const chris("chris");
6090 Account const simon("simon");
6091 Account const ben("ben");
6092 Account const natalie("natalie");
6093 std::vector<Account> const holders{
6094 alice, bob, carol, ed, paul, dan, chris, simon, ben, natalie};
6095 env.fund(XRP(100000), gw, alice, bob, carol, ed, paul, dan, chris, simon, ben, natalie);
6096 env.close();
6097
6098 MPT const btc = MPTTester(
6099 {.env = env,
6100 .issuer = gw,
6101 .holders = holders,
6102 .pay = 40'000'000000,
6103 .flags = kMptDexFlags});
6104
6105 auto const usd = gw["USD"];
6106 for (auto const& holder : holders)
6107 {
6108 env.trust(usd(1'500'000), holder);
6109 env(pay(gw, holder, usd(1'500'000)));
6110 }
6111 env.close();
6112
6113 auto aliceUSD = env.balance(alice, usd);
6114 auto bobUSD = env.balance(bob, usd);
6115 auto carolUSD = env.balance(carol, usd);
6116 auto edUSD = env.balance(ed, usd);
6117 auto paulUSD = env.balance(paul, usd);
6118 auto danUSD = env.balance(dan, usd);
6119 auto chrisUSD = env.balance(chris, usd);
6120 auto simonUSD = env.balance(simon, usd);
6121 auto benUSD = env.balance(ben, usd);
6122 auto natalieUSD = env.balance(natalie, usd);
6123
6124 AMM ammAlice(env, alice, btc(10'000'000000), usd(10000));
6125 BEAST_EXPECT(
6126 ammAlice.expectBalances(btc(10'000'000000), usd(10'000), IOUAmount{10'000'000}));
6127
6128 for (int i = 0; i < 10; ++i)
6129 {
6130 ammAlice.deposit(ben, STAmount{usd, 1, -10});
6131 ammAlice.withdrawAll(ben, usd(0));
6132 ammAlice.deposit(simon, usd(0.1));
6133 ammAlice.withdrawAll(simon, usd(0));
6134 ammAlice.deposit(chris, usd(1));
6135 ammAlice.withdrawAll(chris, usd(0));
6136 ammAlice.deposit(dan, usd(10));
6137 ammAlice.withdrawAll(dan, usd(0));
6138 ammAlice.deposit(bob, usd(100));
6139 ammAlice.withdrawAll(bob, usd(0));
6140 ammAlice.deposit(carol, usd(1'000));
6141 ammAlice.withdrawAll(carol, usd(0));
6142 ammAlice.deposit(ed, usd(10'000));
6143 ammAlice.withdrawAll(ed, usd(0));
6144 ammAlice.deposit(paul, usd(100'000));
6145 ammAlice.withdrawAll(paul, usd(0));
6146 ammAlice.deposit(natalie, usd(1'000'000));
6147 ammAlice.withdrawAll(natalie, usd(0));
6148 }
6149
6150 BEAST_EXPECT(ammAlice.expectBalances(
6151 btc(10'000'000000),
6152 STAmount{usd, UINT64_C(10000'0000000001), -10},
6153 IOUAmount{10'000'000}));
6154
6155 env.require(Balance(bob, bobUSD));
6156 env.require(Balance(carol, carolUSD));
6157 env.require(Balance(ed, edUSD));
6158 env.require(Balance(paul, paulUSD));
6159 env.require(Balance(dan, danUSD));
6160 env.require(Balance(chris, chrisUSD));
6161 env.require(Balance(simon, simonUSD));
6162 env.require(Balance(ben, benUSD));
6163 env.require(Balance(natalie, natalieUSD));
6164
6165 ammAlice.withdrawAll(alice);
6166 BEAST_EXPECT(!ammAlice.ammExists());
6167 env.require(Balance(alice, aliceUSD));
6168 }
6169
6170 // Same as above but deposit/withdraw MPT from USD/MPT pool
6171 {
6172 Env env(*this);
6173 Account const gw{"gateway"};
6174 Account const alice{"alice"};
6175 Account const bob("bob");
6176 Account const carol("carol");
6177 Account const ed("ed");
6178 Account const paul("paul");
6179 Account const dan("dan");
6180 Account const chris("chris");
6181 Account const simon("simon");
6182 Account const ben("ben");
6183 Account const natalie("natalie");
6184 std::vector<Account> const holders{
6185 alice, bob, carol, ed, paul, dan, chris, simon, ben, natalie};
6186 env.fund(XRP(100000), gw, alice, bob, carol, ed, paul, dan, chris, simon, ben, natalie);
6187 env.close();
6188
6189 MPT const btc = MPTTester(
6190 {.env = env,
6191 .issuer = gw,
6192 .holders = holders,
6193 .pay = 40'000'000000,
6194 .flags = kMptDexFlags});
6195
6196 auto const usd = gw["USD"];
6197 for (auto const& holder : holders)
6198 {
6199 env.trust(usd(1'500'000), holder);
6200 env(pay(gw, holder, usd(1'500'000)));
6201 }
6202 env.close();
6203
6204 auto aliceBTC = env.balance(alice, btc);
6205 auto bobBTC = env.balance(bob, btc);
6206 auto carolBTC = env.balance(carol, btc);
6207 auto edBTC = env.balance(ed, btc);
6208 auto paulBTC = env.balance(paul, btc);
6209 auto danBTC = env.balance(dan, btc);
6210 auto chrisBTC = env.balance(chris, btc);
6211 auto simonBTC = env.balance(simon, btc);
6212 auto benBTC = env.balance(ben, btc);
6213 auto natalieBTC = env.balance(natalie, btc);
6214
6215 AMM ammAlice(env, alice, btc(10'000'000000), usd(10000));
6216 BEAST_EXPECT(
6217 ammAlice.expectBalances(btc(10'000'000000), usd(10'000), IOUAmount{10'000'000}));
6218
6219 for (int i = 0; i < 10; ++i)
6220 {
6221 ammAlice.deposit(ben, btc(1));
6222 ammAlice.withdrawAll(ben, btc(0));
6223 ammAlice.deposit(simon, btc(1'000));
6224 ammAlice.withdrawAll(simon, btc(0));
6225 ammAlice.deposit(chris, btc(1));
6226 ammAlice.withdrawAll(chris, btc(0));
6227 ammAlice.deposit(dan, btc(10));
6228 ammAlice.withdrawAll(dan, btc(0));
6229 ammAlice.deposit(bob, btc(100));
6230 ammAlice.withdrawAll(bob, btc(0));
6231 ammAlice.deposit(carol, btc(1'000));
6232 ammAlice.withdrawAll(carol, btc(0));
6233 ammAlice.deposit(ed, btc(10'000));
6234 ammAlice.withdrawAll(ed, btc(0));
6235 ammAlice.deposit(paul, btc(100'000));
6236 ammAlice.withdrawAll(paul, btc(0));
6237 ammAlice.deposit(natalie, btc(1'000'000));
6238 ammAlice.withdrawAll(natalie, btc(0));
6239 }
6240
6241 BEAST_EXPECT(
6242 ammAlice.expectBalances(btc(10'000'000090), usd(10'000), IOUAmount{10'000'000}));
6243
6244 env.require(Balance(bob, bobBTC - btc(10)));
6245 env.require(Balance(carol, carolBTC - btc(10)));
6246 env.require(Balance(ed, edBTC - btc(10)));
6247 env.require(Balance(paul, paulBTC - btc(10)));
6248 env.require(Balance(dan, danBTC - btc(10)));
6249 env.require(Balance(chris, chrisBTC - btc(10)));
6250 env.require(Balance(simon, simonBTC - btc(10)));
6251 env.require(Balance(ben, benBTC - btc(10)));
6252 env.require(Balance(natalie, natalieBTC - btc(10)));
6253
6254 ammAlice.withdrawAll(alice);
6255 BEAST_EXPECT(!ammAlice.ammExists());
6256 env.require(Balance(alice, aliceBTC + btc(90)));
6257 }
6258 }
6259
6260 void
6262 {
6263 testcase("AMMID");
6264 using namespace jtx;
6265
6266 // MPT/XRP
6267 testAMM(
6268 [&](AMM& amm, Env& env) {
6269 amm.setClose(false);
6270 auto const info = env.rpc(
6271 "json",
6272 "account_info",
6273 std::string("{\"account\": \"" + to_string(amm.ammAccount()) + "\"}"));
6274 try
6275 {
6276 BEAST_EXPECT(
6277 info[jss::result][jss::account_data][jss::AMMID].asString() ==
6278 to_string(amm.ammID()));
6279 }
6280 catch (...)
6281 {
6282 fail();
6283 }
6284
6285 amm.deposit(carol_, 1'000);
6286
6287 auto affected =
6288 env.meta()->getJson(JsonOptions::Values::None)[sfAffectedNodes.fieldName];
6289 try
6290 {
6291 bool found = false;
6292 for (auto const& node : affected)
6293 {
6294 if (node.isMember(sfModifiedNode.fieldName) &&
6295 node[sfModifiedNode.fieldName][sfLedgerEntryType.fieldName]
6296 .asString() == "AccountRoot" &&
6297 node[sfModifiedNode.fieldName][sfFinalFields.fieldName][jss::Account]
6298 .asString() == to_string(amm.ammAccount()))
6299 {
6300 found =
6301 node[sfModifiedNode.fieldName][sfFinalFields.fieldName][jss::AMMID]
6302 .asString() == to_string(amm.ammID());
6303 break;
6304 }
6305 }
6306 BEAST_EXPECT(found);
6307 }
6308 catch (...)
6309 {
6310 fail();
6311 }
6312 },
6313 {{XRP(1000), gAmmmpt(1000'000)}});
6314 }
6315
6316 void
6318 {
6319 testcase("Offer/Strand Selection");
6320 using namespace jtx;
6321 Account const ed("ed");
6322 Account const gw1("gw1");
6323
6324 // These tests are expected to fail if the OwnerPaysFee feature
6325 // is ever supported. Updates will need to be made to AMM handling
6326 // in the payment engine, and these tests will need to be updated.
6327
6328 struct MPTList
6329 {
6330 MPTTester const usd;
6331 MPTTester const eth;
6332 MPTTester const can;
6333 };
6334
6335 auto prep = [&](Env& env, uint16_t gwTransferFee, uint16_t gw1TransferFee) -> MPTList {
6336 env.fund(XRP(2'000), gw_, gw1, alice_, bob_, carol_, ed);
6337 MPTTester usd(
6338 {.env = env,
6339 .issuer = gw_,
6340 .holders = {alice_, bob_, carol_, ed},
6341 .transferFee = gwTransferFee,
6342 .pay = 2'000'000'000,
6343 .flags = kMptDexFlags});
6344 MPTTester eth(
6345 {.env = env,
6346 .issuer = gw1,
6347 .holders = {alice_, bob_, carol_, ed},
6348 .transferFee = gw1TransferFee,
6349 .pay = 2'000'000'000,
6350 .flags = kMptDexFlags});
6351 MPTTester can(
6352 {.env = env,
6353 .issuer = gw1,
6354 .holders = {alice_, bob_, carol_, ed},
6355 .transferFee = gw1TransferFee,
6356 .pay = 2'000'000'000,
6357 .flags = kMptDexFlags});
6358 env.close();
6359
6360 return MPTList{
6361 .usd = std::move(usd),
6362 .eth = std::move(eth),
6363 .can = std::move(can),
6364 };
6365 };
6366
6367 static constexpr std::uint32_t kLowRate = 10'000;
6368 static constexpr std::uint32_t kHighRate = 50'000;
6369 for (auto const& rates :
6370 {std::make_pair(kLowRate, kHighRate), std::make_pair(kHighRate, kLowRate)})
6371 {
6372 // Offer Selection
6373
6374 // Cross-currency payment: AMM has the same spot price quality
6375 // as CLOB's offer and can't generate a better quality offer.
6376 // The transfer fee in this case doesn't change the CLOB quality
6377 // because trIn is ignored on adjustment and trOut on payment is
6378 // also ignored because ownerPaysTransferFee is false in this
6379 // case. Run test for 0) offer, 1) AMM, 2) offer and AMM to
6380 // verify that the quality is better in the first case, and CLOB
6381 // is selected in the second case.
6382 {
6384 for (auto i = 0; i < 3; ++i)
6385 {
6386 Env env(*this, features);
6387 auto mpts = prep(env, rates.first, rates.second);
6388 auto usd = mpts.usd;
6389 auto eth = mpts.eth;
6390 auto can = mpts.can;
6392
6393 if (i == 0 || i == 2)
6394 {
6395 env(offer(ed, eth(400'000'000), usd(400'000'000)), Txflags(tfPassive));
6396 env.close();
6397 }
6398 if (i > 0)
6399 amm.emplace(env, ed, usd(1'000'000'000), eth(1'000'000'000));
6400 env(pay(carol_, bob_, usd(100'000'000)),
6401 Path(~MPT(usd)),
6402 Sendmax(eth(500'000'000)));
6403 env.close();
6404 // CLOB and AMM, AMM is not selected
6405 if (i == 2)
6406 {
6407 // NOLINTBEGIN(bugprone-unchecked-optional-access) i==2 implies amm is
6408 // emplaced (i>0)
6409 BEAST_EXPECT(amm->expectBalances(
6410 usd(1'000'000'000), eth(1'000'000'000), amm->tokens()));
6411 // NOLINTEND(bugprone-unchecked-optional-access)
6412 }
6413 env.require(Balance(bob_, usd(2'100'000'000)));
6414 q[i] = Quality(
6415 Amounts{
6416 eth(2'000'000'000) - env.balance(carol_, MPT(eth)),
6417 env.balance(bob_, MPT(usd)) - usd(2'000'000'000)});
6418 }
6419 // CLOB is better quality than AMM
6420 BEAST_EXPECT(q[0] > q[1]);
6421 // AMM is not selected with CLOB
6422 BEAST_EXPECT(q[0] == q[2]);
6423 }
6424 // Offer crossing: AMM has the same spot price quality
6425 // as CLOB's offer and can't generate a better quality offer.
6426 // The transfer fee in this case doesn't change the CLOB quality
6427 // because the quality adjustment is ignored for the offer
6428 // crossing.
6429 for (auto i = 0; i < 3; ++i)
6430 {
6431 Env env(*this, features);
6432 auto mpts = prep(env, rates.first, rates.second);
6433 auto usd = mpts.usd;
6434 auto eth = mpts.eth;
6435 auto can = mpts.can;
6437 if (i == 0 || i == 2)
6438 {
6439 env(offer(ed, eth(400'000'000), usd(400'000'000)), Txflags(tfPassive));
6440 env.close();
6441 }
6442 if (i > 0)
6443 amm.emplace(env, ed, usd(1'000'000'000), eth(1'000'000'000));
6444 env(offer(alice_, usd(400'000'000), eth(400'000'000)));
6445 env.close();
6446 // AMM is not selected
6447 if (i > 0)
6448 {
6449 // NOLINTBEGIN(bugprone-unchecked-optional-access) emplaced when i > 0
6450 BEAST_EXPECT(
6451 amm->expectBalances(usd(1'000'000'000), eth(1'000'000'000), amm->tokens()));
6452 // NOLINTEND(bugprone-unchecked-optional-access)
6453 }
6454 if (i == 0 || i == 2)
6455 {
6456 // Fully crosses
6457 BEAST_EXPECT(expectOffers(env, alice_, 0));
6458 }
6459 // Fails to cross because AMM is not selected
6460 else
6461 {
6462 BEAST_EXPECT(expectOffers(
6463 env, alice_, 1, {Amounts{usd(400'000'000), eth(400'000'000)}}));
6464 }
6465 BEAST_EXPECT(expectOffers(env, ed, 0));
6466 }
6467
6468 // Show that the CLOB quality reduction
6469 // results in AMM offer selection.
6470
6471 // Same as the payment but reduced offer quality
6472 {
6474 for (auto i = 0; i < 3; ++i)
6475 {
6476 Env env(*this, features);
6477 auto mpts = prep(env, rates.first, rates.second);
6478 auto usd = mpts.usd;
6479 auto eth = mpts.eth;
6480 auto can = mpts.can;
6482 if (i == 0 || i == 2)
6483 {
6484 env(offer(ed, eth(400'000'000), usd(330'000'000)), Txflags(tfPassive));
6485 env.close();
6486 }
6487 if (i > 0)
6488 amm.emplace(env, ed, usd(1'000'000'000), eth(1'000'000'000));
6489 env(pay(carol_, bob_, usd(100'000'000)),
6490 Path(~MPT(usd)),
6491 Sendmax(eth(500'000'000)));
6492 env.close();
6493 // AMM and CLOB are selected
6494 if (i > 0)
6495 {
6496 // NOLINTBEGIN(bugprone-unchecked-optional-access) emplaced when i > 0
6497 BEAST_EXPECT(!amm->expectBalances(
6498 usd(1'000'000'000), eth(1'000'000'000), amm->tokens()));
6499 // NOLINTEND(bugprone-unchecked-optional-access)
6500 }
6501
6502 if (i == 2)
6503 {
6504 if (rates.first == kLowRate)
6505 {
6506 BEAST_EXPECT(expectOffers(
6507 env,
6508 ed,
6509 1,
6510 {{Amounts{
6511 eth(377'824'111),
6512 usd(311'704'892),
6513 }}}));
6514 }
6515 else
6516 {
6517 BEAST_EXPECT(expectOffers(
6518 env,
6519 ed,
6520 1,
6521 {{Amounts{
6522 eth(329'339'263),
6523 usd(271'704'892),
6524 }}}));
6525 }
6526 }
6527 env.require(Balance(bob_, usd(2'100'000'000)));
6528 q[i] = Quality(
6529 Amounts{
6530 eth(2'000'000'000) - env.balance(carol_, MPT(eth)),
6531 env.balance(bob_, MPT(usd)) - usd(2'000'000'000)});
6532 }
6533 // AMM is better quality
6534 BEAST_EXPECT(q[1] > q[0]);
6535 // AMM and CLOB produce better quality
6536 BEAST_EXPECT(q[2] > q[1]);
6537 }
6538
6539 // Same as the offer-crossing but reduced offer quality
6540 for (auto i = 0; i < 3; ++i)
6541 {
6542 Env env(*this, features);
6543 auto mpts = prep(env, rates.first, rates.second);
6544 auto usd = mpts.usd;
6545 auto eth = mpts.eth;
6546 auto can = mpts.can;
6548 if (i == 0 || i == 2)
6549 {
6550 env(offer(ed, eth(400'000'000), usd(325'000'002)), Txflags(tfPassive));
6551 env.close();
6552 }
6553 if (i > 0)
6554 amm.emplace(env, ed, usd(1'000'000'000), eth(1'000'000'000));
6555 env(offer(alice_, usd(325'000'000), eth(400'000'000)));
6556 env.close();
6557 // AMM is selected in both cases
6558 if (i > 0)
6559 {
6560 // NOLINTBEGIN(bugprone-unchecked-optional-access) emplaced when i > 0
6561 BEAST_EXPECT(!amm->expectBalances(
6562 usd(1'000'000'000), eth(1'000'000'000), amm->tokens()));
6563 // NOLINTEND(bugprone-unchecked-optional-access)
6564 }
6565 // Partially crosses, AMM is selected, CLOB fails
6566 // limitQuality
6567 if (i == 2)
6568 {
6569 if (rates.first == kLowRate)
6570 {
6571 // Ed offer is partially crossed.
6572 // The updated rounding makes limitQuality
6573 // work if both amendments are enabled
6574 BEAST_EXPECT(expectOffers(
6575 env,
6576 ed,
6577 1,
6578 {{Amounts{
6579 eth(121'368'836),
6580 usd(98'612'180),
6581 }}}));
6582 BEAST_EXPECT(expectOffers(env, alice_, 0));
6583 }
6584 else
6585 {
6586 // Ed offer is partially crossed.
6587 BEAST_EXPECT(expectOffers(
6588 env,
6589 ed,
6590 1,
6591 {{Amounts{
6592 eth(121'368'836),
6593 usd(98'612'180),
6594 }}}));
6595 BEAST_EXPECT(expectOffers(env, alice_, 0));
6596 }
6597 }
6598 }
6599
6600 // Strand selection
6601
6602 // Two book steps strand quality is 1.
6603 // AMM strand's best quality is equal to AMM's spot price
6604 // quality, which is 1. Both strands (steps) are adjusted
6605 // for the transfer fee in qualityUpperBound. In case
6606 // of two strands, AMM offers have better quality and are
6607 // consumed first, remaining liquidity is generated by CLOB
6608 // offers. Liquidity from two strands is better in this case
6609 // than in case of one strand with two book steps. Liquidity
6610 // from one strand with AMM has better quality than either one
6611 // strand with two book steps or two strands. It may appear
6612 // unintuitive, but one strand with AMM is optimized and
6613 // generates one AMM offer, while in case of two strands,
6614 // multiple AMM offers are generated, which results in slightly
6615 // worse overall quality.
6616 {
6618 for (auto i = 0; i < 3; ++i)
6619 {
6620 Env env(*this, features);
6621 auto mpts = prep(env, rates.first, rates.second);
6622 auto usd = mpts.usd;
6623 auto eth = mpts.eth;
6624 auto can = mpts.can;
6626
6627 if (i == 0 || i == 2)
6628 {
6629 env(offer(ed, eth(400'000'000), can(375'000'000)), Txflags(tfPassive));
6630 env(offer(ed, can(375'000'000), usd(338'000'000))), Txflags(tfPassive);
6631 }
6632
6633 if (i > 0)
6634 amm.emplace(env, ed, eth(1'000'000'000), usd(1'000'000'000));
6635
6636 env(pay(carol_, bob_, usd(100'000'000)),
6637 Path(~MPT(usd)),
6638 Path(~MPT(can), ~MPT(usd)),
6639 Sendmax(eth(600'000'000)));
6640 env.close();
6641
6642 env.require(Balance(bob_, usd(2'100'000'000)));
6643
6644 if (i == 2)
6645 {
6646 // NOLINTBEGIN(bugprone-unchecked-optional-access) i==2 implies amm is
6647 // emplaced (i>0)
6648 if (rates.first == kLowRate)
6649 {
6650 // Liquidity is consumed from AMM strand only
6651 BEAST_EXPECT(amm->expectBalances(
6652 eth(1'124'584'936), usd(889'999'993), amm->tokens()));
6653 }
6654 else
6655 {
6656 BEAST_EXPECT(amm->expectBalances(
6657 eth(1'103'723'909), usd(906'023'688), amm->tokens()));
6658 BEAST_EXPECT(expectOffers(
6659 env,
6660 ed,
6661 2,
6662 {{Amounts{
6663 eth(327'069'745),
6664 can(306'627'886),
6665 },
6666 Amounts{
6667 can(312'843'533),
6668 usd(281'976'305),
6669 }}}));
6670 }
6671 // NOLINTEND(bugprone-unchecked-optional-access)
6672 }
6673 q[i] = Quality(
6674 Amounts{
6675 eth(2'000'000'000) - env.balance(carol_, MPT(eth)),
6676 env.balance(bob_, MPT(usd)) - usd(2'000'000'000)});
6677 }
6678 BEAST_EXPECT(q[1] > q[0]);
6679 BEAST_EXPECT(q[2] > q[0] && q[2] < q[1]);
6680 }
6681 }
6682 }
6683
6684 void
6686 {
6687 testcase("Malformed");
6688 using namespace jtx;
6689
6690 testAMM(
6691 [&](AMM& ammAlice, Env& env) {
6692 WithdrawArg const args{
6693 .flags = tfSingleAsset,
6694 .err = Ter(temMALFORMED),
6695 };
6696 ammAlice.withdraw(args);
6697 },
6698 {{XRP(10'000), gAmmmpt(10'000)}});
6699
6700 testAMM(
6701 [&](AMM& ammAlice, Env& env) {
6702 WithdrawArg const args{
6703 .flags = tfOneAssetLPToken,
6704 .err = Ter(temMALFORMED),
6705 };
6706 ammAlice.withdraw(args);
6707 },
6708 {{XRP(10'000), gAmmmpt(10'000)}});
6709
6710 testAMM(
6711 [&](AMM& ammAlice, Env& env) {
6712 WithdrawArg const args{
6713 .flags = tfLimitLPToken,
6714 .err = Ter(temMALFORMED),
6715 };
6716 ammAlice.withdraw(args);
6717 },
6718 {{XRP(10'000), gAmmmpt(10'000)}});
6719
6720 testAMM(
6721 [&](AMM& ammAlice, Env& env) {
6722 WithdrawArg const args{
6723 .asset1Out = MPT(ammAlice[1])(100),
6724 .asset2Out = MPT(ammAlice[1])(100),
6725 .err = Ter(temBAD_AMM_TOKENS),
6726 };
6727 ammAlice.withdraw(args);
6728 },
6729 {{XRP(10'000), gAmmmpt(10'000)}});
6730
6731 testAMM(
6732 [&](AMM& ammAlice, Env& env) {
6733 MPTTester const btc(
6734 {.env = env,
6735 .issuer = gw_,
6736 .holders = {alice_, carol_},
6737 .pay = 2'000,
6738 .flags = tfMPTCanLock | kMptDexFlags});
6739 WithdrawArg const args{
6740 .asset1Out = XRP(100),
6741 .asset2Out = btc(100),
6742 .err = Ter(temBAD_AMM_TOKENS),
6743 };
6744 ammAlice.withdraw(args);
6745 },
6746 {{XRP(10'000), gAmmmpt(10'000)}});
6747
6748 testAMM(
6749 [&](AMM& ammAlice, Env& env) {
6750 json::Value jv;
6751 jv[jss::TransactionType] = jss::AMMWithdraw;
6752 jv[jss::Flags] = tfLimitLPToken;
6753 jv[jss::Account] = alice_.human();
6754 ammAlice.setTokens(jv);
6755 XRP(100).value().setJson(jv[jss::Amount]);
6756 MPTTester const btc(
6757 {.env = env,
6758 .issuer = gw_,
6759 .holders = {alice_, carol_},
6760 .pay = 2'000,
6761 .flags = tfMPTCanLock | kMptDexFlags});
6762 btc(100).value().setJson(jv[jss::EPrice]);
6763 env(jv, Ter(telENV_RPC_FAILED));
6764 },
6765 {{XRP(10'000), gAmmmpt(10'000)}});
6766 }
6767
6768 void
6770 {
6771 testcase("AMM Offer Blocked By LOB");
6772 using namespace jtx;
6773
6774 // Low quality LOB offer blocks AMM liquidity
6775
6776 // USD/MPT crosses AMM despite of low quality LOB
6777 {
6778 Env env(*this, features);
6779
6780 fund(env, gw_, {alice_, carol_}, XRP(1'000'000), {USD(1'000'000)});
6781 MPT const btc = MPTTester(
6782 {.env = env,
6783 .issuer = gw_,
6784 .holders = {alice_, carol_},
6785 .pay = 40'000'000000,
6786 .flags = kMptDexFlags});
6787
6788 env(offer(alice_, btc(1), USD(0.01)));
6789 env.close();
6790 AMM const amm(env, gw_, btc(200'000), USD(100'000));
6791 env(offer(carol_, USD(0.49), btc(1)));
6792 env.close();
6793
6794 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
6795 {
6796 BEAST_EXPECT(amm.expectBalances(btc(200'000), USD(100'000), amm.tokens()));
6797 BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), USD(0.01)}}}));
6798 BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{USD(0.49), btc(1)}}}));
6799 }
6800
6801 if (features[fixAMMv1_1] && features[fixAMMv1_3])
6802 {
6803 BEAST_EXPECT(amm.expectBalances(btc(200'001), USD(99'999.51), amm.tokens()));
6804 BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), USD(0.01)}}}));
6805 // Carol's offer crosses AMM
6806 BEAST_EXPECT(expectOffers(env, carol_, 0));
6807 }
6808 }
6809
6810 // XRP/MPT crosses AMM despite of low quality LOB
6811 {
6812 Env env(*this, features);
6813
6814 fund(env, gw_, {alice_, carol_}, XRP(1'000'000), {USD(1'000'000)});
6815 MPT const btc = MPTTester(
6816 {.env = env,
6817 .issuer = gw_,
6818 .holders = {alice_, carol_},
6819 .pay = 40'000'000000,
6820 .flags = kMptDexFlags});
6821
6822 env(offer(alice_, btc(1), XRP(0.01)));
6823 env.close();
6824 AMM const amm(env, gw_, btc(200'000), XRP(100'000));
6825 env(offer(carol_, XRP(0.49), btc(1)));
6826 env.close();
6827
6828 if (!features[fixAMMv1_1] && !features[fixAMMv1_3])
6829 {
6830 BEAST_EXPECT(amm.expectBalances(btc(200'000), XRP(100'000), amm.tokens()));
6831 BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), XRP(0.01)}}}));
6832 BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(0.49), btc(1)}}}));
6833 }
6834
6835 if (features[fixAMMv1_1] && features[fixAMMv1_3])
6836 {
6837 BEAST_EXPECT(amm.expectBalances(btc(200'001), XRP(99'999.51), amm.tokens()));
6838 BEAST_EXPECT(expectOffers(env, alice_, 1, {{Amounts{btc(1), XRP(0.01)}}}));
6839 // Carol's offer crosses AMM
6840 BEAST_EXPECT(expectOffers(env, carol_, 0));
6841 }
6842 }
6843 }
6844
6845 void
6847 {
6848 testcase("LPToken Balance");
6849 using namespace jtx;
6850
6851 Env env(*this, features);
6852 Account const gw{"gateway"}, alice{"alice"}, bob{"bob"};
6853 env.fund(XRP(100000), gw, alice, bob);
6854 env.close();
6855 env(fset(gw, asfAllowTrustLineClawback));
6856 env.close();
6857
6858 auto const usd = gw["USD"];
6859 env.trust(usd(100000), alice);
6860 env(pay(gw, alice, usd(50000)));
6861 env.trust(usd(100000), bob);
6862 env(pay(gw, bob, usd(40000)));
6863 env.close();
6864
6865 MPT const btc = MPTTester(
6866 {.env = env,
6867 .issuer = gw,
6868 .holders = {alice, bob},
6869 .pay = 40'000'000000,
6870 .flags = tfMPTCanClawback | tfMPTCanLock | kMptDexFlags});
6871
6872 AMM amm(env, alice, btc(2), usd(1));
6873 amm.deposit(alice, IOUAmount{1'876123487565916, -15});
6874 amm.deposit(bob, IOUAmount{1'000});
6875 amm.withdraw(alice, IOUAmount{1'876123487565916, -15});
6876 amm.withdrawAll(bob);
6877
6878 auto const lpToken =
6879 getAccountLines(env, alice, amm.lptIssue())[jss::lines][0u][jss::balance].asString();
6880 auto const lpTokenBalance =
6881 amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value].asString();
6882
6883 BEAST_EXPECT(lpToken == "1.414213562374011" && lpTokenBalance == "1.4142135623741");
6884
6885 auto res = isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice);
6886 BEAST_EXPECT(res && res.value());
6887
6888 amm.withdrawAll(alice);
6889 BEAST_EXPECT(!amm.ammExists());
6890 }
6891
6892 void
6894 {
6895 testcase("AMM with vault shares — underlying freeze blocks share withdrawal");
6896 using namespace jtx;
6897 // AMMTestBase::testableAmendments() strips featureSingleAssetVault,
6898 // but vault shares require it. Use the global jtx set directly.
6900
6901 // When alice's underlying asset is individually frozen:
6902 //
6903 // Deposit (post-fixCleanup3_3_0): checkDepositFreeze checks the AMM
6904 // pseudo-account's underlying, not alice's — deposit is allowed.
6905 // Pre-fix: featureAMMClawback calls isFrozen(alice, share) which
6906 // descends via isVaultPseudoAccountFrozen(alice,...) and finds the
6907 // frozen underlying — deposit is blocked.
6908 //
6909 // Withdrawal (post-fixCleanup3_3_0): checkWithdrawFreeze ends with
6910 // checkDeepFrozen(alice, share) which calls isFrozen(alice, share)
6911 // and finds the frozen underlying — withdrawal is blocked.
6912 // Pre-fix: the old path only checks the AMM account's MPToken lock,
6913 // which is unset — withdrawal succeeds.
6914
6915 auto runIOU = [&](FeatureBitset const& features) {
6916 bool const fix330 = features[fixCleanup3_3_0];
6917 Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
6918
6919 env.fund(XRP(100'000), gw_, alice_);
6920 env(fset(gw_, asfDefaultRipple));
6921 env.close();
6922
6923 PrettyAsset const iou = gw_["IOU"];
6924 env.trust(iou(1'000'000), alice_);
6925 env(pay(gw_, alice_, iou(10'000)));
6926 env.close();
6927
6928 Vault const vault{env};
6929 auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = iou});
6930 env(createTx);
6931 env.close();
6932
6933 // 200 IOU → 200,000,000 vault shares (IOU vault scale = 6)
6934 env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = iou(200)}));
6935 env.close();
6936
6937 auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID);
6938 // Use half the shares for the AMM; alice keeps the other half.
6939 STAmount const shareAmt{MPTIssue{shareMPTID}, 100'000'000};
6940 // Pool: XRP(100) = 1e8 drops, shares = 1e8 → LP ≈ 1e8
6941 AMM amm{env, alice_, XRP(100), shareAmt};
6942 env.close();
6943
6944 // Freeze alice's IOU trustline (individual freeze on underlying).
6945 env(trust(gw_, iou(0), alice_, tfSetFreeze));
6946 env.close();
6947
6948 // post-fix330: checkDepositFreeze checks AMM pseudo's underlying
6949 // (not alice's) → deposit is allowed
6950 // pre-fix330: featureAMMClawback path calls isFrozen(alice, share)
6951 // which descends to alice's frozen IOU → tecLOCKED
6952 amm.deposit(
6953 {.account = alice_,
6954 .asset1In = XRP(1),
6955 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))});
6956
6957 // post-fix330: checkWithdrawFreeze → checkDeepFrozen(alice, share)
6958 // descends to alice's frozen IOU → tecLOCKED
6959 // pre-fix330: the AMM pseudo-account is not authorized for the
6960 // share's underlying (requireAuth recurses share→IOU
6961 // and the AMM holds no IOU trustline), so
6962 // accountHolds(ZeroIfUnauthorized) reports the pool's
6963 // share balance as 0 and the withdrawal math fails →
6964 // tecAMM_FAILED. Vault shares deposited into an AMM are
6965 // only withdrawable once fixCleanup3_3_0 exempts the
6966 // pseudo-account from the recursive auth check.
6967 amm.withdraw(
6968 {.account = alice_,
6969 .tokens = 1'000,
6970 .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))});
6971
6972 env(trust(gw_, iou(0), alice_, tfClearFreeze));
6973 env.close();
6974
6975 // Lifting the freeze lets the deposit through in both cases. The
6976 // withdrawal only succeeds post-fix330; pre-fix330 the share balance
6977 // remains inaccessible to the unauthorized pseudo-account, so the
6978 // shares stay stuck → tecAMM_FAILED.
6979 amm.deposit({.account = alice_, .asset1In = XRP(1)});
6980 amm.withdraw(
6981 {.account = alice_,
6982 .tokens = 1'000,
6983 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))});
6984 };
6985
6986 runIOU(all);
6987 runIOU(all - fixCleanup3_3_0);
6988
6989 auto runMPT = [&](FeatureBitset const& features) {
6990 bool const fix330 = features[fixCleanup3_3_0];
6991 // Expected freeze failures fire invariant checks that log at Error;
6992 // silence them so the test output stays clean.
6993 Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
6994
6995 env.fund(XRP(100'000), gw_, alice_);
6996 env.close();
6997
6998 MPTTester mptt{env, gw_, kMptInitNoFund};
6999 mptt.create({.flags = kMptDexFlags | tfMPTCanLock});
7000 PrettyAsset const mpt = mptt.issuanceID();
7001 mptt.authorize({.account = alice_});
7002 env(pay(gw_, alice_, mpt(30'000)));
7003 env.close();
7004
7005 Vault const vault{env};
7006 auto [createTx, vaultKeylet] = vault.create({.owner = alice_, .asset = mpt});
7007 env(createTx);
7008 env.close();
7009
7010 // 20000 MPT → 20000 vault shares (MPT vault scale = 0)
7011 env(vault.deposit({.depositor = alice_, .id = vaultKeylet.key, .amount = mpt(20'000)}));
7012 env.close();
7013
7014 auto const shareMPTID = env.le(vaultKeylet)->at(sfShareMPTID);
7015 // Use half the shares for the AMM; alice keeps the other half.
7016 // Pool: XRP(100) = 1e8 drops, shares = 10000 → LP ≈ 1e6
7017 STAmount const shareAmt{MPTIssue{shareMPTID}, 10'000};
7018 AMM amm{env, alice_, XRP(100), shareAmt};
7019 env.close();
7020
7021 // Lock alice's underlying MPT (individual lock).
7022 mptt.set({.holder = alice_, .flags = tfMPTLock});
7023
7024 // Same pre/post-fix330 semantics as the IOU case above.
7025 amm.deposit(
7026 {.account = alice_,
7027 .asset1In = XRP(1),
7028 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecLOCKED))});
7029
7030 // {.tokens = 1'000} → frac = 1000/1e6 = 0.001
7031 // XRP out = 1e8 * 0.001 = 1e5 drops, shares out = 10000 * 0.001 = 10
7032 // post-fix330: checkWithdrawFreeze sees alice's locked underlying
7033 // MPT via the share → tecLOCKED.
7034 // pre-fix330: the AMM pseudo-account is unauthorized for the
7035 // share's underlying MPT (it holds no underlying
7036 // MPToken), so accountHolds(ZeroIfUnauthorized) zeros
7037 // the pool's share balance and the math fails →
7038 // tecAMM_FAILED.
7039 amm.withdraw(
7040 {.account = alice_,
7041 .tokens = 1'000,
7042 .err = Ter(fix330 ? TER(tecLOCKED) : TER(tecAMM_FAILED))});
7043
7044 mptt.set({.holder = alice_, .flags = tfMPTUnlock});
7045
7046 // Unlocking lets the deposit through; the withdrawal only succeeds
7047 // post-fix330 (pre-fix330 the shares remain stuck → tecAMM_FAILED).
7048 amm.deposit({.account = alice_, .asset1In = XRP(1)});
7049 amm.withdraw(
7050 {.account = alice_,
7051 .tokens = 1'000,
7052 .err = Ter(fix330 ? TER(tesSUCCESS) : TER(tecAMM_FAILED))});
7053 };
7054
7055 runMPT(all);
7056 runMPT(all - fixCleanup3_3_0);
7057 }
7058
7059 void
7061 {
7062 testcase("test AMMDeposit with frozen assets");
7063 using namespace jtx;
7064
7065 // This lambda function is used to create trustline, MPT.
7066 // and create an AMM account.
7067 // And also test the callback function.
7068 auto testAMMDeposit = [&](Env& env, std::function<void(AMM & amm, MPTTester & btc)> cb) {
7069 env.fund(XRP(1'000), gw_, alice_);
7070 env.close();
7071 MPTTester btc(
7072 {.env = env,
7073 .issuer = gw_,
7074 .holders = {alice_},
7075 .pay = 30'000,
7076 .flags = tfMPTCanLock | kMptDexFlags});
7077
7078 AMM amm(env, alice_, btc(100), XRP(100));
7079 env.close();
7080 btc.set({.holder = alice_, .flags = tfMPTLock});
7081 cb(amm, btc);
7082 };
7083
7084 // Deposit two assets, one of which is locked,
7085 // then we should get tecLOCKED error.
7086 {
7087 Env env(*this);
7088 testAMMDeposit(env, [&](AMM& amm, MPTTester& btc) {
7089 amm.deposit(alice_, btc(100), XRP(100), std::nullopt, tfTwoAsset, Ter(tecLOCKED));
7090 });
7091 }
7092
7093 // Deposit one asset, which is the locked token,
7094 // then we should get tecLOCKED error.
7095 {
7096 Env env(*this);
7097 testAMMDeposit(env, [&](AMM& amm, MPTTester& btc) {
7098 amm.deposit(
7099 alice_, btc(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tecLOCKED));
7100 });
7101 }
7102
7103 // Deposit one asset which is not the frozen token,
7104 // but the other asset is frozen. We should get tecLOCKED error
7105 // when feature AMMClawback is enabled.
7106 {
7107 Env env(*this);
7108 testAMMDeposit(env, [&](AMM& amm, MPTTester& btc) {
7109 amm.deposit(
7110 alice_, XRP(100), std::nullopt, std::nullopt, tfSingleAsset, Ter(tecLOCKED));
7111 });
7112 }
7113 }
7114
7115 void
7117 {
7118 testcase("Auto Delete");
7119
7120 using namespace jtx;
7121 FeatureBitset const all{testableAmendments()};
7122
7123 {
7124 Env env(
7125 *this,
7127 cfg->fees.referenceFee = XRPAmount(1);
7128 return cfg;
7129 }),
7130 all);
7131 env.fund(XRP(1'000), gw_, alice_);
7132 MPTTester const usd({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000});
7133 MPTTester const btc({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000});
7134 AMM amm(env, gw_, usd(10'000), btc(10'000));
7135 for (auto i = 0; i < kMaxDeletableAmmTrustLines + 10; ++i)
7136 {
7137 Account const a{std::to_string(i)};
7138 env.fund(XRP(1'000), a);
7139 env(trust(a, STAmount{amm.lptIssue(), 10'000}));
7140 env.close();
7141 }
7142 // The trustlines are partially deleted,
7143 // AMM is set to an empty state.
7144 amm.withdrawAll(gw_);
7145 BEAST_EXPECT(amm.ammExists());
7146
7147 // Bid,Vote,Deposit,Withdraw,SetTrust failing with
7148 // tecAMM_EMPTY. Deposit succeeds with tfTwoAssetIfEmpty option.
7149 env(amm.bid({
7150 .account = alice_,
7151 .bidMin = 1000,
7152 }),
7153 Ter(tecAMM_EMPTY));
7154 amm.vote({.account = alice_, .tfee = 100, .err = Ter(tecAMM_EMPTY)});
7155 amm.withdraw({.account = alice_, .tokens = 100, .err = Ter(tecAMM_EMPTY)});
7156 amm.deposit({.account = alice_, .asset1In = usd(100), .err = Ter(tecAMM_EMPTY)});
7157 env(trust(alice_, STAmount{amm.lptIssue(), 10'000}), Ter(tecAMM_EMPTY));
7158
7159 // Can deposit with tfTwoAssetIfEmpty option
7160 amm.deposit(
7161 {.account = alice_,
7162 .asset1In = usd(1'000),
7163 .asset2In = btc(1'000),
7164 .flags = tfTwoAssetIfEmpty,
7165 .tfee = 1'000});
7166 BEAST_EXPECT(amm.expectBalances(usd(1'000), btc(1'000), IOUAmount{1'000}));
7167 BEAST_EXPECT(amm.expectTradingFee(1'000));
7168 BEAST_EXPECT(amm.expectAuctionSlot(100, 0, IOUAmount{0}));
7169
7170 // Withdrawing all tokens deletes AMM since the number
7171 // of remaining trustlines is less than max
7172 amm.withdrawAll(alice_);
7173 BEAST_EXPECT(!amm.ammExists());
7174 BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
7175 }
7176
7177 {
7178 Env env(
7179 *this,
7181 cfg->fees.referenceFee = XRPAmount(1);
7182 return cfg;
7183 }),
7184 all);
7185 env.fund(XRP(1'000), gw_, alice_);
7186 MPTTester const usd({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000});
7187 MPTTester const btc({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000});
7188 AMM amm(env, gw_, usd(10'000), btc(10'000));
7189 for (auto i = 0; i < (kMaxDeletableAmmTrustLines * 2) + 10; ++i)
7190 {
7191 Account const a{std::to_string(i)};
7192 env.fund(XRP(1'000), a);
7193 env(trust(a, STAmount{amm.lptIssue(), 10'000}));
7194 env.close();
7195 }
7196 // The trustlines are partially deleted.
7197 amm.withdrawAll(gw_);
7198 BEAST_EXPECT(amm.ammExists());
7199
7200 // AMMDelete has to be called twice to delete AMM.
7201 amm.ammDelete(alice_, Ter(tecINCOMPLETE));
7202 BEAST_EXPECT(amm.ammExists());
7203 // Deletes remaining trustlines and deletes AMM.
7204 amm.ammDelete(alice_);
7205 BEAST_EXPECT(!amm.ammExists());
7206 BEAST_EXPECT(!env.le(keylet::ownerDir(amm.ammAccount())));
7207
7208 // Try redundant delete
7209 amm.ammDelete(alice_, Ter(terNO_AMM));
7210 }
7211
7212 {
7213 Env env(
7214 *this,
7216 cfg->fees.referenceFee = XRPAmount(1);
7217 return cfg;
7218 }),
7219 all);
7220 env.fund(XRP(1'000), gw_, alice_, carol_);
7221 MPTTester const usd(
7222 {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 20'000});
7223 MPTTester const btc(
7224 {.env = env, .issuer = gw_, .holders = {alice_, carol_}, .pay = 20'000});
7225 AMM amm(env, gw_, usd(10'000), btc(10'000));
7226 amm.deposit({.account = alice_, .tokens = 1'000});
7227 amm.deposit({.account = carol_, .tokens = 1'000});
7228 amm.withdrawAll(alice_);
7229 amm.withdrawAll(carol_);
7230 amm.withdrawAll(gw_);
7231 BEAST_EXPECT(!amm.ammExists());
7232 }
7233
7234 // This test validates both invariant changes work together for
7235 // the specific case of MPT/MPT pools with > kMaxDeletableAmmTrustLines.
7236 {
7237 Env env(
7238 *this,
7240 cfg->fees.referenceFee = XRPAmount(1);
7241 return cfg;
7242 }),
7243 all);
7244
7245 env.fund(XRP(1'000), gw_, alice_);
7246 MPT const usd =
7247 MPTTester({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000});
7248 MPT const btc =
7249 MPTTester({.env = env, .issuer = gw_, .holders = {alice_}, .pay = 20'000});
7250
7251 // MPT/MPT pool with MANY trustlines
7252 AMM amm(env, gw_, usd(10'000), btc(10'000));
7253 for (auto i = 0; i < (kMaxDeletableAmmTrustLines * 2) + 10; ++i)
7254 {
7255 Account const a{std::to_string(i)};
7256 env.fund(XRP(1'000), a);
7257 env(trust(a, STAmount{amm.lptIssue(), 10'000}));
7258 env.close();
7259 }
7260
7261 amm.withdrawAll(gw_);
7262 // AMM is in empty state, but can't be auto-deleted because of the LPTokens trustlines.
7263 BEAST_EXPECT(amm.expectBalances(usd(0), btc(0), IOUAmount(0)));
7264 BEAST_EXPECT(amm.ammExists());
7265
7266 // Critical: MPT/MPT pool + tecINCOMPLETE
7267 amm.ammDelete(alice_, Ter(tecINCOMPLETE));
7268 BEAST_EXPECT(amm.ammExists());
7269
7270 amm.ammDelete(alice_);
7271 BEAST_EXPECT(!amm.ammExists());
7272 }
7273 }
7274
7275 void
7276 run() override
7277 {
7281 for (auto const& f : jtx::amendmentCombinations({fixCleanup3_3_0, featureAMMClawback}, all))
7283 testDeposit();
7285 testWithdraw();
7287 testFeeVote();
7289 testBid(all);
7290 testClawback();
7292 testClawbackFromAMMAccount(all - featureSingleAssetVault);
7295 testAMMTokens();
7296 testAmendment();
7297 testAMMAndCLOB(all);
7298 testTradingFee(all);
7299 testTradingFee(all - fixAMMv1_3);
7300 testAdjustedTokens(all);
7301 testAMMID();
7302 testSelection(all);
7303 testMalformed();
7304 testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3);
7306 testLPTokenBalance(all);
7307 testLPTokenBalance(all - fixAMMv1_3);
7311 }
7312};
7313
7315
7316} // namespace xrpl::test
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:522
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
Represents a JSON value.
Definition json_value.h:130
Value removeMember(char const *key)
Remove and return the named member.
virtual Config & config()=0
std::unordered_set< uint256, beast::Uhash<> > features
Definition Config.h:261
Floating point representation of amounts with high dynamic range.
Definition IOUAmount.h:24
A currency issued by an account.
Definition Issue.h:13
std::chrono::time_point< NetClock > time_point
Definition chrono.h:46
Number is a floating point type that can represent a wide range of values.
Definition Number.h:306
Represents the logical ratio of output currency to input currency.
Definition Quality.h:91
static constexpr int kMinOffset
Definition STAmount.h:47
static constexpr std::uint64_t kMinValue
Definition STAmount.h:51
json::Value getJson(JsonOptions) const override
Definition STIssue.cpp:82
jtx::Account const alice_
Definition AMMTest.h:74
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={testableAmendments()})
testAMM() funds 30,000XRP and 30,000IOU for each non-XRP asset to Alice and Carol
Definition AMMTest.cpp:120
jtx::Account const gw_
Definition AMMTest.h:72
static FeatureBitset testableAmendments()
Definition AMMTest.h:86
jtx::Account const carol_
Definition AMMTest.h:73
jtx::Account const bob_
Definition AMMTest.h:75
static XRPAmount reserve(jtx::Env &env, std::uint32_t count)
Definition AMMTest.cpp:198
static XRPAmount ammCrtFee(jtx::Env &env)
Definition AMMTest.cpp:204
Convenience class to test AMM functionality.
bool expectTradingFee(std::uint16_t fee) const
Definition AMM.cpp:340
json::Value bid(BidArg const &arg)
Definition AMM.cpp:737
IOUAmount getLPTokensBalance(std::optional< AccountID > const &account=std::nullopt) const
Definition AMM.cpp:282
bool ammExists() const
Definition AMM.cpp:347
IOUAmount withdrawAll(std::optional< Account > const &account, std::optional< STAmount > const &asset1OutDetails=std::nullopt, std::optional< Ter > const &ter=std::nullopt)
bool expectAuctionSlot(std::uint32_t fee, std::optional< std::uint8_t > timeSlot, IOUAmount expectedPrice) const
Definition AMM.cpp:307
IOUAmount tokens() const
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:466
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:607
AccountID const & ammAccount() const
void setTokens(json::Value &jv, std::optional< std::pair< Asset, Asset > > const &assets=std::nullopt)
Definition AMM.cpp:395
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< Asset, Asset > > const &assets=std::nullopt, std::optional< Ter > const &ter=std::nullopt)
Definition AMM.cpp:711
static json::Value voteJv(VoteArg const &arg)
Definition AMM.cpp:693
bool expectLPTokens(AccountID const &account, IOUAmount const &tokens) const
Definition AMM.cpp:296
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:269
Immutable cryptographic account descriptor.
Definition jtx/Account.h:17
AccountID id() const
Returns the Account ID.
Definition jtx/Account.h:85
A transaction testing environment.
Definition Env.h:143
Application & app()
Definition Env.h:280
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:284
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:296
Account const & master
Definition Env.h:147
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:864
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:566
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:327
std::shared_ptr< STTx const > ust(JTx const &jt)
Create a STTx from a JTx without sanitizing Use to inject bogus values into test transactions by firs...
Definition Env.cpp:637
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:511
bool enabled(uint256 feature) const
Definition Env.h:715
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:174
beast::Journal const journal
Definition Env.h:184
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:605
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:353
NetClock::time_point now()
Returns the current network time.
Definition Env.h:305
Set the fee on a JTx.
Definition fee.h:15
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:385
void set(MPTSet const &set={})
Definition mpt.cpp:482
void create(MPTCreate const &arg=MPTCreate{})
Definition mpt.cpp:256
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:368
MPTID const & issuanceID() const
Definition mpt.h:576
Converts to MPT Issue or STAmount.
Add a path.
Definition paths.h:39
Sets the SendMax on a JTx.
Definition sendmax.h:13
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:13
Set the flags on a JTx.
Definition txflags.h:9
T is_same_v
T make_pair(T... args)
@ Object
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:357
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:425
json::Value create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
Deposit preauthorize operations.
Definition deposit.h:7
json::Value create(AccountID const &account, AccountID const &to, STAmount const &amount)
Definition escrow.cpp:21
auto const kCondition
Definition escrow.h:74
auto const kCancelTime
Set the "CancelAfter" time tag on a JTx.
Definition escrow.h:72
auto const kFinishTime
Set the "FinishAfter" time tag on a JTx.
Definition escrow.h:69
std::array< std::uint8_t, 39 > const kCb1
Definition escrow.h:47
json::Value create(AccountID const &account, AccountID const &to, STAmount const &amount, NetClock::duration const &settleDelay, PublicKey const &pk, std::optional< NetClock::time_point > const &cancelAfter, std::optional< std::uint32_t > const &dstTag)
auto const kMptDexFlags
Definition mpt.h:25
json::Value claw(Account const &account, STAmount const &amount, std::optional< Account > const &mptHolder)
Definition trust.cpp:51
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
std::vector< STAmount > fund(jtx::Env &env, jtx::Account const &gw, std::vector< jtx::Account > const &accounts, std::vector< STAmount > const &amts, Fund how)
Definition AMMTest.cpp:34
bool expectLedgerEntryRoot(Env &env, Account const &acct, STAmount const &expectedValue)
bool expectMPT(Env &env, AccountID const &account, STAmount const &value)
std::vector< FeatureBitset > amendmentCombinations(std::initializer_list< uint256 > features, FeatureBitset seed)
Returns all 2^N permutations of a seed FeatureBitset with each subset of the given features excluded.
Definition Env.h:107
bool expectOffers(Env &env, AccountID const &account, std::uint16_t size, std::vector< Amounts > const &toMatch)
void testHelper2TokensMix(TTester &&tester)
bool expectHolding(Env &env, AccountID const &account, STAmount const &value, bool defaultLimits)
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
XRPAmount txFee(Env const &env, std::uint16_t n)
FeatureBitset testableAmendments()
Definition Env.h:76
static auto gAmmmpt
Definition AMMTest.h:33
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:28
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
json::Value rate(Account const &account, double multiplier)
Set a transfer rate.
Definition rate.cpp:15
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
json::Value getAccountLines(Env &env, AccountID const &acctId)
json::Value getAccountOffers(Env &env, AccountID const &acct, bool current)
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
static MPTInit const kMptInitNoFund
Definition mpt.h:142
STTx createTx(bool disabling, LedgerIndex seq, PublicKey const &txKey)
Create ttUNL_MODIFY Tx.
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:48
BEAST_DEFINE_TESTSUITE_PRIO(AccountDelete, app, xrpl, 2)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ telENV_RPC_FAILED
Definition TER.h:52
@ terNO_AMM
Definition TER.h:219
@ terNO_ACCOUNT
Definition TER.h:209
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:97
constexpr std::uint32_t kTotalTimeSlotSecs
Definition AMMCore.h:14
std::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.
TAmounts< STAmount, STAmount > Amounts
Definition Quality.h:64
STAmount amountFromString(Asset const &asset, std::string const &amount)
Definition STAmount.cpp:907
STAmount ammAssetOut(STAmount const &assetBalance, STAmount const &lptAMMBalance, STAmount const &lpTokens, std::uint16_t tfee)
Calculate asset withdrawal by tokens.
constexpr std::uint16_t kMaxDeletableAmmTrustLines
The maximum number of trustlines to delete as part of AMM account deletion cleanup.
Definition Protocol.h:280
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
MPTID noMPT()
Definition MPTIssue.h:103
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
Definition UintTypes.h:44
constexpr std::uint16_t kAuctionSlotTimeIntervals
Definition AMMCore.h:15
@ temBAD_FEE
Definition TER.h:78
@ temBAD_MPT
Definition TER.h:130
@ temBAD_AMM_TOKENS
Definition TER.h:115
@ temMALFORMED
Definition TER.h:73
@ temDISABLED
Definition TER.h:100
@ temBAD_AMOUNT
Definition TER.h:75
TERSubset< CanCvtToTER > TER
Definition TER.h:634
MPTID badMPT()
Definition MPTIssue.h:110
@ tecPSEUDO_ACCOUNT
Definition TER.h:360
@ tecAMM_EMPTY
Definition TER.h:330
@ tecLOCKED
Definition TER.h:356
@ tecPATH_PARTIAL
Definition TER.h:280
@ tecAMM_INVALID_TOKENS
Definition TER.h:329
@ tecINSUF_RESERVE_LINE
Definition TER.h:286
@ tecAMM_FAILED
Definition TER.h:328
@ tecAMM_NOT_EMPTY
Definition TER.h:331
@ tecPATH_DRY
Definition TER.h:292
@ tecINCOMPLETE
Definition TER.h:333
@ tecUNFUNDED_AMM
Definition TER.h:326
@ tecOBJECT_NOT_FOUND
Definition TER.h:324
@ tecNO_AUTH
Definition TER.h:298
@ tecAMM_BALANCE
Definition TER.h:327
@ tecFROZEN
Definition TER.h:301
@ tecAMM_ACCOUNT
Definition TER.h:332
@ tecOWNERS
Definition TER.h:296
@ tecNO_PERMISSION
Definition TER.h:303
@ tecDUPLICATE
Definition TER.h:313
@ tesSUCCESS
Definition TER.h:240
constexpr std::uint32_t kAuctionSlotIntervalDuration
Definition AMMCore.h:20
T push_back(T... args)
Zero allows classes to offer efficient comparisons to zero.
Definition Zero.h:25
Basic tests of AMM functionality involving MPT assets, excluding those that use offers.
void testTradingFee(FeatureBitset features)
void testClawbackFromAMMAccount(FeatureBitset features)
void testFixAMMOfferBlockedByLOB(FeatureBitset features)
void testLPTokenBalance(FeatureBitset features)
void testAdjustedTokens(FeatureBitset features)
void testSelection(FeatureBitset features)
void testInvalidDeposit(FeatureBitset features)
void run() override
Runs the suite.
void testBid(FeatureBitset features)
void testAMMAndCLOB(FeatureBitset features)
json::Value jv
Definition JTx.h:24
Set the sequence number on a JTx.
Definition seq.h:12
T to_string(T... args)