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