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