xrpld
Loading...
Searching...
No Matches
AMMExtendedMPT_test.cpp
1#include <test/jtx/AMM.h>
2#include <test/jtx/AMMTest.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/PathSet.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/balance.h>
8#include <test/jtx/delivermin.h>
9#include <test/jtx/fee.h>
10#include <test/jtx/flags.h>
11#include <test/jtx/jtx_json.h>
12#include <test/jtx/mpt.h>
13#include <test/jtx/multisign.h>
14#include <test/jtx/noop.h>
15#include <test/jtx/offer.h>
16#include <test/jtx/owners.h>
17#include <test/jtx/paths.h>
18#include <test/jtx/pay.h>
19#include <test/jtx/regkey.h>
20#include <test/jtx/require.h>
21#include <test/jtx/sendmax.h>
22#include <test/jtx/sig.h>
23#include <test/jtx/tags.h>
24#include <test/jtx/ter.h>
25#include <test/jtx/txflags.h>
26
27#include <xrpl/beast/unit_test/suite.h>
28#include <xrpl/json/json_value.h>
29#include <xrpl/json/to_string.h>
30#include <xrpl/ledger/ApplyView.h>
31#include <xrpl/ledger/OpenView.h>
32#include <xrpl/ledger/PaymentSandbox.h>
33#include <xrpl/ledger/Sandbox.h>
34#include <xrpl/ledger/helpers/OfferHelpers.h>
35#include <xrpl/protocol/AccountID.h>
36#include <xrpl/protocol/Feature.h>
37#include <xrpl/protocol/Indexes.h>
38#include <xrpl/protocol/Issue.h>
39#include <xrpl/protocol/KeyType.h>
40#include <xrpl/protocol/Quality.h>
41#include <xrpl/protocol/SField.h>
42#include <xrpl/protocol/STPathSet.h>
43#include <xrpl/protocol/Seed.h>
44#include <xrpl/protocol/TER.h>
45#include <xrpl/protocol/TxFlags.h>
46#include <xrpl/protocol/UintTypes.h>
47#include <xrpl/protocol/XRPAmount.h>
48#include <xrpl/protocol/jss.h>
49#include <xrpl/tx/paths/Flow.h>
50#include <xrpl/tx/paths/detail/Steps.h>
51
52#include <cstdint>
53#include <optional>
54#include <tuple>
55
56namespace xrpl::test {
57
62{
63private:
64 void
66 {
67 testcase("Incorrect Removal of Funded Offers");
68
69 // We need at least two paths. One at good quality and one at bad
70 // quality. The bad quality path needs two offer books in a row.
71 // Each offer book should have two offers at the same quality, the
72 // offers should be completely consumed, and the payment should
73 // require both offers to be satisfied. The first offer must
74 // be "taker gets" XRP. Ensure that the payment engine does not remove
75 // the first "taker gets" xrp offer, because the offer is still
76 // funded and not used for the payment.
77
78 using namespace jtx;
79 Env env{*this, features};
80
81 fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000));
82
83 MPTTester const eth(
84 {.env = env,
85 .issuer = gw_,
86 .holders = {alice_, bob_, carol_},
87 .pay = 200'000'000'000'000'000,
88 .flags = kMptDexFlags});
89
90 MPTTester const btc(
91 {.env = env,
92 .issuer = gw_,
93 .holders = {alice_, bob_, carol_},
94 .pay = 2'000'000'000'000'000,
95 .flags = kMptDexFlags});
96
97 // Must be two offers at the same quality
98 // "taker gets" must be XRP
99 // (Different amounts so I can distinguish the offers)
100 env(offer(carol_, btc(49'000'000'000'000), XRP(49)));
101 env(offer(carol_, btc(51'000'000'000'000), XRP(51)));
102
103 // Offers for the poor quality path
104 // Must be two offers at the same quality
105 env(offer(carol_, XRP(50), eth(50'000'000'000'000)));
106 env(offer(carol_, XRP(50), eth(50'000'000'000'000)));
107
108 // Good quality path
109 AMM const ammCarol(env, carol_, btc(1'000'000'000'000'000), eth(100'100'000'000'000'000));
110
111 PathSet const paths(TestPath(XRP, MPT(eth)), TestPath(MPT(eth)));
112
113 env(pay(alice_, bob_, eth(100'000'000'000'000)),
114 Json(paths.json()),
115 Sendmax(btc(1'000'000'000'000'000)),
116 Txflags(tfPartialPayment));
117
118 BEAST_EXPECT(ammCarol.expectBalances(
119 btc(1'001'000'000'374'816), eth(100'000'000'000'000'000), ammCarol.tokens()));
120
121 env.require(Balance(bob_, eth(200'100'000'000'000'000)));
122 BEAST_EXPECT(isOffer(env, carol_, btc(49'000'000'000'000), XRP(49)));
123 }
124
125 void
127 {
128 testcase("Fill Modes");
129 using namespace jtx;
130
131 auto const startBalance = XRP(1'000'000);
132
133 // Fill or Kill - unless we fully cross, just charge a fee and don't
134 // place the offer on the books. But also clean up expired offers
135 // that are discovered along the way.
136 testAMM(
137 [&](AMM& ammAlice, Env& env) {
138 auto const& btc = MPT(ammAlice[1]);
139 auto const baseFee = env.current()->fees().base;
140 auto carolBTC = env.balance(carol_, btc);
141 auto carolXRP = env.balance(carol_, XRP);
142 // Order that can't be filled
143 env(offer(carol_, btc(100), XRP(100)), Txflags(tfFillOrKill), Ter(tecKILLED));
144 env.close();
145 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000), ammAlice.tokens()));
146 // fee = AMM
147 env.require(Balance(carol_, carolXRP - baseFee));
148 env.require(Balance(carol_, carolBTC));
149
150 BEAST_EXPECT(expectOffers(env, carol_, 0));
151 carolXRP = env.balance(carol_, XRP);
152
153 // Order that can be filled
154 env(offer(carol_, XRP(100), btc(100)), Txflags(tfFillOrKill), Ter(tesSUCCESS));
155 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100), ammAlice.tokens()));
156 env.require(Balance(carol_, carolXRP + XRP(100) - baseFee));
157 env.require(Balance(carol_, carolBTC - btc(100)));
158 BEAST_EXPECT(expectOffers(env, carol_, 0));
159 },
160 {{XRP(10'100), gAmmmpt(10'000)}},
161 0,
162 std::nullopt,
163 {features});
164
165 // Immediate or Cancel - cross as much as possible
166 // and add nothing on the books.
167 testAMM(
168 [&](AMM& ammAlice, Env& env) {
169 auto const& btc = MPT(ammAlice[1]);
170 auto const baseFee = env.current()->fees().base;
171 auto carolBTC = env.balance(carol_, btc);
172 auto carolXRP = env.balance(carol_, XRP);
173 env(offer(carol_, XRP(200), btc(200)),
174 Txflags(tfImmediateOrCancel),
175 Ter(tesSUCCESS));
176
177 // AMM generates a synthetic offer of 100BTC/100XRP
178 // to match the CLOB offer quality.
179 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100), ammAlice.tokens()));
180 // +AMM - offer * fee
181 env.require(Balance(carol_, carolXRP + XRP(100) - baseFee));
182 env.require(Balance(carol_, carolBTC - btc(100)));
183 BEAST_EXPECT(expectOffers(env, carol_, 0));
184 },
185 {{XRP(10'100), gAmmmpt(10'000)}},
186 0,
187 std::nullopt,
188 {features});
189
190 // tfPassive -- place the offer without crossing it.
191 {
192 Env env{*this, features};
193 fund(env, gw_, {alice_, carol_}, XRP(30'000'000));
194
195 MPTTester const btc(
196 {.env = env,
197 .issuer = gw_,
198 .holders = {alice_, carol_},
199 .pay = 30'000'000,
200 .flags = kMptDexFlags});
201
202 AMM const ammAlice(env, alice_, XRP(10'100'000), btc(10'000'000));
203
204 // Scale the exact-quality fixture up so the visual relationship
205 // stays clear: the passive CLOB offer has the same 1:1 quality as
206 // the generated AMM offer, so it should not cross.
207 env(offer(carol_, XRP(100'000), btc(100'000), tfPassive));
208 env.close();
209 BEAST_EXPECT(
210 ammAlice.expectBalances(XRP(10'100'000), btc(10'000'000), ammAlice.tokens()));
211 BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100'000), btc(100'000)}}}));
212 }
213
214 // tfPassive -- cross only offers of better quality.
215 testAMM(
216 [&](AMM& ammAlice, Env& env) {
217 auto const& btc = MPT(ammAlice[1]);
218 env(offer(alice_, btc(110), XRP(100)));
219 env.close();
220
221 // Carol creates a passive offer. That offer should cross
222 // AMM and leave Alice's offer untouched.
223 env(offer(carol_, XRP(100), btc(100), tfPassive));
224 env.close();
225 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'900), btc(9083), ammAlice.tokens()));
226 BEAST_EXPECT(expectOffers(env, carol_, 0));
227 BEAST_EXPECT(expectOffers(env, alice_, 1));
228 },
229 {{XRP(11'000), gAmmmpt(9'000)}},
230 0,
231 std::nullopt,
232 {features});
233 }
234
235 void
237 {
238 testcase("Offer Crossing with XRP, Normal order");
239
240 using namespace jtx;
241
242 Env env{*this, features};
243
244 fund(env, gw_, {bob_, alice_}, XRP(300'000));
245
246 MPTTester const btc(
247 {.env = env,
248 .issuer = gw_,
249 .holders = {alice_, bob_},
250 .pay = 100'000'000,
251 .flags = kMptDexFlags});
252
253 AMM const ammAlice(env, alice_, XRP(150'000), btc(50'000'000));
254
255 // Existing offer pays better than this wants.
256 // Partially consume existing offer.
257 // Pay 1'000'000 BTC, get 3061224490 Drops.
258 auto const xrpTransferred = XRPAmount{3'061'224'490};
259 env(offer(bob_, btc(1'000'000), XRP(4'000)));
260
261 BEAST_EXPECT(ammAlice.expectBalances(
262 XRP(150'000) + xrpTransferred, btc(49'000'000), IOUAmount{273'861'278752583, -5}));
263
264 env.require(Balance(bob_, btc(101'000'000)));
265 BEAST_EXPECT(
266 expectLedgerEntryRoot(env, bob_, XRP(300'000) - xrpTransferred - 2 * txFee(env, 1)));
267 BEAST_EXPECT(expectOffers(env, bob_, 0));
268 }
269
270 void
272 {
273 testcase("Offer Crossing with Limit Override");
274
275 using namespace jtx;
276
277 Env env{*this, features};
278
279 env.fund(XRP(200'000), gw_, alice_, bob_);
280 env.close();
281
282 MPTTester const btc(
283 {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
284 env(pay(gw_, alice_, btc(500'000'000)));
285
286 AMM const ammAlice(env, alice_, XRP(150'000), btc(51'000'000));
287 env(offer(bob_, btc(1'000'000), XRP(3'000)));
288
289 BEAST_EXPECT(ammAlice.expectBalances(XRP(153'000), btc(50'000'000), ammAlice.tokens()));
290
291 env.require(Balance(bob_, btc(1'000'000)));
292 env.require(Balance(bob_, XRP(200'000) - XRP(3'000) - env.current()->fees().base * 2));
293 }
294
295 void
297 {
298 testcase("Currency Conversion: Entire Offer");
299
300 using namespace jtx;
301
302 Env env{*this, features};
303
304 fund(env, gw_, {alice_, bob_}, XRP(10'000));
305 env.require(Owners(bob_, 0));
306
307 MPTTester const btc(
308 {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
309 env(pay(gw_, bob_, btc(1'000'000'000)));
310
311 env.require(Owners(alice_, 1), Owners(bob_, 1));
312
313 env(pay(gw_, alice_, btc(100'000'000)));
314 AMM const ammBob(env, bob_, btc(200'000'000), XRP(1'500));
315
316 env(pay(alice_, alice_, XRP(500)), Sendmax(btc(100'000'000)));
317
318 BEAST_EXPECT(ammBob.expectBalances(btc(300'000'000), XRP(1'000), ammBob.tokens()));
319 env.require(Balance(alice_, btc(0)));
320
321 auto jrr = ledgerEntryRoot(env, alice_);
322 env.require(Balance(alice_, XRP(10'000) + XRP(500) - env.current()->fees().base * 2));
323 }
324
325 void
327 {
328 testcase("Currency Conversion: In Parts");
329
330 using namespace jtx;
331
332 Env env{*this, features};
333 env.fund(XRP(30'000), gw_, bob_);
334 env.fund(XRP(40'000), alice_);
335
336 MPTTester const btc(
337 {.env = env,
338 .issuer = gw_,
339 .holders = {alice_, bob_},
340 .pay = 30'000'000'000,
341 .flags = kMptDexFlags});
342 env(pay(gw_, alice_, btc(10'000'000'000)));
343
344 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'000'000'000));
345 env.close();
346
347 // Alice converts BTC to XRP which should fail
348 // due to PartialPayment.
349 env(pay(alice_, alice_, XRP(100)), Sendmax(btc(100'000'000)), Ter(tecPATH_PARTIAL));
350
351 // Alice converts BTC to XRP, should succeed because
352 // we permit partial payment
353 env(pay(alice_, alice_, XRP(100)), Sendmax(btc(100'000'000)), Txflags(tfPartialPayment));
354 env.close();
355 BEAST_EXPECT(ammAlice.expectBalances(
356 XRPAmount{9'900'990'100}, btc(10'100'000'000), ammAlice.tokens()));
357 // initial 40,000'000'000 - 10,000'000'000AMM - 100'000'000pay
358 env.require(Balance(alice_, btc(29'900'000'000)));
359 // initial 40,000 - 10,0000AMM + 99.009900pay - fee*3
360 BEAST_EXPECT(expectLedgerEntryRoot(
361 env,
362 alice_,
363 XRP(40'000) - XRP(10'000) + XRPAmount{99'009'900} - ammCrtFee(env) - txFee(env, 3)));
364 }
365
366 void
368 {
369 testcase("Cross Currency Payment: Start with XRP");
370
371 using namespace jtx;
372
373 Env env{*this, features};
374 env.fund(XRP(30'000), gw_);
375 env.fund(XRP(40'000), alice_);
376 env.fund(XRP(1'000), bob_);
377
378 MPTTester const btc(
379 {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
380 env(pay(gw_, alice_, btc(10'100'000'000)));
381
382 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000));
383 env.close();
384
385 env(pay(alice_, bob_, btc(100'000'000)), Sendmax(XRP(100)));
386 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000'000'000), ammAlice.tokens()));
387 env.require(Balance(bob_, btc(100'000'000)));
388 }
389
390 void
392 {
393 testcase("Cross Currency Payment: End with XRP");
394
395 using namespace jtx;
396
397 Env env{*this, features};
398 env.fund(XRP(30'000), gw_);
399 env.fund(XRP(40'100), alice_);
400 env.fund(XRP(1'000), bob_);
401
402 MPTTester const btc(
403 {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
404 env(pay(gw_, alice_, btc(40'000'000'000)));
405
406 AMM const ammAlice(env, alice_, XRP(10'100), btc(10'000'000'000));
407 env.close();
408
409 env(pay(alice_, bob_, XRP(100)), Sendmax(btc(100'000'000)));
410 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100'000'000), ammAlice.tokens()));
411 BEAST_EXPECT(expectLedgerEntryRoot(env, bob_, XRP(1'000) + XRP(100) - txFee(env, 1)));
412 }
413
414 void
416 {
417 testcase("Cross Currency Payment: Bridged");
418
419 using namespace jtx;
420
421 auto test = [&](auto&& issue1, auto&& issue2) {
422 Env env(*this);
423 auto const dan = Account{"dan"};
424 env.fund(XRP(60'000), alice_, bob_, carol_, gw_, dan);
425 env.close();
426 auto const eth = issue1(
427 {.env = env,
428 .token = "ETH",
429 .issuer = gw_,
430 .holders = {alice_, bob_, carol_, dan},
431 .limit = 10'000'000'000'000'000});
432 auto const btc = issue2(
433 {.env = env,
434 .token = "BTC",
435 .issuer = gw_,
436 .holders = {alice_, bob_, carol_, dan},
437 .limit = 10'000'000'000'000'000});
438 env(pay(gw_, alice_, btc(500'000'000'000'000)));
439 env(pay(gw_, carol_, btc(6'000'000'000'000'000)));
440 env(pay(gw_, dan, eth(400'000'000'000'000)));
441 env.close();
442 env.close();
443 AMM const ammCarol(env, carol_, btc(5'000'000'000'000'000), XRP(50'000));
444
445 env(offer(dan, XRP(500), eth(50'000'000'000'000)));
446 env.close();
447
449 jtp[0u][0u][jss::currency] = "XRP";
450 env(pay(alice_, bob_, eth(30'000'000'000'000)),
451 Json(jss::Paths, jtp),
452 Sendmax(btc(333'000'000'000'000)));
453 env.close();
454 BEAST_EXPECT(ammCarol.expectBalances(
455 XRP(49'700), btc(5'030'181'086'519'115), ammCarol.tokens()));
456 BEAST_EXPECT(expectOffers(env, dan, 1, {{Amounts{XRP(200), eth(20'000'000'000'000)}}}));
457 env.require(Balance(bob_, eth(30'000'000'000'000)));
458 };
460 }
461
462 void
464 {
465 testcase("Offer Fees Consume Funds");
466
467 using namespace jtx;
468
469 Env env{*this, features};
470
471 // Provide micro amounts to compensate for fees to make results round
472 // nice.
473 auto const startingXrp =
474 XRP(100) + env.current()->fees().accountReserve(2, 1) + env.current()->fees().base * 3;
475
476 env.fund(startingXrp, gw_, alice_);
477 env.fund(XRP(2'000), bob_);
478 env.close();
479
480 MPTTester const btc(
481 {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
482
483 // Created only to increase one reserve count for alice
484 MPTTester const eth(
485 {.env = env, .issuer = gw_, .holders = {alice_}, .flags = kMptDexFlags});
486
487 env(pay(gw_, bob_, btc(1'200'000'000'000'000)));
488
489 AMM const ammBob(env, bob_, XRP(1'000), btc(1'200'000'000'000'000));
490 // Alice has 400 - (2 reserve of 50 = 300 reserve) = 100 available.
491 // Ask for more than available to prove reserve works.
492 env(offer(alice_, btc(200'000'000'000'000), XRP(200)));
493
494 // The pool gets only 100XRP for ~109.09e12BTC, even though
495 // it can exchange more.
496 BEAST_EXPECT(
497 ammBob.expectBalances(XRP(1'100), btc(1'090'909'090'909'091), ammBob.tokens()));
498
499 env.require(Balance(alice_, btc(109'090'909'090'909)));
500 env.require(Balance(alice_, XRP(300)));
501 }
502
503 void
505 {
506 testcase("Offer Create, then Cross");
507
508 using namespace jtx;
509
510 Env env{*this, features};
511
512 fund(env, gw_, {alice_, bob_}, XRP(200'000));
513
514 MPTTester const btc(
515 {.env = env,
516 .issuer = gw_,
517 .holders = {alice_, bob_},
518 .transferFee = 500,
519 .flags = kMptDexFlags});
520
521 env(pay(gw_, bob_, btc(1'000'000'000'000)));
522 env(pay(gw_, alice_, btc(200'000'000'000'000)));
523
524 AMM const ammAlice(env, alice_, btc(150'000'000'000'000), XRP(150'100));
525 env(offer(bob_, XRP(100), btc(100'000'000'000)));
526
527 BEAST_EXPECT(
528 ammAlice.expectBalances(btc(150'100'000'000'000), XRP(150'000), ammAlice.tokens()));
529
530 // Bob pays 0.005 transfer fee.
531 env.require(Balance(bob_, btc(899'500'000'000)));
532 }
533
534 void
536 {
537 testcase("Offer tfSell: Basic Sell");
538
539 using namespace jtx;
540
541 Env env{*this, features};
542 env.fund(XRP(30'000), gw_, bob_, carol_);
543 env.fund(XRP(39'900), alice_);
544
545 MPTTester const btc(
546 {.env = env,
547 .issuer = gw_,
548 .holders = {alice_, bob_, carol_},
549 .pay = 30'000,
550 .flags = kMptDexFlags});
551 env(pay(gw_, alice_, btc(10'100)));
552
553 AMM const ammAlice(env, alice_, XRP(9'900), btc(10'100));
554
555 env(offer(carol_, btc(100), XRP(100)), Json(jss::Flags, tfSell));
556 env.close();
557 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(9'999), ammAlice.tokens()));
558 BEAST_EXPECT(expectOffers(env, carol_, 0));
559 env.require(Balance(carol_, btc(30'101)));
560 BEAST_EXPECT(
561 expectLedgerEntryRoot(env, carol_, XRP(30'000) - XRP(100) - 2 * txFee(env, 1)));
562 }
563
564 void
566 {
567 testcase("Offer tfSell: 2x Sell Exceed Limit");
568
569 using namespace jtx;
570
571 Env env{*this, features};
572
573 auto const startingXrp = XRP(100) + reserve(env, 1) + env.current()->fees().base * 2;
574
575 env.fund(startingXrp, gw_, alice_);
576 env.fund(XRP(2'000), bob_);
577 env.close();
578
579 MPTTester const btc(
580 {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
581 env(pay(gw_, bob_, btc(2'200'000'000)));
582
583 AMM const ammBob(env, bob_, XRP(1'000), btc(2'200'000'000));
584 // Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available.
585 // Ask for more than available to prove reserve works.
586 // Taker pays 100'000'000 BTC for 100 XRP.
587 // Selling XRP.
588 // Will sell all 100 XRP and get more BTC than asked for.
589 env(offer(alice_, btc(100'000'000), XRP(200)), Json(jss::Flags, tfSell));
590 BEAST_EXPECT(ammBob.expectBalances(XRP(1'100), btc(2'000'000'000), ammBob.tokens()));
591 env.require(Balance(alice_, btc(200'000'000)));
592 BEAST_EXPECT(expectLedgerEntryRoot(env, alice_, XRP(250)));
593 BEAST_EXPECT(expectOffers(env, alice_, 0));
594 }
595
596 void
598 {
599 testcase("Client Issue: Gateway Cross Currency");
600
601 using namespace jtx;
602
603 Env env{*this, features};
604
605 auto const startingXrp = XRP(100.1) + reserve(env, 1) + env.current()->fees().base * 2;
606 env.fund(startingXrp, gw_, alice_, bob_);
607
608 MPTTester const xts(
609 {.env = env,
610 .issuer = gw_,
611 .holders = {alice_, bob_},
612 .pay = 1'000'000'000'000'000,
613 .flags = kMptDexFlags});
614 MPTTester const xxx(
615 {.env = env,
616 .issuer = gw_,
617 .holders = {alice_, bob_},
618 .pay = 1'000'000'000'000'000,
619 .flags = kMptDexFlags});
620
621 AMM const ammAlice(env, alice_, xts(1'000'000'000'000'000), xxx(1'000'000'000'000'000));
622
623 json::Value payment;
624 payment[jss::secret] = toBase58(generateSeed("bob"));
625 payment[jss::id] = env.seq(bob_);
626 payment[jss::build_path] = true;
627 payment[jss::tx_json] = pay(bob_, bob_, xxx(10'000'000'000'000));
628 payment[jss::tx_json][jss::Sequence] =
629 env.current()->read(keylet::account(bob_.id()))->getFieldU32(sfSequence);
630 payment[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base);
631 payment[jss::tx_json][jss::SendMax] =
632 xts(15'000'000'000'000).value().getJson(JsonOptions::Values::None);
633 payment[jss::tx_json][jss::Flags] = tfPartialPayment;
634 auto const jrr = env.rpc("json", "submit", to_string(payment));
635 BEAST_EXPECT(jrr[jss::result][jss::status] == "success");
636 BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
637
638 BEAST_EXPECT(ammAlice.expectBalances(
639 xts(1'010'101'010'101'011), xxx(990'000'000'000'000), ammAlice.tokens()));
640 env.require(Balance(bob_, xts(989'898'989'898'989)));
641 env.require(Balance(bob_, xxx(1'010'000'000'000'000)));
642 }
643
644 void
646 {
647 testcase("Bridged Crossing");
648
649 using namespace jtx;
650
651 {
652 Env env{*this, features};
653 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
654
655 MPTTester const btc(
656 {.env = env,
657 .issuer = gw_,
658 .holders = {alice_, bob_, carol_},
659 .pay = 15'000'000'000,
660 .flags = kMptDexFlags});
661 MPTTester const eth(
662 {.env = env,
663 .issuer = gw_,
664 .holders = {alice_, bob_, carol_},
665 .pay = 15'000'000'000,
666 .flags = kMptDexFlags});
667
668 // The scenario:
669 // o BTC/XRP AMM is created.
670 // o ETH/XRP AMM is created.
671 // o carol has ETH but wants BTC.
672 // Note that carol's offer must come last. If carol's offer is
673 // placed before AMM is created, then autobridging will not occur.
674 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000));
675 AMM const ammBob(env, bob_, eth(10'000'000'000), XRP(10'100));
676
677 // Carol makes an offer that consumes AMM liquidity and
678 // fully consumes Carol's offer.
679 env(offer(carol_, btc(100'000'000), eth(100'000'000)));
680 env.close();
681
682 BEAST_EXPECT(
683 ammAlice.expectBalances(XRP(10'100), btc(10'000'000'000), ammAlice.tokens()));
684 BEAST_EXPECT(ammBob.expectBalances(XRP(10'000), eth(10'100'000'000), ammBob.tokens()));
685 env.require(Balance(carol_, btc(15'100'000'000)));
686 env.require(Balance(carol_, eth(14'900'000'000)));
687 BEAST_EXPECT(expectOffers(env, carol_, 0));
688 }
689
690 {
691 Env env{*this, features};
692 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
693
694 MPTTester const btc(
695 {.env = env,
696 .issuer = gw_,
697 .holders = {alice_, bob_, carol_},
698 .pay = 15'000'000'000,
699 .flags = kMptDexFlags});
700 MPTTester const eth(
701 {.env = env,
702 .issuer = gw_,
703 .holders = {alice_, bob_, carol_},
704 .pay = 15'000'000'000,
705 .flags = kMptDexFlags});
706
707 // The scenario:
708 // o BTC/XRP AMM is created.
709 // o ETH/XRP offer is created.
710 // o carol has ETH but wants BTC.
711 // Note that carol's offer must come last. If carol's offer is
712 // placed before AMM and bob's offer are created, then autobridging
713 // will not occur.
714 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000'000));
715 env(offer(bob_, eth(100'000'000), XRP(100)));
716 env.close();
717
718 // Carol makes an offer that consumes AMM liquidity and
719 // fully consumes Carol's offer.
720 env(offer(carol_, btc(100'000'000), eth(100'000'000)));
721 env.close();
722
723 BEAST_EXPECT(
724 ammAlice.expectBalances(XRP(10'100), btc(10'000'000'000), ammAlice.tokens()));
725 env.require(Balance(carol_, btc(15'100'000'000)));
726 env.require(Balance(carol_, eth(14'900'000'000)));
727 BEAST_EXPECT(expectOffers(env, carol_, 0));
728 BEAST_EXPECT(expectOffers(env, bob_, 0));
729 }
730
731 {
732 Env env{*this, features};
733 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
734
735 MPTTester const btc(
736 {.env = env,
737 .issuer = gw_,
738 .holders = {alice_, bob_, carol_},
739 .pay = 15'000'000'000,
740 .flags = kMptDexFlags});
741 MPTTester const eth(
742 {.env = env,
743 .issuer = gw_,
744 .holders = {alice_, bob_, carol_},
745 .pay = 15'000'000'000,
746 .flags = kMptDexFlags});
747
748 // The scenario:
749 // o BTC/XRP offer is created.
750 // o ETH/XRP AMM is created.
751 // o carol has ETH but wants BTC.
752 // Note that carol's offer must come last. If carol's offer is
753 // placed before AMM and alice's offer are created, then
754 // autobridging will not occur.
755 env(offer(alice_, XRP(100), btc(100'000'000)));
756 env.close();
757 AMM const ammBob(env, bob_, eth(10'000'000'000), XRP(10'100));
758
759 // Carol makes an offer that consumes AMM liquidity and
760 // fully consumes Carol's offer.
761 env(offer(carol_, btc(100'000'000), eth(100'000'000)));
762 env.close();
763
764 BEAST_EXPECT(ammBob.expectBalances(XRP(10'000), eth(10'100'000'000), ammBob.tokens()));
765 env.require(Balance(carol_, btc(15'100'000'000)));
766 env.require(Balance(carol_, eth(14'900'000'000)));
767 BEAST_EXPECT(expectOffers(env, carol_, 0));
768 BEAST_EXPECT(expectOffers(env, alice_, 0));
769 }
770 }
771
772 void
774 {
775 // Test a number of different corner cases regarding offer crossing
776 // when both the tfSell flag and tfFillOrKill flags are set.
777 testcase("Combine tfSell with tfFillOrKill");
778
779 using namespace jtx;
780
781 {
782 Env env{*this, features};
783 env.fund(XRP(30'000), gw_, alice_, bob_);
784
785 MPTTester const btc(
786 {.env = env,
787 .issuer = gw_,
788 .holders = {alice_, bob_},
789 .pay = 20'000'000'000,
790 .flags = kMptDexFlags});
791 AMM const ammBob(env, bob_, XRP(20'000), btc(200'000'000));
792 // alice submits a tfSell | tfFillOrKill offer that does not cross.
793 env(offer(alice_, btc(2'100'000), XRP(210), tfSell | tfFillOrKill), Ter(tecKILLED));
794
795 BEAST_EXPECT(ammBob.expectBalances(XRP(20'000), btc(200'000'000), ammBob.tokens()));
796 BEAST_EXPECT(expectOffers(env, bob_, 0));
797 }
798 {
799 Env env{*this, features};
800 env.fund(XRP(30'000), gw_, alice_, bob_);
801
802 MPTTester const btc(
803 {.env = env,
804 .issuer = gw_,
805 .holders = {alice_, bob_},
806 .pay = 1'000'000'000'000'000,
807 .flags = kMptDexFlags});
808 AMM const ammBob(env, bob_, XRP(20'000), btc(200'000'000'000'000));
809 // alice submits a tfSell | tfFillOrKill offer that crosses.
810 // Even though tfSell is present it doesn't matter this time.
811 env(offer(alice_, btc(2'000'000'000'000), XRP(220), tfSell | tfFillOrKill));
812 env.close();
813 BEAST_EXPECT(
814 ammBob.expectBalances(XRP(20'220), btc(197'823'936'696'341), ammBob.tokens()));
815 env.require(Balance(alice_, btc(1'002'176'063'303'659)));
816 BEAST_EXPECT(expectOffers(env, alice_, 0));
817 }
818 {
819 // alice submits a tfSell | tfFillOrKill offer that crosses and
820 // returns more than was asked for (because of the tfSell flag).
821 Env env{*this, features};
822 env.fund(XRP(30'000), gw_, alice_, bob_);
823
824 MPTTester const btc(
825 {.env = env,
826 .issuer = gw_,
827 .holders = {alice_, bob_},
828 .pay = 1'000'000'000'000'000,
829 .flags = kMptDexFlags});
830 AMM const ammBob(env, bob_, XRP(20'000), btc(200'000'000'000'000));
831
832 env(offer(alice_, btc(10'000'000'000'000), XRP(1'500), tfSell | tfFillOrKill));
833 env.close();
834
835 BEAST_EXPECT(
836 ammBob.expectBalances(XRP(21'500), btc(186'046'511'627'907), ammBob.tokens()));
837 env.require(Balance(alice_, btc(1'013'953'488'372'093)));
838 BEAST_EXPECT(expectOffers(env, alice_, 0));
839 }
840 {
841 // alice submits a tfSell | tfFillOrKill offer that doesn't cross.
842 // This would have succeeded with a regular tfSell, but the
843 // fillOrKill prevents the transaction from crossing since not
844 // all of the offer is consumed because AMM generated offer,
845 // which matches alice's offer quality is ~ 10XRP/0.01996e3BTC.
846 Env env{*this, features};
847 env.fund(XRP(30'000), gw_, alice_, bob_);
848
849 MPTTester const btc(
850 {.env = env,
851 .issuer = gw_,
852 .holders = {alice_, bob_},
853 .pay = 10'000'000'000,
854 .flags = kMptDexFlags});
855 AMM const ammBob(env, bob_, XRP(5000), btc(10'000'000));
856
857 env(offer(alice_, btc(1'000'000), XRP(501), tfSell | tfFillOrKill), Ter(tecKILLED));
858 env.close();
859 BEAST_EXPECT(expectOffers(env, alice_, 0));
860 BEAST_EXPECT(expectOffers(env, bob_, 0));
861 }
862 }
863
864 void
866 {
867 testcase("Transfer Rate Offer");
868
869 using namespace jtx;
870
871 // AMM XRP/BTC. Alice places BTC/XRP offer.
872 {
873 Env env(*this, features);
874 env.fund(XRP(30'000), gw_, bob_, carol_);
875 env.fund(XRP(40'000), alice_);
876
877 MPTTester const btc(
878 {.env = env,
879 .issuer = gw_,
880 .holders = {alice_, bob_, carol_},
881 .transferFee = 25'000,
882 .pay = 30'000'000,
883 .flags = kMptDexFlags});
884 env(pay(gw_, alice_, btc(10'100'000)));
885
886 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000));
887 env.close();
888
889 env(offer(carol_, btc(100'000), XRP(100)));
890 env.close();
891
892 // AMM doesn't pay the transfer fee
893 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000'000), ammAlice.tokens()));
894 env.require(Balance(carol_, btc(30'100'000)));
895 BEAST_EXPECT(expectOffers(env, carol_, 0));
896 }
897
898 {
899 Env env(*this, features);
900 env.fund(XRP(30'000), gw_, bob_, carol_);
901 env.fund(XRP(40'100), alice_);
902
903 MPTTester const btc(
904 {.env = env,
905 .issuer = gw_,
906 .holders = {alice_, bob_, carol_},
907 .transferFee = 25'000,
908 .pay = 30'000'000,
909 .flags = kMptDexFlags});
910 env(pay(gw_, alice_, btc(10'000'000)));
911
912 AMM const ammAlice(env, alice_, XRP(10'100), btc(10'000'000));
913 env.close();
914
915 env(offer(carol_, XRP(100), btc(100'000)));
916 env.close();
917
918 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'100'000), ammAlice.tokens()));
919 // Carol pays 25% transfer fee
920 env.require(Balance(carol_, btc(29'875'000)));
921 BEAST_EXPECT(expectOffers(env, carol_, 0));
922 }
923
924 {
925 // Bridged crossing.
926 Env env{*this, features};
927 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
928
929 MPTTester const btc(
930 {.env = env,
931 .issuer = gw_,
932 .holders = {alice_, bob_, carol_},
933 .transferFee = 25'000,
934 .pay = 15'000'000,
935 .flags = kMptDexFlags});
936 MPTTester const eth(
937 {.env = env,
938 .issuer = gw_,
939 .holders = {alice_, bob_, carol_},
940 .transferFee = 25'000,
941 .pay = 15'000'000,
942 .flags = kMptDexFlags});
943
944 // The scenario:
945 // o BTC/XRP AMM is created.
946 // o ETH/XRP Offer is created.
947 // o carol has ETH but wants BTC.
948 // Note that Carol's offer must come last. If Carol's offer is
949 // placed before AMM is created, then autobridging will not occur.
950 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'100'000));
951 env(offer(bob_, eth(100'000), XRP(100)));
952 env.close();
953
954 // Carol makes an offer that consumes AMM liquidity and
955 // fully consumes Bob's offer.
956 env(offer(carol_, btc(100'000), eth(100'000)));
957 env.close();
958
959 // AMM doesn't pay the transfer fee
960 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000'000), ammAlice.tokens()));
961 env.require(Balance(carol_, btc(15'100'000)));
962 // Carol pays 25% transfer fee.
963 env.require(Balance(carol_, eth(14'875'000)));
964 BEAST_EXPECT(expectOffers(env, carol_, 0));
965 BEAST_EXPECT(expectOffers(env, bob_, 0));
966 }
967
968 {
969 // Bridged crossing. The transfer fee is paid on the step not
970 // involving AMM as src/dst.
971 Env env{*this, features};
972 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
973
974 MPTTester const btc(
975 {.env = env,
976 .issuer = gw_,
977 .holders = {alice_, bob_, carol_},
978 .transferFee = 25'000,
979 .pay = 15'000'000,
980 .flags = kMptDexFlags});
981 MPTTester const eth(
982 {.env = env,
983 .issuer = gw_,
984 .holders = {alice_, bob_, carol_},
985 .transferFee = 25'000,
986 .pay = 15'000'000,
987 .flags = kMptDexFlags});
988
989 // The scenario:
990 // o BTC/XRP AMM is created.
991 // o ETH/XRP Offer is created.
992 // o carol has ETH but wants BTC.
993 // Note that Carol's offer must come last. If Carol's offer is
994 // placed before AMM is created, then autobridging will not occur.
995 AMM const ammAlice(env, alice_, XRP(10'000), btc(10'050'000));
996 env(offer(bob_, eth(100'000), XRP(100)));
997 env.close();
998
999 // Carol makes an offer that consumes AMM liquidity and
1000 // partially consumes Bob's offer.
1001 env(offer(carol_, btc(50'000), eth(50'000)));
1002 env.close();
1003 // This test verifies that the amount removed from an offer
1004 // accounts for the transfer fee that is removed from the
1005 // account but not from the remaining offer.
1006
1007 // AMM doesn't pay the transfer fee
1008 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'050), btc(10'000'000), ammAlice.tokens()));
1009 env.require(Balance(carol_, btc(15'050'000)));
1010 // Carol pays 25% transfer fee.
1011 env.require(Balance(carol_, eth(14'937'500)));
1012 BEAST_EXPECT(expectOffers(env, carol_, 0));
1013 BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{eth(50'000), XRP(50)}}}));
1014 }
1015 }
1016
1017 void
1019 {
1020 // This test is not the same as corresponding testSelfIssueOffer()
1021 // in the Offer_test. It simply tests AMM with self issue and
1022 // offer crossing.
1023 using namespace jtx;
1024
1025 Env env{*this, features};
1026
1027 auto const f = env.current()->fees().base;
1028
1029 env.fund(XRP(30'000) + f, alice_, bob_);
1030 env.close();
1031
1032 MPTTester const btc(
1033 {.env = env, .issuer = bob_, .holders = {alice_}, .flags = kMptDexFlags});
1034
1035 AMM const ammBob(env, bob_, XRP(10'000), btc(10'100));
1036
1037 env(offer(alice_, btc(100), XRP(100)));
1038 env.close();
1039
1040 BEAST_EXPECT(ammBob.expectBalances(XRP(10'100), btc(10'000), ammBob.tokens()));
1041 BEAST_EXPECT(expectOffers(env, alice_, 0));
1042 env.require(Balance(alice_, btc(100)));
1043 }
1044
1045 void
1047 {
1048 // The offer crossing code expects that a DirectStep is always
1049 // preceded by a BookStep. In one instance the default path
1050 // was not matching that assumption. Here we recreate that case
1051 // so we can prove the bug stays fixed.
1052 testcase("Direct to Direct path");
1053
1054 using namespace jtx;
1055
1056 Env env{*this, features};
1057
1058 auto const ann = Account("ann");
1059 auto const bob = Account("bob");
1060 auto const cam = Account("cam");
1061 auto const carol = Account("carol");
1062
1063 auto const fee = env.current()->fees().base;
1064 env.fund(XRP(1'000), carol);
1065 env.fund(reserve(env, 4) + (fee * 5), ann, bob, cam);
1066 env.close();
1067
1068 MPTTester const aBux(
1069 {.env = env, .issuer = ann, .holders = {bob, cam, carol}, .flags = kMptDexFlags});
1070
1071 MPTTester const bBux(
1072 {.env = env, .issuer = bob, .holders = {ann, cam, carol}, .flags = kMptDexFlags});
1073
1074 env(pay(ann, cam, aBux(350'000'000'000'000)));
1075 env(pay(bob, cam, bBux(350'000'000'000'000)));
1076 env(pay(bob, carol, bBux(4'000'000'000'000'000)));
1077 env(pay(ann, carol, aBux(4'000'000'000'000'000)));
1078
1079 AMM const ammCarol(env, carol, aBux(3'000'000'000'000'000), bBux(3'300'000'000'000'000));
1080
1081 // cam puts an offer on the books that her upcoming offer could cross.
1082 // But this offer should be deleted, not crossed, by her upcoming
1083 // offer.
1084 env(offer(cam, aBux(290'000'000'000'000), bBux(300'000'000'000'000), tfPassive));
1085 env.close();
1086 env.require(Balance(cam, aBux(350'000'000'000'000)));
1087 env.require(Balance(cam, bBux(350'000'000'000'000)));
1088 env.require(offers(cam, 1));
1089
1090 // This offer caused the assert.
1091 env(offer(cam, bBux(300'000'000'000'000), aBux(300'000'000'000'000)));
1092
1093 // AMM is consumed up to the first cam Offer quality
1094 BEAST_EXPECT(ammCarol.expectBalances(
1095 aBux(3'093'541'659'651'603), bBux(3'200'215'509'984'419), ammCarol.tokens()));
1096 BEAST_EXPECT(expectOffers(
1097 env, cam, 1, {{Amounts{bBux(200'215'509'984'419), aBux(200'215'509'984'419)}}}));
1098 }
1099
1100 void
1102 {
1103 testcase("RequireAuth");
1104
1105 using namespace jtx;
1106
1107 Env env{*this, features};
1108 env.fund(XRP(400'000), gw_, alice_, bob_);
1109
1110 MPTTester btc(
1111 {.env = env,
1112 .issuer = gw_,
1113 .holders = {alice_, bob_},
1114 .flags = tfMPTRequireAuth | kMptDexFlags});
1115
1116 // Authorize bob and alice
1117 btc.authorize({.holder = alice_});
1118 btc.authorize({.holder = bob_});
1119
1120 env(pay(gw_, alice_, btc(1'000)));
1121 env.close();
1122
1123 // Alice is able to create AMM since the GW has authorized her
1124 AMM const ammAlice(env, alice_, btc(1'000), XRP(1'050));
1125
1126 env(pay(gw_, bob_, btc(50)));
1127 env.close();
1128
1129 env.require(Balance(bob_, btc(50)));
1130
1131 // Bob's offer should cross Alice's AMM
1132 env(offer(bob_, XRP(50), btc(50)));
1133 env.close();
1134
1135 BEAST_EXPECT(ammAlice.expectBalances(btc(1'050), XRP(1'000), ammAlice.tokens()));
1136 BEAST_EXPECT(expectOffers(env, bob_, 0));
1137 env.require(Balance(bob_, btc(0)));
1138 }
1139
1140 void
1142 {
1143 testcase("Missing Auth");
1144
1145 using namespace jtx;
1146
1147 Env env{*this, features};
1148
1149 env.fund(XRP(400'000), gw_, alice_, bob_);
1150 env.close();
1151
1152 MPTTester btc(
1153 {.env = env,
1154 .issuer = gw_,
1155 .holders = {alice_, bob_},
1156 .flags = tfMPTRequireAuth | kMptDexFlags});
1157
1158 // Alice doesn't have the funds
1159 {
1160 AMM const ammAlice(env, alice_, btc(1'000), XRP(1'000), Ter(tecNO_AUTH));
1161 }
1162
1163 btc.authorize({.holder = bob_});
1164 env(pay(gw_, bob_, btc(50)));
1165 env.close();
1166 env.require(Balance(bob_, btc(50)));
1167
1168 // Alice should not be able to create AMM without authorization.
1169 {
1170 AMM const ammAlice(env, alice_, btc(1'000), XRP(1'000), Ter(tecNO_AUTH));
1171 }
1172
1173 // Finally, authorize alice. Now alice's AMM create should succeed.
1174 btc.authorize({.holder = alice_});
1175 env(pay(gw_, alice_, btc(1'000)));
1176 env.close();
1177
1178 AMM const ammAlice(env, alice_, btc(1'000), XRP(1'050));
1179
1180 // Authorize AMM.
1181 // BTC.authorize({.account = ammAlice.ammAccount()});
1182 // env.close();
1183
1184 // Now bob creates his offer again, which crosses with alice's AMM.
1185 env(offer(bob_, XRP(50), btc(50)));
1186 env.close();
1187
1188 BEAST_EXPECT(ammAlice.expectBalances(btc(1'050), XRP(1'000), ammAlice.tokens()));
1189 BEAST_EXPECT(expectOffers(env, bob_, 0));
1190 env.require(Balance(bob_, btc(0)));
1191 }
1192
1193 void
1220
1221 void
1223 {
1224 testcase("path find consume all");
1225 using namespace jtx;
1226
1227 Env env = pathTestEnv();
1228 env.fund(XRP(100'000'260), alice_);
1229 env.fund(XRP(30'000), gw_, bob_, carol_);
1230
1231 MPTTester const eth(
1232 {.env = env,
1233 .issuer = gw_,
1234 .holders = {alice_, bob_, carol_},
1235 .pay = 100'000'000'000'000,
1236 .flags = kMptDexFlags});
1237
1238 AMM const ammCarol(env, carol_, XRP(100), eth(100'000'000'000'000));
1239
1240 STPathSet st;
1241 STAmount sa;
1242 STAmount da;
1243 std::tie(st, sa, da) = findPaths(
1244 env, alice_, bob_, bob_["AUD"](-1), std::optional<STAmount>(XRP(100'000'000)));
1245 BEAST_EXPECT(st.empty());
1246 std::tie(st, sa, da) =
1247 findPaths(env, alice_, bob_, eth(-1), std::optional<STAmount>(XRP(100'000'000)));
1248 // Alice sends all requested 100,000,000XRP
1249 BEAST_EXPECT(sa == XRP(100'000'000));
1250 // Bob gets ~99.99e12ETH. This is the amount Bob
1251 // can get out of AMM for 100,000,000XRP.
1252 BEAST_EXPECT(equal(da, eth(99'999'900'000'099)));
1253 }
1254
1255 // carol holds ETH, sells ETH for XRP
1256 // bob will hold ETH
1257 // alice pays bob ETH using XRP
1258 void
1260 {
1261 testcase("via gateway");
1262 using namespace jtx;
1263
1264 Env env = pathTestEnv();
1265 env.fund(XRP(10'000), alice_, bob_, carol_, gw_);
1266 env.close();
1267
1268 MPTTester const eth(
1269 {.env = env,
1270 .issuer = gw_,
1271 .holders = {alice_, bob_, carol_},
1272 .transferFee = 10'000,
1273 .flags = kMptDexFlags});
1274
1275 MPTTester const btc(
1276 {.env = env,
1277 .issuer = gw_,
1278 .holders = {alice_, bob_, carol_},
1279 .transferFee = 10'000,
1280 .flags = kMptDexFlags});
1281
1282 env(pay(gw_, carol_, eth(51)));
1283 env.close();
1284 AMM const ammCarol(env, carol_, XRP(40), eth(51));
1285 env(pay(alice_, bob_, eth(10)), Sendmax(XRP(100)), Paths(XRP));
1286 env.close();
1287 // AMM offer is 51.282052XRP/11ETH, 11ETH/1.1 = 10ETH to bob
1288 BEAST_EXPECT(ammCarol.expectBalances(XRP(51), eth(40), ammCarol.tokens()));
1289 env.require(Balance(bob_, eth(10)));
1290
1291 auto const result = findPaths(env, alice_, bob_, btc(25));
1292 BEAST_EXPECT(std::get<0>(result).empty());
1293 }
1294
1295 void
1297 {
1298 testcase("Receive max");
1299 using namespace jtx;
1300 auto const charlie = Account("charlie");
1301 {
1302 // XRP -> MPT receive max
1303 Env env = pathTestEnv();
1304 env.fund(XRP(30'000), alice_, bob_, charlie, gw_);
1305
1306 MPTTester const eth(
1307 {.env = env,
1308 .issuer = gw_,
1309 .holders = {alice_, bob_, charlie},
1310 .pay = 11'000'000'000'000,
1311 .flags = kMptDexFlags});
1312
1313 AMM const ammCharlie(env, charlie, XRP(10), eth(11'000'000'000'000));
1314 auto [st, sa, da] = findPaths(env, alice_, bob_, eth(-1), XRP(1).value());
1315 BEAST_EXPECT(sa == XRP(1));
1316 BEAST_EXPECT(equal(da, eth(1'000'000'000'000)));
1317 if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1))
1318 {
1319 auto const& pathElem = st[0][0];
1320 BEAST_EXPECT(
1321 pathElem.isOffer() && pathElem.getIssuerID() == gw_.id() &&
1322 pathElem.getMPTID() == eth.issuanceID());
1323 }
1324 }
1325 {
1326 // MPT -> XRP receive max
1327 Env env = pathTestEnv();
1328 env.fund(XRP(30'000), alice_, bob_, charlie, gw_);
1329
1330 MPTTester const eth(
1331 {.env = env,
1332 .issuer = gw_,
1333 .holders = {alice_, bob_, charlie},
1334 .pay = 11'000'000'000'000,
1335 .flags = kMptDexFlags});
1336
1337 AMM const ammCharlie(env, charlie, XRP(11), eth(10'000'000'000'000));
1338 env.close();
1339 auto [st, sa, da] =
1340 findPaths(env, alice_, bob_, drops(-1), eth(1'000'000'000'000).value());
1341 BEAST_EXPECT(sa == eth(1'000'000'000'000));
1342 BEAST_EXPECT(equal(da, XRP(1)));
1343 if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1))
1344 {
1345 auto const& pathElem = st[0][0];
1346 BEAST_EXPECT(
1347 pathElem.isOffer() && pathElem.getIssuerID() == xrpAccount() &&
1348 pathElem.getCurrency() == xrpCurrency());
1349 }
1350 }
1351 }
1352
1353 void
1355 {
1356 testcase("Path Find: XRP -> XRP and XRP -> MPT");
1357 using namespace jtx;
1358 Env env = pathTestEnv();
1359 Account a1{"A1"};
1360 Account a2{"A2"};
1361 Account a3{"A3"};
1362 Account const g1{"G1"};
1363 Account const g2{"G2"};
1364 Account g3{"G3"};
1365 Account m1{"M1"};
1366
1367 env.fund(XRP(100'000), a1);
1368 env.fund(XRP(10'000), a2);
1369 env.fund(XRP(1'000), a3, g1, g2, g3);
1370 env.fund(XRP(20'000), m1);
1371 env.close();
1372
1373 MPTTester const xyzG1(
1374 {.env = env, .issuer = g1, .holders = {a1, m1, a2}, .flags = kMptDexFlags});
1375
1376 MPTTester const xyzG2(
1377 {.env = env, .issuer = g2, .holders = {a2, m1, a1}, .flags = kMptDexFlags});
1378
1379 MPTTester const abcG3(
1380 {.env = env, .issuer = g3, .holders = {a1, a2, m1, a3}, .flags = kMptDexFlags});
1381
1382 MPTTester const abcA2(
1383 {.env = env, .issuer = a2, .holders = {g3, a1}, .flags = kMptDexFlags});
1384
1385 env(pay(g1, a1, xyzG1(3'500'000'000)));
1386 env(pay(g3, a1, abcG3(1'200'000'000)));
1387 env(pay(g1, m1, xyzG1(25'000'000'000)));
1388 env(pay(g2, m1, xyzG2(25'000'000'000)));
1389 env(pay(g3, m1, abcG3(25'000'000'000)));
1390 env(pay(a2, g3, abcA2(101'000'000)));
1391 env.close();
1392
1393 AMM const ammM1XyzG1XyzG2(env, m1, xyzG1(1'000'000'000), xyzG2(1'000'000'000));
1394 AMM const ammM1XrpAbcG3(env, m1, XRP(10'000), abcG3(1'000'000'000));
1395 AMM const ammG3AbcG3AbcA2(env, g3, abcG3(100'000'000), abcA2(101'000'000));
1396 env.close();
1397
1398 STPathSet st;
1399 STAmount sa, da;
1400
1401 {
1402 auto const& sendAmt = XRP(10);
1403 std::tie(st, sa, da) = findPaths(env, a1, a2, sendAmt, std::nullopt, xrpCurrency());
1404 BEAST_EXPECT(equal(da, sendAmt));
1405 BEAST_EXPECT(st.empty());
1406 }
1407
1408 {
1409 // no path should exist for this since dest account
1410 // does not exist.
1411 auto const& sendAmt = XRP(200);
1412 std::tie(st, sa, da) =
1413 findPaths(env, a1, Account{"A0"}, sendAmt, std::nullopt, xrpCurrency());
1414 BEAST_EXPECT(equal(da, sendAmt));
1415 BEAST_EXPECT(st.empty());
1416 }
1417
1418 {
1419 auto const& sendAmt = abcG3(10'000'000);
1420 std::tie(st, sa, da) = findPaths(env, a2, g3, sendAmt, std::nullopt, xrpCurrency());
1421 BEAST_EXPECT(equal(da, sendAmt));
1422 BEAST_EXPECT(equal(sa, XRPAmount{101'010'102}));
1423 BEAST_EXPECT(same(st, stpath(ipe(MPT(abcG3)))));
1424 }
1425
1426 {
1427 auto const& sendAmt = abcA2(1'000'000);
1428 std::tie(st, sa, da) = findPaths(env, a1, a2, sendAmt, std::nullopt, xrpCurrency());
1429 BEAST_EXPECT(equal(da, sendAmt));
1430 BEAST_EXPECT(equal(sa, XRPAmount{10'010'011}));
1431 BEAST_EXPECT(same(st, stpath(ipe(MPT(abcG3)), ipe(MPT(abcA2)))));
1432 }
1433 }
1434
1435 void
1437 {
1438 testcase("Path Find: non-XRP -> XRP");
1439 using namespace jtx;
1440 Env env = pathTestEnv();
1441 Account a1{"A1"};
1442 Account a2{"A2"};
1443 Account const g3{"G3"};
1444 Account m1{"M1"};
1445
1446 env.fund(XRP(1'000), a1, a2, g3);
1447 env.fund(XRP(11'000), m1);
1448 env.close();
1449
1450 MPTTester const eth(
1451 {.env = env,
1452 .issuer = g3,
1453 .holders = {a1, a2, m1},
1454 .pay = 1'000'000'000,
1455 .flags = kMptDexFlags});
1456
1457 AMM const ammM1(env, m1, eth(1'000'000'000), XRP(10'010));
1458
1459 STPathSet st;
1460 STAmount sa, da;
1461
1462 auto const& sendAmt = XRP(10);
1463
1464 std::tie(st, sa, da) =
1465 findPathsByElement(env, a1, a2, sendAmt, std::nullopt, ipe(MPT(eth)));
1466 BEAST_EXPECT(equal(da, sendAmt));
1467 BEAST_EXPECT(equal(sa, eth(1'000'000)));
1468 BEAST_EXPECT(same(st, stpath(ipe(xrpIssue()))));
1469 }
1470
1471 void
1473 {
1474 testcase("Path Find: non-XRP -> non-XRP, same issuanceID");
1475 using namespace jtx;
1476 {
1477 Env env = pathTestEnv();
1478 Account a1{"A1"};
1479 Account a2{"A2"};
1480 Account const a3{"A3"};
1481 Account const g1{"G1"};
1482 Account const g2{"G2"};
1483 Account m1{"M1"};
1484
1485 env.fund(XRP(11'000), m1);
1486 env.fund(XRP(1'000), a1, a2, a3, g1, g2);
1487 env.close();
1488
1489 MPTTester const hkdG1(
1490 {.env = env,
1491 .issuer = g1,
1492 .holders = {a1, m1},
1493 .pay = 5'000'000'000,
1494 .flags = kMptDexFlags});
1495
1496 MPTTester const hkdG2(
1497 {.env = env,
1498 .issuer = g2,
1499 .holders = {a2, m1},
1500 .pay = 5'000'000'000,
1501 .flags = kMptDexFlags});
1502
1503 AMM const ammM1(env, m1, hkdG1(1'000'000'000), hkdG2(1'010'000'000));
1504
1505 auto const& sendAmt = hkdG2(10'000'000);
1506 STPathSet st;
1507 STAmount sa, da;
1508 std::tie(st, sa, da) = jtx::findPaths(
1509 env, g1, a2, sendAmt, std::nullopt, hkdG1.issuanceID(), std::nullopt, std::nullopt);
1510 BEAST_EXPECT(equal(da, sendAmt));
1511 BEAST_EXPECT(equal(sa, hkdG1(10'000'000)));
1512 BEAST_EXPECT(same(st, stpath(ipe(MPT(hkdG2)))));
1513 }
1514 }
1515
1516 void
1518 {
1519 testcase("Path Find: MPT AMM source amount is executable");
1520 using namespace jtx;
1521
1522 auto const checkQuote = [&](std::int64_t usdPool,
1523 std::int64_t eurPool,
1524 std::int64_t deliverAmount,
1525 std::int64_t expectedSourceAmount) {
1526 Env env = pathTestEnv();
1527 env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
1528 env.close();
1529
1530 MPTTester const usd(
1531 {.env = env,
1532 .issuer = gw_,
1533 .holders = {alice_, bob_, carol_},
1534 .pay = usdPool,
1535 .flags = kMptDexFlags});
1536
1537 MPTTester const eur(
1538 {.env = env,
1539 .issuer = gw_,
1540 .holders = {alice_, bob_, carol_},
1541 .pay = eurPool,
1542 .flags = kMptDexFlags});
1543
1544 AMM const ammCarol(env, carol_, usd(usdPool), eur(eurPool));
1545 env.close();
1546
1547 STPathSet st;
1548 STAmount sa, da;
1549 auto const deliver = eur(deliverAmount);
1550 std::tie(st, sa, da) = findPaths(
1551 env,
1552 alice_,
1553 bob_,
1554 deliver,
1555 std::nullopt,
1556 usd.issuanceID(),
1557 std::nullopt,
1558 std::nullopt);
1559
1560 // Each quote must execute when used as an exact-output SendMax.
1561 BEAST_EXPECT(equal(da, deliver));
1562 BEAST_EXPECT(equal(sa, usd(expectedSourceAmount)));
1563 BEAST_EXPECT(!st.empty());
1564
1565 auto const before = eur.getBalance(bob_);
1566 env(pay(alice_, bob_, deliver),
1567 Json(jss::Paths, st.getJson(JsonOptions::Values::None)),
1568 Sendmax(sa),
1569 Txflags(tfNoRippleDirect));
1570 BEAST_EXPECT(eur.getBalance(bob_) == before + deliverAmount);
1571 };
1572
1573 struct TestCase
1574 {
1575 std::int64_t usdPool;
1576 std::int64_t eurPool;
1577 std::int64_t deliverAmount;
1578 std::int64_t expectedSourceAmount;
1579 };
1580
1581 // Cover the original 2:1 pool and the same pool scaled down by 1000.
1582 // clang-format off
1583 TestCase const testCases[] = {
1584 {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 1, .expectedSourceAmount = 3},
1585 {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 2, .expectedSourceAmount = 5},
1586 {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 10, .expectedSourceAmount = 21},
1587 {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 100, .expectedSourceAmount = 201},
1588 {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 1'000, .expectedSourceAmount = 2'003},
1589 {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 1, .expectedSourceAmount = 3},
1590 {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 2, .expectedSourceAmount = 5},
1591 {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 10, .expectedSourceAmount = 21},
1592 {.usdPool = 2'000, .eurPool = 1'000, .deliverAmount = 100, .expectedSourceAmount = 223},
1593 };
1594 // clang-format on
1595
1596 for (auto const& testCase : testCases)
1597 {
1598 checkQuote(
1599 testCase.usdPool,
1600 testCase.eurPool,
1601 testCase.deliverAmount,
1602 testCase.expectedSourceAmount);
1603 }
1604 }
1605
1606 void
1608 {
1609 testcase("falseDryChanges");
1610
1611 using namespace jtx;
1612
1613 Env env(*this, features);
1614 env.memoize(bob_);
1615
1616 env.fund(XRP(10'000), alice_, gw_);
1617 fund(env, gw_, {carol_}, XRP(10'000), {}, Fund::Acct);
1618 auto const ammxrpPool = env.current()->fees().increment * 2;
1619 env.fund(reserve(env, 5) + ammCrtFee(env) + ammxrpPool, bob_);
1620 env.close();
1621
1622 MPTTester const eth(
1623 {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags});
1624
1625 MPTTester const btc(
1626 {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags});
1627
1628 env(pay(gw_, alice_, eth(50'000)));
1629 env(pay(gw_, bob_, btc(150'000)));
1630
1631 // Bob has _just_ slightly less than 50 xrp available
1632 // If his owner count changes, he will have more liquidity.
1633 // This is one error case to test (when Flow is used).
1634 // Computing the incoming xrp to the XRP/BTC offer will require two
1635 // recursive calls to the ETH/XRP offer. The second call will return
1636 // tecPATH_DRY, but the entire path should not be marked as dry.
1637 // This is the second error case to test (when flowV1 is used).
1638 env(offer(bob_, eth(50'000), XRP(50)));
1639 AMM const ammBob(env, bob_, ammxrpPool, btc(150'000));
1640
1641 env(pay(alice_, carol_, btc(1'000'000'000)),
1642 Path(~XRP, ~MPT(btc)),
1643 Sendmax(eth(500'000)),
1644 Txflags(tfNoRippleDirect | tfPartialPayment));
1645
1646 auto const carolBTC = env.balance(carol_, MPT(btc));
1647 BEAST_EXPECT(carolBTC > btc(0) && carolBTC < btc(50'000));
1648 }
1649
1650 void
1652 {
1653 testcase("Book Step");
1654
1655 using namespace jtx;
1656
1657 // simple MPT/IOU mix offer
1658 {
1659 auto test = [&](auto&& issue1, auto&& issue2) {
1660 Env env(*this);
1661 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
1662 env.close();
1663 auto const eth = issue1(
1664 {.env = env,
1665 .token = "ETH",
1666 .issuer = gw_,
1667 .holders = {alice_, bob_, carol_},
1668 .limit = 100'000'000});
1669 auto const btc = issue2(
1670 {.env = env,
1671 .token = "BTC",
1672 .issuer = gw_,
1673 .holders = {alice_, bob_, carol_},
1674 .limit = 100'000'000});
1675 env(pay(gw_, alice_, btc(500000)));
1676 env(pay(gw_, bob_, btc(500000)));
1677 env(pay(gw_, carol_, btc(500000)));
1678 env(pay(gw_, alice_, eth(500000)));
1679 env(pay(gw_, bob_, eth(500000)));
1680 env(pay(gw_, carol_, eth(500000)));
1681 env.close();
1682 AMM const ammBob(env, bob_, btc(100'000), eth(150'000));
1683
1684 env(pay(alice_, carol_, eth(50'000)), Path(~eth), Sendmax(btc(50'000)));
1685
1686 env.require(Balance(alice_, btc(450'000)));
1687 env.require(Balance(bob_, btc(400'000)));
1688 env.require(Balance(bob_, eth(350'000)));
1689 env.require(Balance(carol_, eth(550'000)));
1690 BEAST_EXPECT(ammBob.expectBalances(btc(150'000), eth(100'000), ammBob.tokens()));
1691 };
1693 }
1694
1695 {
1696 // simple MPT/XRP XRP/MPT offer
1697 Env env(*this, features);
1698 env.fund(XRP(10'000), gw_, alice_, bob_, carol_);
1699
1700 MPTTester const btc(
1701 {.env = env,
1702 .issuer = gw_,
1703 .holders = {alice_, bob_, carol_},
1704 .pay = 100'000,
1705 .flags = kMptDexFlags});
1706
1707 MPTTester const eth(
1708 {.env = env,
1709 .issuer = gw_,
1710 .holders = {alice_, bob_, carol_},
1711 .pay = 150'000,
1712 .flags = kMptDexFlags});
1713
1714 AMM const ammBobBtcXrp(env, bob_, btc(100'000), XRP(150));
1715 AMM const ammBobXrpEth(env, bob_, XRP(100), eth(150'000));
1716
1717 env(pay(alice_, carol_, eth(50'000)), Path(~XRP, ~MPT(eth)), Sendmax(btc(50'000)));
1718
1719 env.require(Balance(alice_, btc(50'000)));
1720 env.require(Balance(bob_, btc(0)));
1721 env.require(Balance(bob_, eth(0)));
1722 env.require(Balance(carol_, eth(200'000)));
1723 BEAST_EXPECT(
1724 ammBobBtcXrp.expectBalances(btc(150'000), XRP(100), ammBobBtcXrp.tokens()));
1725 BEAST_EXPECT(
1726 ammBobXrpEth.expectBalances(XRP(150), eth(100'000), ammBobXrpEth.tokens()));
1727 }
1728 {
1729 // simple XRP -> MPT through offer and sendmax
1730 Env env(*this, features);
1731 XRPAmount const baseFee{env.current()->fees().base};
1732 env.fund(XRP(10'000), gw_, alice_, bob_, carol_);
1733
1734 MPTTester const eth(
1735 {.env = env,
1736 .issuer = gw_,
1737 .holders = {alice_, bob_, carol_},
1738 .pay = 150'000,
1739 .flags = kMptDexFlags});
1740
1741 AMM const ammBob(env, bob_, XRP(100), eth(150'000));
1742
1743 env(pay(alice_, carol_, eth(50'000)), Path(~MPT(eth)), Sendmax(XRP(50)));
1744 BEAST_EXPECT(expectLedgerEntryRoot(env, alice_, XRP(10'000) - XRP(50) - 2 * baseFee));
1745 BEAST_EXPECT(expectLedgerEntryRoot(
1746 env, bob_, XRP(10'000) - XRP(100) - ammCrtFee(env) - baseFee));
1747 env.require(Balance(bob_, eth(0)));
1748 env.require(Balance(carol_, eth(200'000)));
1749 BEAST_EXPECT(ammBob.expectBalances(XRP(150), eth(100'000), ammBob.tokens()));
1750 }
1751 {
1752 // simple MPT -> XRP through offer and sendmax
1753 Env env(*this, features);
1754 XRPAmount const baseFee{env.current()->fees().base};
1755 env.fund(XRP(10'000), gw_, alice_, bob_, carol_);
1756
1757 MPTTester const eth(
1758 {.env = env,
1759 .issuer = gw_,
1760 .holders = {alice_, bob_, carol_},
1761 .pay = 100'000,
1762 .flags = kMptDexFlags});
1763
1764 AMM const ammBob(env, bob_, eth(100'000), XRP(150));
1765
1766 env(pay(alice_, carol_, XRP(50)), Path(~XRP), Sendmax(eth(50'000)));
1767
1768 env.require(Balance(alice_, eth(50'000)));
1769 BEAST_EXPECT(expectLedgerEntryRoot(
1770 env, bob_, XRP(10'000) - XRP(150) - ammCrtFee(env) - baseFee));
1771 env.require(Balance(bob_, eth(0)));
1772 BEAST_EXPECT(expectLedgerEntryRoot(env, carol_, XRP(10'000 + 50) - baseFee));
1773 BEAST_EXPECT(ammBob.expectBalances(eth(150'000), XRP(100), ammBob.tokens()));
1774 }
1775
1776 // test unfunded offers are removed when payment succeeds
1777 {
1778 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
1779 Env env(*this, features);
1780 env.fund(XRP(10'000), alice_, bob_, carol_, gw_);
1781 env.close();
1782 auto const btc = issue1(
1783 {.env = env,
1784 .token = "BTC",
1785 .issuer = gw_,
1786 .holders = {alice_, bob_, carol_},
1787 .limit = 1000'000000});
1788 auto const eth = issue2(
1789 {.env = env,
1790 .token = "ETH",
1791 .issuer = gw_,
1792 .holders = {alice_, bob_, carol_},
1793 .limit = 1000'000000});
1794 auto const gbp = issue3(
1795 {.env = env,
1796 .token = "GBP",
1797 .issuer = gw_,
1798 .holders = {alice_, bob_, carol_},
1799 .limit = 1000'000000});
1800
1801 env(pay(gw_, alice_, btc(60'000)));
1802 env(pay(gw_, bob_, eth(200'000)));
1803 env(pay(gw_, bob_, gbp(150'000)));
1804 env(offer(bob_, btc(50'000), eth(50'000)));
1805 env(offer(bob_, btc(40'000), gbp(50'000)));
1806 env.close();
1807 AMM const ammBob(env, bob_, gbp(100'000), eth(150'000));
1808
1809 // unfund offer
1810 env(pay(bob_, gw_, gbp(50'000)));
1811 BEAST_EXPECT(isOffer(env, bob_, btc(50'000), eth(50'000)));
1812 BEAST_EXPECT(isOffer(env, bob_, btc(40'000), gbp(50'000)));
1813 env(pay(alice_, carol_, eth(50'000)),
1814 Path(~eth),
1815 Path(~gbp, ~eth),
1816 Sendmax(btc(60'000)));
1817 env.require(Balance(alice_, btc(10'000)));
1818 env.require(Balance(bob_, btc(50'000)));
1819 env.require(Balance(bob_, eth(0)));
1820 env.require(Balance(bob_, gbp(0)));
1821 env.require(Balance(carol_, eth(50'000)));
1822 // used in the payment
1823 BEAST_EXPECT(!isOffer(env, bob_, btc(50'000), eth(50'000)));
1824 // found unfunded
1825 BEAST_EXPECT(!isOffer(env, bob_, btc(40'000), gbp(50'000)));
1826 // unchanged
1827 BEAST_EXPECT(ammBob.expectBalances(gbp(100'000), eth(150'000), ammBob.tokens()));
1828 };
1830 }
1831
1832 {
1833 // test unfunded offers are removed when the payment fails.
1834 // bob makes two offers: a funded 50'000'000 ETH for 50'000'000 BTC
1835 // and an unfunded 50'000'000 GBP for 60'000'000 BTC. alice pays
1836 // carol 61'000'000 ETH with 61'000'000 BTC. alice only has
1837 // 60'000'000 BTC, so the payment will fail. The payment uses two
1838 // paths: one through bob's funded offer and one through his
1839 // unfunded offer. When the payment fails `flow` should return the
1840 // unfunded offer. This test is intentionally similar to the one
1841 // that removes unfunded offers when the payment succeeds.
1842 Env env(*this, features);
1843
1844 env.fund(XRP(10'000), bob_, carol_, gw_);
1845 env.close();
1846 // Sets rippling on, this is different from
1847 // the original test
1848 fund(env, gw_, {alice_}, XRP(10'000), {}, Fund::Acct);
1849
1850 MPTTester btc(
1851 {.env = env,
1852 .issuer = gw_,
1853 .holders = {alice_, bob_, carol_},
1854 .flags = kMptDexFlags});
1855
1856 MPTTester eth(
1857 {.env = env,
1858 .issuer = gw_,
1859 .holders = {alice_, bob_, carol_},
1860 .flags = kMptDexFlags});
1861
1862 MPTTester gbp(
1863 {.env = env,
1864 .issuer = gw_,
1865 .holders = {alice_, bob_, carol_},
1866 .flags = kMptDexFlags});
1867
1868 env(pay(gw_, alice_, btc(60'000'000)));
1869 env(pay(gw_, bob_, btc(100'000'000)));
1870 env(pay(gw_, bob_, eth(100'000'000)));
1871 env(pay(gw_, bob_, gbp(50'000'000)));
1872 env(pay(gw_, carol_, gbp(1'000'000)));
1873 env.close();
1874
1875 // This is multiplath, which generates limited # of offers
1876 AMM const ammBobBtcEth(env, bob_, btc(50'000'000), eth(50'000'000));
1877 env(offer(bob_, btc(60'000'000), gbp(50'000'000)));
1878 env(offer(carol_, btc(1'000'000'000), gbp(1'000'000)));
1879 env(offer(bob_, gbp(50'000'000), eth(50'000'000)));
1880
1881 // unfund offer
1882 env(pay(bob_, gw_, gbp(50'000'000)));
1883 BEAST_EXPECT(ammBobBtcEth.expectBalances(
1884 btc(50'000'000), eth(50'000'000), ammBobBtcEth.tokens()));
1885 BEAST_EXPECT(isOffer(env, bob_, btc(60'000'000), gbp(50'000'000)));
1886 BEAST_EXPECT(isOffer(env, carol_, btc(1'000'000'000), gbp(1'000'000)));
1887 BEAST_EXPECT(isOffer(env, bob_, gbp(50'000'000), eth(50'000'000)));
1888
1889 auto flowJournal = env.app().getLogs().journal("Flow");
1890 auto const flowResult = [&] {
1891 STAmount const deliver(eth(51'000'000));
1892 STAmount smax(btc(61'000'000));
1893 PaymentSandbox sb(env.current().get(), TapNone);
1894 STPathSet paths;
1895 auto ipe = [](MPTTester const& iss) {
1896 return STPathElement(
1898 xrpAccount(),
1899 PathAsset{iss.issuanceID()},
1900 iss.issuer());
1901 };
1902 {
1903 // BTC -> ETH
1904 STPath const p1({ipe(eth)});
1905 paths.pushBack(p1);
1906 // BTC -> GBP -> ETH
1907 STPath const p2({ipe(gbp), ipe(eth)});
1908 paths.pushBack(p2);
1909 }
1910
1911 return flow(
1912 sb,
1913 deliver,
1914 alice_,
1915 carol_,
1916 paths,
1917 false,
1918 false,
1919 true,
1921 std::nullopt,
1922 smax,
1923 std::nullopt,
1924 flowJournal);
1925 }();
1926
1927 BEAST_EXPECT(flowResult.removableOffers.size() == 1);
1928 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
1929 if (flowResult.removableOffers.empty())
1930 return false;
1931 Sandbox sb(&view, TapNone);
1932 for (auto const& o : flowResult.removableOffers)
1933 {
1934 if (auto ok = sb.peek(keylet::offer(o)))
1935 {
1936 offerDelete(sb, ok, flowJournal);
1937 }
1938 }
1939 sb.apply(view);
1940 return true;
1941 });
1942
1943 // used in payment, but since payment failed should be untouched
1944 BEAST_EXPECT(ammBobBtcEth.expectBalances(
1945 btc(50'000'000), eth(50'000'000), ammBobBtcEth.tokens()));
1946 BEAST_EXPECT(isOffer(env, carol_, btc(1'000'000'000), gbp(1'000'000)));
1947 // found unfunded
1948 BEAST_EXPECT(!isOffer(env, bob_, btc(60'000'000), gbp(50'000'000)));
1949 }
1950 {
1951 // Do not produce more in the forward pass than the reverse pass
1952 // This test uses a path that whose reverse pass will compute a
1953 // 500 ETH input required for a 1'000 BTC output. It sets a sendmax
1954 // of 400 ETH, so the payment engine will need to do a forward
1955 // pass. Without limits, the 400 ETH would produce 1'000 BTC in
1956 // the forward pass. This test checks that the payment produces
1957 // 1'000 BTC, as expected.
1958
1959 auto test = [&](auto&& issue1, auto&& issue2) {
1960 Env env(*this);
1961 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
1962 env.close();
1963 auto const eth = issue1(
1964 {.env = env,
1965 .token = "ETH",
1966 .issuer = gw_,
1967 .holders = {alice_, bob_, carol_},
1968 .limit = 10'000'000});
1969 auto const btc = issue2(
1970 {.env = env,
1971 .token = "BTC",
1972 .issuer = gw_,
1973 .holders = {alice_, bob_, carol_},
1974 .limit = 10'000'000});
1975
1976 env(pay(gw_, alice_, eth(1'000'000)));
1977 env(pay(gw_, bob_, btc(1'000'000)));
1978 env(pay(gw_, bob_, eth(1'000'000)));
1979 env.close();
1980
1981 AMM const ammBob(env, bob_, eth(8'000), XRPAmount{21});
1982 env(offer(bob_, drops(1), btc(1'000'000)), Txflags(tfPassive));
1983
1984 env(pay(alice_, carol_, btc(1'000)),
1985 Path(~XRP, ~btc),
1986 Sendmax(eth(400)),
1987 Txflags(tfNoRippleDirect | tfPartialPayment));
1988
1989 env.require(Balance(carol_, btc(1'000)));
1990 BEAST_EXPECT(ammBob.expectBalances(eth(8400), XRPAmount{20}, ammBob.tokens()));
1991 };
1993 }
1994 }
1995
1996 void
1998 {
1999 testcase("No Owner Fee");
2000 using namespace jtx;
2001
2002 {
2003 // payment via AMM
2004 Env env(*this, features);
2005 env.fund(XRP(1'000), gw_, alice_, bob_, carol_);
2006
2007 MPTTester const gbp(
2008 {.env = env,
2009 .issuer = gw_,
2010 .holders = {alice_, bob_, carol_},
2011 .transferFee = 25'000,
2012 .pay = 1'000'000'000'000'000,
2013 .flags = kMptDexFlags});
2014
2015 MPTTester const btc(
2016 {.env = env,
2017 .issuer = gw_,
2018 .holders = {alice_, bob_, carol_},
2019 .transferFee = 25'000,
2020 .pay = 1'000'000'000'000'000,
2021 .flags = kMptDexFlags});
2022
2023 AMM const amm(env, bob_, gbp(1'000'000'000'000'000), btc(1'000'000'000'000'000));
2024
2025 env(pay(alice_, carol_, btc(100'000'000'000'000)),
2026 Path(~MPT(btc)),
2027 Sendmax(gbp(150'000'000'000'000)),
2028 Txflags(tfNoRippleDirect | tfPartialPayment));
2029 env.close();
2030
2031 // alice buys 107.1428e12BTC with 120e12GBP and pays 25% tr fee on
2032 // 120e12GBP 1,000e12 - 120e12*1.25 = 850e12GBP
2033 env.require(Balance(alice_, gbp(850'000'000'000'000)));
2034
2035 BEAST_EXPECT(amm.expectBalances(
2036 gbp(1'120'000'000'000'000), btc(892'857'142'857'143), amm.tokens()));
2037
2038 // 25% of 85.7142e12BTC is paid in tr fee
2039 // 85.7142e12*1.25 = 107.1428e12BTC
2040 env.require(Balance(carol_, btc(1'085'714'285'714'285)));
2041 }
2042 {
2043 // Payment via offer and AMM
2044 Env env(*this, features);
2045 Account const ed("ed");
2046
2047 env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed);
2048
2049 MPTTester const gbp(
2050 {.env = env,
2051 .issuer = gw_,
2052 .holders = {alice_, bob_, carol_, ed},
2053 .transferFee = 25'000,
2054 .pay = 1'000'000'000'000'000,
2055 .flags = kMptDexFlags});
2056
2057 MPTTester const btc(
2058 {.env = env,
2059 .issuer = gw_,
2060 .holders = {alice_, bob_, carol_, ed},
2061 .transferFee = 25'000,
2062 .pay = 1'000'000'000'000'000,
2063 .flags = kMptDexFlags});
2064
2065 MPTTester const eth(
2066 {.env = env,
2067 .issuer = gw_,
2068 .holders = {alice_, bob_, carol_, ed},
2069 .transferFee = 25'000,
2070 .pay = 1'000'000'000'000'000,
2071 .flags = kMptDexFlags});
2072
2073 env(offer(ed, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000)),
2074 Txflags(tfPassive));
2075 env.close();
2076
2077 AMM const amm(env, bob_, eth(1'000'000'000'000'000), btc(1'000'000'000'000'000));
2078
2079 env(pay(alice_, carol_, btc(100'000'000'000'000)),
2080 Path(~MPT(eth), ~MPT(btc)),
2081 Sendmax(gbp(150'000'000'000'000)),
2082 Txflags(tfNoRippleDirect | tfPartialPayment));
2083 env.close();
2084
2085 // alice buys 120e12ETH with 120e12GBP via the offer
2086 // and pays 25% tr fee on 120e12GBP
2087 // 1,000e12 - 120e12*1.25 = 850e12GBP
2088 env.require(Balance(alice_, gbp(850'000'000'000'000)));
2089 // consumed offer is 120e12GBP/120e12ETH
2090 // ed doesn't pay tr fee
2091 env.require(Balance(ed, eth(880'000'000'000'000)));
2092 env.require(Balance(ed, gbp(1'120'000'000'000'000)));
2093 BEAST_EXPECT(expectOffers(
2094 env, ed, 1, {Amounts{gbp(880'000'000'000'000), eth(880'000'000'000'000)}}));
2095 // 25% on 96e12ETH is paid in tr fee 96e12*1.25 = 120e12ETH
2096 // 96e12ETH is swapped in for 87.5912e12BTC
2097 BEAST_EXPECT(amm.expectBalances(
2098 eth(1'096'000'000'000'000), btc(912'408'759'124'088), amm.tokens()));
2099 // 25% on 70.0729e12BTC is paid in tr fee 70.0729e12*1.25
2100 // = 87.5912e12BTC
2101 env.require(Balance(carol_, btc(1'070'072'992'700'729)));
2102 }
2103 {
2104 // Payment via AMM, AMM
2105 Env env(*this, features);
2106 Account const ed("ed");
2107
2108 env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed);
2109
2110 MPTTester const gbp(
2111 {.env = env,
2112 .issuer = gw_,
2113 .holders = {alice_, bob_, carol_, ed},
2114 .transferFee = 25'000,
2115 .pay = 1'000'000'000'000'000,
2116 .flags = kMptDexFlags});
2117
2118 MPTTester const btc(
2119 {.env = env,
2120 .issuer = gw_,
2121 .holders = {alice_, bob_, carol_, ed},
2122 .transferFee = 25'000,
2123 .pay = 1'000'000'000'000'000,
2124 .flags = kMptDexFlags});
2125
2126 MPTTester const eth(
2127 {.env = env,
2128 .issuer = gw_,
2129 .holders = {alice_, bob_, carol_, ed},
2130 .transferFee = 25'000,
2131 .pay = 1'000'000'000'000'000,
2132 .flags = kMptDexFlags});
2133
2134 AMM const amm1(env, bob_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000));
2135 AMM const amm2(env, ed, eth(1'000'000'000'000'000), btc(1'000'000'000'000'000));
2136
2137 env(pay(alice_, carol_, btc(100'000'000'000'000)),
2138 Path(~MPT(eth), ~MPT(btc)),
2139 Sendmax(gbp(150'000'000'000'000)),
2140 Txflags(tfNoRippleDirect | tfPartialPayment));
2141 env.close();
2142
2143 env.require(Balance(alice_, gbp(850'000'000'000'000)));
2144
2145 // alice buys 107.1428e12ETH with 120e12GBP and pays 25% tr fee on
2146 // 120e12GBP 1,000e12 - 120e12*1.25 = 850e12GBP 120e12GBP is swapped
2147 // in for 107.1428e12ETH
2148 BEAST_EXPECT(amm1.expectBalances(
2149 gbp(1'120'000'000'000'000), eth(892'857'142'857'143), amm1.tokens()));
2150 // 25% on 85.7142e12ETH is paid in tr fee 85.7142e12*1.25 =
2151 // 107.1428e12ETH 85.7142e12ETH is swapped in for 78.9473e12BTC
2152 BEAST_EXPECT(amm2.expectBalances(
2153 eth(1'085'714'285'714'285), btc(921'052'631'578'948), amm2.tokens()));
2154
2155 // 25% on 63.1578e12BTC is paid in tr fee 63.1578e12*1.25
2156 // = 78.9473e12BTC
2157 env.require(Balance(carol_, btc(1'063'157'894'736'841)));
2158 }
2159 {
2160 // AMM offer crossing
2161 Env env(*this, features);
2162
2163 env.fund(XRP(1'000), gw_, alice_, bob_);
2164
2165 MPTTester const btc(
2166 {.env = env,
2167 .issuer = gw_,
2168 .holders = {alice_, bob_},
2169 .transferFee = 25'000,
2170 .pay = 1'100'000,
2171 .flags = kMptDexFlags});
2172
2173 MPTTester const eth(
2174 {.env = env,
2175 .issuer = gw_,
2176 .holders = {alice_, bob_},
2177 .transferFee = 25'000,
2178 .pay = 1'100'000,
2179 .flags = kMptDexFlags});
2180
2181 AMM const amm(env, bob_, btc(1'000'000), eth(1'100'000));
2182 env(offer(alice_, eth(100'000), btc(100'000)));
2183 env.close();
2184
2185 // 100e3BTC is swapped in for 100e3ETH
2186 BEAST_EXPECT(amm.expectBalances(btc(1'100'000), eth(1'000'000), amm.tokens()));
2187 // alice pays 25% tr fee on 100e3BTC 1100e3-100e3*1.25 = 975e3BTC
2188 env.require(Balance(alice_, btc(975'000)));
2189 env.require(Balance(alice_, eth(1'200'000)));
2190 BEAST_EXPECT(expectOffers(env, alice_, 0));
2191 }
2192 {
2193 // Payment via AMM with limit quality
2194 Env env(*this, features);
2195
2196 env.fund(XRP(1'000), gw_, alice_, bob_, carol_);
2197
2198 MPTTester const btc(
2199 {.env = env,
2200 .issuer = gw_,
2201 .holders = {alice_, bob_, carol_},
2202 .transferFee = 25'000,
2203 .pay = 1'000'000'000'000'000,
2204 .flags = kMptDexFlags});
2205
2206 MPTTester const gbp(
2207 {.env = env,
2208 .issuer = gw_,
2209 .holders = {alice_, bob_, carol_},
2210 .transferFee = 25'000,
2211 .pay = 1'000'000'000'000'000,
2212 .flags = kMptDexFlags});
2213
2214 AMM const amm(env, bob_, gbp(1'000'000'000'000'000), btc(1'000'000'000'000'000));
2215
2216 // requested quality limit is 100e12BTC/178.58e12GBP = 0.55997
2217 // trade quality is 100e12BTC/178.5714 = 0.55999e12
2218 env(pay(alice_, carol_, btc(100'000'000'000'000)),
2219 Path(~MPT(btc)),
2220 Sendmax(gbp(178'580'000'000'000)),
2221 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
2222 env.close();
2223
2224 // alice buys 125e12BTC with 142.8571e12GBP and pays 25% tr fee
2225 // on 142.8571e12GBP
2226 // 1,000e12 - 142.8571e12*1.25 = 821.4285e12GBP
2227 env.require(Balance(alice_, gbp(821'428'571'428'571)));
2228 // 142.8571e12GBP is swapped in for 125e12BTC
2229 BEAST_EXPECT(amm.expectBalances(
2230 gbp(1'142'857'142'857'143), btc(875'000'000'000'000), amm.tokens()));
2231 // 25% on 100e12BTC is paid in tr fee
2232 // 100e12*1.25 = 125e12BTC
2233 env.require(Balance(carol_, btc(1'100'000'000'000'000)));
2234 }
2235 {
2236 // Payment via AMM with limit quality, deliver less
2237 // than requested
2238 Env env(*this, features);
2239
2240 env.fund(XRP(1'000), gw_, alice_, bob_, carol_);
2241
2242 MPTTester const btc(
2243 {.env = env,
2244 .issuer = gw_,
2245 .holders = {alice_, bob_, carol_},
2246 .transferFee = 25'000,
2247 .pay = 1'200'000'000'000'000,
2248 .flags = kMptDexFlags});
2249
2250 MPTTester const gbp(
2251 {.env = env,
2252 .issuer = gw_,
2253 .holders = {alice_, bob_, carol_},
2254 .transferFee = 25'000,
2255 .pay = 1'200'000'000'000'000,
2256 .flags = kMptDexFlags});
2257
2258 AMM const amm(env, bob_, gbp(1'000'000'000'000'000), btc(1'200'000'000'000'000));
2259
2260 // requested quality limit is 90e12BTC/120e12GBP = 0.75
2261 // trade quality is 22.5e12BTC/30e12GBP = 0.75
2262 env(pay(alice_, carol_, btc(90'000'000'000'000)),
2263 Path(~MPT(btc)),
2264 Sendmax(gbp(120'000'000'000'000)),
2265 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
2266 env.close();
2267
2268 // alice buys 28.125e12BTC with 24e12GBP and pays 25% tr fee
2269 // on 24e12GBP
2270 // 1,200e12 - 24e12*1.25 =~ 1,170e12GBP
2271 env.require(Balance(alice_, gbp(1'170'000'000'000'000)));
2272 // 24e12GBP is swapped in for 28.125e12BTC
2273 BEAST_EXPECT(amm.expectBalances(
2274 gbp(1'024'000'000'000'000), btc(1'171'875'000'000'000), amm.tokens()));
2275
2276 // 25% on 22.5e12BTC is paid in tr fee
2277 // 22.5*1.25 = 28.125e12BTC
2278 env.require(Balance(carol_, btc(1'222'500'000'000'000)));
2279 }
2280 {
2281 // Payment via offer and AMM with limit quality, deliver less
2282 // than requested
2283 Env env(*this, features);
2284 Account const ed("ed");
2285
2286 env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed);
2287
2288 MPTTester const btc(
2289 {.env = env,
2290 .issuer = gw_,
2291 .holders = {alice_, bob_, carol_, ed},
2292 .transferFee = 25'000,
2293 .pay = 1'400'000'000'000'000,
2294 .flags = kMptDexFlags});
2295
2296 MPTTester const gbp(
2297 {.env = env,
2298 .issuer = gw_,
2299 .holders = {alice_, bob_, carol_, ed},
2300 .transferFee = 25'000,
2301 .pay = 1'400'000'000'000'000,
2302 .flags = kMptDexFlags});
2303
2304 MPTTester const eth(
2305 {.env = env,
2306 .issuer = gw_,
2307 .holders = {alice_, bob_, carol_, ed},
2308 .transferFee = 25'000,
2309 .pay = 1'400'000'000'000'000,
2310 .flags = kMptDexFlags});
2311
2312 env(offer(ed, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000)),
2313 Txflags(tfPassive));
2314 env.close();
2315
2316 AMM const amm(env, bob_, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000));
2317
2318 // requested quality limit is 95e12BTC/140e12GBP = 0.6785
2319 // trade quality is 59.7321e12BTC/88.0262e12GBP = 0.6785
2320 env(pay(alice_, carol_, btc(95'000'000'000'000)),
2321 Path(~MPT(eth), ~MPT(btc)),
2322 Sendmax(gbp(140'000'000'000'000)),
2323 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
2324 env.close();
2325
2326 // alice buys 70.4210e12ETH with 70.4210e12GBP via the offer
2327 // and pays 25% tr fee on 70.4210e12GBP
2328 // 1,400e12 - 70.4210e12*1.25 = 1400e12 - 88.0262e12 =
2329 // 1311.9736e12GBP
2330 env.require(Balance(alice_, gbp(1'311'973'684'210'525)));
2331 // ed doesn't pay tr fee, the balances reflect consumed offer
2332 // 70.4210e12GBP/70.4210e12ETH
2333 env.require(Balance(ed, eth(1'329'578'947'368'420)));
2334 env.require(Balance(ed, gbp(1'470'421'052'631'580)));
2335 BEAST_EXPECT(expectOffers(
2336 env, ed, 1, {Amounts{gbp(929'578'947'368'420), eth(929'578'947'368'420)}}));
2337 // 25% on 56.3368e12ETH is paid in tr fee 56.3368e12*1.25
2338 // = 70.4210e12ETH 56.3368e12ETH is swapped in for 74.6651e12BTC
2339 BEAST_EXPECT(amm.expectBalances(
2340 eth(1'056'336'842'105'264), btc(1'325'334'821'428'571), amm.tokens()));
2341
2342 // 25% on 59.7321e12BTC is paid in tr fee 59.7321e12*1.25
2343 // = 74.6651e12BTC
2344 env.require(Balance(carol_, btc(1'459'732'142'857'143)));
2345 }
2346 {
2347 // Payment via AMM and offer with limit quality, deliver less
2348 // than requested
2349 Env env(*this, features);
2350 Account const ed("ed");
2351
2352 env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed);
2353
2354 MPTTester const btc(
2355 {.env = env,
2356 .issuer = gw_,
2357 .holders = {alice_, bob_, carol_, ed},
2358 .transferFee = 25'000,
2359 .pay = 1'400'000'000'000'000,
2360 .flags = kMptDexFlags});
2361
2362 MPTTester const gbp(
2363 {.env = env,
2364 .issuer = gw_,
2365 .holders = {alice_, bob_, carol_, ed},
2366 .transferFee = 25'000,
2367 .pay = 1'400'000'000'000'000,
2368 .flags = kMptDexFlags});
2369
2370 MPTTester const eth(
2371 {.env = env,
2372 .issuer = gw_,
2373 .holders = {alice_, bob_, carol_, ed},
2374 .transferFee = 25'000,
2375 .pay = 1'400'000'000'000'000,
2376 .flags = kMptDexFlags});
2377
2378 AMM const amm(env, bob_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000));
2379
2380 env(offer(ed, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000)),
2381 Txflags(tfPassive));
2382 env.close();
2383
2384 // requested quality limit is 95e12BTC/140e12GBP = 0.6785
2385 // trade quality is 47.7857e12BTC/70.4210e12GBP = 0.6785
2386 env(pay(alice_, carol_, btc(95'000'000'000'000)),
2387 Path(~MPT(eth), ~MPT(btc)),
2388 Sendmax(gbp(140'000'000'000'000)),
2389 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
2390 env.close();
2391
2392 // alice buys 53.3322e12ETH with 56.3368e12GBP via the amm
2393 // and pays 25% tr fee on 56.3368e12GBP
2394 // 1,400e12 - 56.3368e12*1.25 = 1400e12 - 70.4210e12 =
2395 // 1329.5789e12GBP
2396 env.require(Balance(alice_, gbp(1'329'578'947'368'420)));
2401 // 56.3368e12GBP is swapped in for 53.3322e12ETH
2402 BEAST_EXPECT(amm.expectBalances(
2403 gbp(1'056'336'842'105'264), eth(946'667'729'591'836), amm.tokens()));
2404
2405 // 25% on 42.6658e12ETH is paid in tr fee 42.6658e12*1.25
2406 // = 53.3322e12ETH 42.6658e12ETH/59.7321e12BTC
2407 env.require(Balance(ed, btc(1'340'267'857'142'857)));
2408 env.require(Balance(ed, eth(1'442'665'816'326'531)));
2409 BEAST_EXPECT(expectOffers(
2410 env, ed, 1, {Amounts{eth(957'334'183'673'469), btc(1'340'267'857'142'857)}}));
2411 // 25% on 47.7857e12BTC is paid in tr fee 47.7857e12*1.25
2412 // = 59.7321e12BTC
2413 env.require(Balance(carol_, btc(1'447'785714285714)));
2414 }
2415 {
2416 // Payment via AMM, AMM with limit quality, deliver less
2417 // than requested
2418 Env env(*this, features);
2419 Account const ed("ed");
2420
2421 env.fund(XRP(1'000), gw_, alice_, bob_, carol_, ed);
2422
2423 MPTTester const btc(
2424 {.env = env,
2425 .issuer = gw_,
2426 .holders = {alice_, bob_, carol_, ed},
2427 .transferFee = 25'000,
2428 .pay = 1'400'000'000'000'000,
2429 .flags = kMptDexFlags});
2430
2431 MPTTester const gbp(
2432 {.env = env,
2433 .issuer = gw_,
2434 .holders = {alice_, bob_, carol_, ed},
2435 .transferFee = 25'000,
2436 .pay = 1'400'000'000'000'000,
2437 .flags = kMptDexFlags});
2438
2439 MPTTester const eth(
2440 {.env = env,
2441 .issuer = gw_,
2442 .holders = {alice_, bob_, carol_, ed},
2443 .transferFee = 25'000,
2444 .pay = 1'400'000'000'000'000,
2445 .flags = kMptDexFlags});
2446
2447 AMM const amm1(env, bob_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000));
2448 AMM const amm2(env, ed, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000));
2449
2450 // requested quality limit is 90e12BTC/145e12GBP = 0.6206
2451 // trade quality is 66.7432e12BTC/107.5308e12GBP = 0.6206
2452 env(pay(alice_, carol_, btc(90'000'000'000'000)),
2453 Path(~MPT(eth), ~MPT(btc)),
2454 Sendmax(gbp(145'000'000'000'000)),
2455 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
2456 env.close();
2457
2458 // alice buys 53.3322e12ETH with 107.5308e12GBP
2459 // 25% on 86.0246e12GBP is paid in tr fee
2460 // 1,400e12 - 86.0246e12*1.25 = 1400e12 - 107.5308e12 =
2461 // 1229.4691e12GBP
2462 env.require(Balance(alice_, gbp(1'292'469'135'802'465)));
2463 // 86.0246e12GBP is swapped in for 79.2106e12ETH
2464 BEAST_EXPECT(amm1.expectBalances(
2465 gbp(1'086'024'691'358'028), eth(920'789'377'955'618), amm1.tokens()));
2466 // 25% on 63.3684e12ETH is paid in tr fee 63.3684e12*1.25
2467 // = 79.2106e12ETH 63.3684e12ETH is swapped in for 83.4291e12BTC
2468 BEAST_EXPECT(amm2.expectBalances(
2469 eth(1'063'368'497'635'505), btc(1'316'570'881'226'053), amm2.tokens()));
2470
2471 // 25% on 66.7432e12BTC is paid in tr fee 66.7432e12*1.25
2472 // = 83.4291e12BTC
2473 env.require(Balance(carol_, btc(1'466'743'295'019'157)));
2474 }
2475 {
2476 // Payment by the issuer via AMM, AMM with limit quality,
2477 // deliver less than requested
2478 Env env(*this, features);
2479
2480 env.fund(XRP(1'000), gw_, alice_, bob_, carol_);
2481
2482 MPTTester const btc(
2483 {.env = env,
2484 .issuer = gw_,
2485 .holders = {alice_, bob_, carol_},
2486 .transferFee = 25'000,
2487 .pay = 1'400'000'000'000'000,
2488 .flags = kMptDexFlags});
2489
2490 MPTTester const gbp(
2491 {.env = env,
2492 .issuer = gw_,
2493 .holders = {alice_, bob_, carol_},
2494 .transferFee = 25'000,
2495 .pay = 1'400'000'000'000'000,
2496 .flags = kMptDexFlags});
2497
2498 MPTTester const eth(
2499 {.env = env,
2500 .issuer = gw_,
2501 .holders = {alice_, bob_, carol_},
2502 .transferFee = 25'000,
2503 .pay = 1'400'000'000'000'000,
2504 .flags = kMptDexFlags});
2505
2506 AMM const amm1(env, alice_, gbp(1'000'000'000'000'000), eth(1'000'000'000'000'000));
2507 AMM const amm2(env, bob_, eth(1'000'000'000'000'000), btc(1'400'000'000'000'000));
2508
2509 // requested quality limit is 90e12BTC/120e12GBP = 0.75
2510 // trade quality is 81.1111e12BTC/108.1481e12GBP = 0.75
2511 env(pay(gw_, carol_, btc(90'000'000'000'000)),
2512 Path(~MPT(eth), ~MPT(btc)),
2513 Sendmax(gbp(120'000'000'000'000)),
2514 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
2515 env.close();
2516
2517 // 108.1481e12GBP is swapped in for 97.5935e12ETH
2518 BEAST_EXPECT(amm1.expectBalances(
2519 gbp(1'108'148'148'148'150), eth(902'406'417'112'298), amm1.tokens()));
2520 // 25% on 78.0748e12ETH is paid in tr fee 78.0748e12*1.25
2521 // = 97.5935e12ETH 78.0748e12ETH is swapped in for 101.3888e12BTC
2522 BEAST_EXPECT(amm2.expectBalances(
2523 eth(1'078'074'866'310'161), btc(1'298'611'111'111'111), amm2.tokens()));
2524
2525 // 25% on 81.1111e12BTC is paid in tr fee 81.1111e12*1.25 =
2526 // 101.3888e12BTC
2527 env.require(Balance(carol_, btc(1'481'111'111'111'111)));
2528 }
2529 }
2530
2531 void
2533 {
2534 // Single path with amm, offer, and limit quality. The quality limit
2535 // is such that the first offer should be taken but the second
2536 // should not. The total amount delivered should be the sum of the
2537 // two offers and sendMax should be more than the first offer.
2538 testcase("limitQuality");
2539 using namespace jtx;
2540
2541 {
2542 Env env(*this);
2543 env.fund(XRP(10'000), gw_, alice_, bob_, carol_);
2544
2545 MPTTester const eth(
2546 {.env = env,
2547 .issuer = gw_,
2548 .holders = {alice_, bob_, carol_},
2549 .pay = 2'000'000,
2550 .flags = kMptDexFlags});
2551
2552 AMM const ammBob(env, bob_, XRP(1'000), eth(1'050'000));
2553 env(offer(bob_, XRP(100), eth(50'000)));
2554
2555 env(pay(alice_, carol_, eth(100'000)),
2556 Path(~MPT(eth)),
2557 Sendmax(XRP(100)),
2558 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
2559
2560 BEAST_EXPECT(ammBob.expectBalances(XRP(1'050), eth(1'000'000), ammBob.tokens()));
2561 env.require(Balance(carol_, eth(2'050'000)));
2562 BEAST_EXPECT(expectOffers(env, bob_, 1, {{{XRP(100), eth(50'000)}}}));
2563 }
2564 }
2565
2566 void
2568 {
2569 testcase("Circular XRP");
2570
2571 using namespace jtx;
2572
2573 // Payment path starting with XRP
2574 {
2575 auto test = [&](auto&& issue1, auto&& issue2) {
2576 Env env(*this);
2577 env.fund(XRP(30'000), alice_, bob_, gw_);
2578 env.close();
2579 auto const eth = issue1(
2580 {.env = env,
2581 .token = "ETH",
2582 .issuer = gw_,
2583 .holders = {alice_, bob_},
2584 .limit = 2000'000});
2585 auto const btc = issue2(
2586 {.env = env,
2587 .token = "BTC",
2588 .issuer = gw_,
2589 .holders = {alice_, bob_},
2590 .limit = 2000'000});
2591
2592 env(pay(gw_, alice_, btc(200'000)));
2593 env(pay(gw_, bob_, btc(200'000)));
2594 env(pay(gw_, alice_, eth(200'000)));
2595 env(pay(gw_, bob_, eth(200'000)));
2596 env.close();
2597
2598 AMM const ammAliceXrpBtc(env, alice_, XRP(100), btc(101'000));
2599 AMM const ammAliceXrpEth(env, alice_, XRP(100), eth(101'000));
2600 env(pay(alice_, bob_, eth(1'000)),
2601 Path(~btc, ~XRP, ~eth),
2602 Sendmax(XRP(1)),
2603 Txflags(tfNoRippleDirect),
2605 };
2607 }
2608
2609 // Payment path ending with XRP
2610 {
2611 auto test = [&](auto&& issue1, auto&& issue2) {
2612 Env env(*this);
2613 env.fund(XRP(30'000), alice_, bob_, gw_);
2614 env.close();
2615 auto const eth = issue1(
2616 {.env = env,
2617 .token = "ETH",
2618 .issuer = gw_,
2619 .holders = {alice_, bob_},
2620 .limit = 2000'000});
2621 auto const btc = issue2(
2622 {.env = env,
2623 .token = "BTC",
2624 .issuer = gw_,
2625 .holders = {alice_, bob_},
2626 .limit = 2000'000});
2627
2628 env(pay(gw_, alice_, btc(200'000)));
2629 env(pay(gw_, bob_, btc(200'000)));
2630 env(pay(gw_, alice_, eth(200'000)));
2631 env(pay(gw_, bob_, eth(200'000)));
2632 env.close();
2633
2634 AMM const ammAliceXrpBtc(env, alice_, XRP(100), btc(100'000));
2635 AMM const ammAliceXrpEth(env, alice_, XRP(100), eth(100'000));
2636 // ETH -> //XRP -> //BTC ->XRP
2637 env(pay(alice_, bob_, XRP(1)),
2638 Path(~XRP, ~btc, ~XRP),
2639 Sendmax(eth(1'000)),
2640 Txflags(tfNoRippleDirect),
2642 };
2644 }
2645
2646 // Payment where loop is formed in the middle of the path, not
2647 // on an endpoint
2648 {
2649 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
2650 Env env(*this);
2651 env.fund(XRP(10'000), gw_, alice_, bob_);
2652 env.close();
2653 auto const eth = issue1(
2654 {.env = env,
2655 .token = "ETH",
2656 .issuer = gw_,
2657 .holders = {alice_, bob_},
2658 .limit = 2000'000});
2659 auto const btc = issue2(
2660 {.env = env,
2661 .token = "BTC",
2662 .issuer = gw_,
2663 .holders = {alice_, bob_},
2664 .limit = 2000'000});
2665 auto const jpy = issue2(
2666 {.env = env,
2667 .token = "JPY",
2668 .issuer = gw_,
2669 .holders = {alice_, bob_},
2670 .limit = 2000'000});
2671
2672 env(pay(gw_, alice_, btc(200'000)));
2673 env(pay(gw_, bob_, btc(200'000)));
2674 env(pay(gw_, alice_, eth(200'000)));
2675 env(pay(gw_, bob_, eth(200'000)));
2676 env(pay(gw_, alice_, jpy(200'000)));
2677 env(pay(gw_, bob_, jpy(200'000)));
2678 env.close();
2679
2680 AMM const ammAliceXrpBtc(env, alice_, XRP(100), btc(100'000));
2681 AMM const ammAliceXrpEth(env, alice_, XRP(100), eth(100'000));
2682 AMM const ammAliceXrpJpy(env, alice_, XRP(100), jpy(100'000));
2683
2684 env(pay(alice_, bob_, jpy(1'000)),
2685 Path(~XRP, ~eth, ~XRP, ~jpy),
2686 Sendmax(btc(1'000)),
2687 Txflags(tfNoRippleDirect),
2689 };
2691 }
2692 }
2693
2694 void
2696 {
2697 testcase("Step Limit");
2698
2699 using namespace jtx;
2700 {
2701 Env env(*this, features);
2702 auto const dan = Account("dan");
2703 auto const ed = Account("ed");
2704
2705 env.fund(XRP(100'000'000), gw_, alice_, bob_, carol_, dan, ed);
2706
2707 MPTTester const btc(
2708 {.env = env, .issuer = gw_, .holders = {bob_, dan, ed}, .flags = kMptDexFlags});
2709
2710 env(pay(gw_, ed, btc(11'000'000'000'000)));
2711 env(pay(gw_, bob_, btc(1'000'000'000'000)));
2712 env(pay(gw_, dan, btc(1'000'000'000'000)));
2713
2714 nOffers(env, 2'000, bob_, XRP(1), btc(1'000'000'000'000));
2715 nOffers(env, 1, dan, XRP(1), btc(1'000'000'000'000));
2716 AMM const ammEd(env, ed, XRP(9), btc(11'000'000'000'000));
2717
2718 // Alice offers to buy 1000 XRP for 1000e12 BTC. She takes Bob's
2719 // first offer, removes 999 more as unfunded, then hits the step
2720 // limit.
2721 env(offer(alice_, btc(1'000'000'000'000'000), XRP(1'000)));
2722 env.require(Balance(alice_, btc(2'050'125'257'867)));
2723 env.require(Owners(alice_, 2));
2724 env.require(Balance(bob_, btc(0)));
2725 env.require(Owners(bob_, 1'001));
2726 env.require(Balance(dan, btc(1'000'000'000'000)));
2727 env.require(Owners(dan, 2));
2728
2729 // Carol offers to buy 1000 XRP for 1000e12 BTC. She removes Bob's
2730 // next 1000 offers as unfunded and hits the step limit.
2731 env(offer(carol_, btc(1'000'000'000'000'000), XRP(1'000)));
2732 env.require(Balance(carol_, MPT(btc)(kNone)));
2733 env.require(Owners(carol_, 1));
2734 env.require(Balance(bob_, btc(0)));
2735 env.require(Owners(bob_, 1));
2736 env.require(Balance(dan, btc(1'000'000'000'000)));
2737 env.require(Owners(dan, 2));
2738 }
2739
2740 // MPT/IOU, similar to the case above
2741 {
2742 Env env(*this, features);
2743 auto const dan = Account("dan");
2744 auto const ed = Account("ed");
2745
2746 env.fund(XRP(100'000), gw_, alice_, bob_, carol_, dan, ed);
2747 env.close();
2748
2749 MPTTester const usd(
2750 {.env = env,
2751 .issuer = gw_,
2752 .holders = {alice_, bob_, carol_, dan, ed},
2753 .pay = 10000'000'000,
2754 .flags = kMptDexFlags});
2755
2756 env.trust(BTC(11'000'000'000'000), ed);
2757 env(pay(gw_, ed, BTC(11'000'000'000'000)));
2758 env.trust(BTC(1'000'000'000'000), bob_);
2759 env(pay(gw_, bob_, BTC(1'000'000'000'000)));
2760 env.trust(BTC(1'000'000'000'000), dan);
2761 env(pay(gw_, dan, BTC(1'000'000'000'000)));
2762 env.close();
2763
2764 nOffers(env, 2'000, bob_, usd(1000000), BTC(1'000'000'000'000));
2765 nOffers(env, 1, dan, usd(1000000), BTC(1'000'000'000'000));
2766 AMM const ammEd(env, ed, usd(9000000), BTC(11'000'000'000'000));
2767 env(offer(alice_, BTC(1'000'000'000'000'000), usd(1'000000000)));
2768
2769 env.require(Balance(alice_, STAmount{BTC, UINT64_C(2050125257867'587), -3}));
2770 env.require(Owners(alice_, 3));
2771 env.require(Balance(bob_, BTC(0)));
2772 env.require(Owners(bob_, 1'002));
2773 env.require(Balance(dan, BTC(1000000000000)));
2774 env.require(Owners(dan, 3));
2775 }
2776
2777 // IOU/MPT, similar to the case above
2778 {
2779 Env env(*this, features);
2780 auto const dan = Account("dan");
2781 auto const ed = Account("ed");
2782
2783 env.fund(XRP(100'000), gw_, alice_, bob_, carol_, dan, ed);
2784 env.close();
2785
2786 env.trust(USD(10000'000'000), alice_);
2787 env(pay(gw_, alice_, USD(10000'000'000)));
2788 env.trust(USD(10000'000'000), bob_);
2789 env(pay(gw_, bob_, USD(10000'000'000)));
2790 env.trust(USD(10000'000'000), carol_);
2791 env(pay(gw_, carol_, USD(10000'000'000)));
2792 env.trust(USD(10000'000'000), dan);
2793 env(pay(gw_, dan, USD(10000'000'000)));
2794 env.trust(USD(10000'000'000), ed);
2795 env(pay(gw_, ed, USD(10000'000'000)));
2796 env.close();
2797
2798 MPTTester const btc(
2799 {.env = env, .issuer = gw_, .holders = {bob_, dan, ed}, .flags = kMptDexFlags});
2800
2801 env(pay(gw_, ed, btc(11'000'000'000'000)));
2802 env(pay(gw_, bob_, btc(1'000'000'000'000)));
2803 env(pay(gw_, dan, btc(1'000'000'000'000)));
2804 env.close();
2805
2806 nOffers(env, 2'000, bob_, USD(1000000), btc(1'000'000'000'000));
2807 nOffers(env, 1, dan, USD(1000000), btc(1'000'000'000'000));
2808 AMM const ammEd(env, ed, USD(9000000), btc(11'000'000'000'000));
2809 env(offer(alice_, btc(1'000'000'000'000'000), USD(1'000000000)));
2810
2811 env.require(Balance(alice_, btc(2050125628933)));
2812 env.require(Owners(alice_, 3));
2813 env.require(Balance(bob_, btc(0)));
2814 env.require(Owners(bob_, 1'002));
2815 env.require(Balance(dan, btc(1000000000000)));
2816 env.require(Owners(dan, 3));
2817 }
2818
2819 // MPT/MPT, similar to the case above
2820 {
2821 Env env(*this, features);
2822 auto const dan = Account("dan");
2823 auto const ed = Account("ed");
2824
2825 env.fund(XRP(100'000), gw_, alice_, bob_, carol_, dan, ed);
2826 env.close();
2827
2828 MPTTester const btc(
2829 {.env = env, .issuer = gw_, .holders = {bob_, dan, ed}, .flags = kMptDexFlags});
2830 MPTTester const usd(
2831 {.env = env,
2832 .issuer = gw_,
2833 .holders = {alice_, bob_, carol_, dan, ed},
2834 .pay = 10000'000'000,
2835 .flags = kMptDexFlags});
2836
2837 env(pay(gw_, ed, btc(11'000'000'000'000)));
2838 env(pay(gw_, bob_, btc(1'000'000'000'000)));
2839 env(pay(gw_, dan, btc(1'000'000'000'000)));
2840 env.close();
2841
2842 nOffers(env, 2'000, bob_, usd(1000000), btc(1'000'000'000'000));
2843 nOffers(env, 1, dan, usd(1000000), btc(1'000'000'000'000));
2844 AMM const ammEd(env, ed, usd(9000000), btc(11'000'000'000'000));
2845 env(offer(alice_, btc(1'000'000'000'000'000), usd(1'000000000)));
2846
2847 env.require(Balance(alice_, btc(2050125257867)));
2848 env.require(Owners(alice_, 3));
2849 env.require(Balance(bob_, btc(0)));
2850 env.require(Owners(bob_, 1'002));
2851 env.require(Balance(dan, btc(1000000000000)));
2852 env.require(Owners(dan, 3));
2853 }
2854 }
2855
2856 void
2858 {
2859 testcase("Convert all of an asset using DeliverMin");
2860
2861 using namespace jtx;
2862
2863 {
2864 Env env(*this, features);
2865 fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000));
2866
2867 MPTTester const btc(
2868 {.env = env,
2869 .issuer = gw_,
2870 .holders = {alice_, bob_, carol_},
2871 .flags = kMptDexFlags});
2872
2873 env(pay(alice_, bob_, btc(10'000)), DeliverMin(btc(10'000)), Ter(temBAD_AMOUNT));
2874 env(pay(alice_, bob_, btc(10'000)),
2875 DeliverMin(btc(-5'000)),
2876 Txflags(tfPartialPayment),
2878 env(pay(alice_, bob_, btc(10'000)),
2879 DeliverMin(XRP(5)),
2880 Txflags(tfPartialPayment),
2882 env(pay(alice_, bob_, btc(10'000)),
2883 DeliverMin(btc(5'000)),
2884 Txflags(tfPartialPayment),
2885 Ter(tecPATH_DRY));
2886 env(pay(alice_, bob_, btc(10'000)),
2887 DeliverMin(btc(15'000)),
2888 Txflags(tfPartialPayment),
2890 env(pay(gw_, carol_, btc(50'000)));
2891 AMM const ammCarol(env, carol_, XRP(10), btc(15'000));
2892 env(pay(alice_, bob_, btc(10'000)),
2893 Paths(XRP),
2894 DeliverMin(btc(7'000)),
2895 Txflags(tfPartialPayment),
2896 Sendmax(XRP(5)),
2898 env.require(
2899 Balance(alice_, drops(10'000'000'000 - (3 * env.current()->fees().base.drops()))));
2900 env.require(Balance(bob_, drops(10'000'000'000 - env.current()->fees().base.drops())));
2901 }
2902
2903 {
2904 Env env(*this, features);
2905 fund(env, gw_, {alice_, bob_}, XRP(10'000));
2906
2907 MPTTester const btc(
2908 {.env = env, .issuer = gw_, .holders = {alice_, bob_}, .flags = kMptDexFlags});
2909
2910 env(pay(gw_, bob_, btc(1'100'000)));
2911 AMM const ammBob(env, bob_, XRP(1'000), btc(1'100'000));
2912 env(pay(alice_, alice_, btc(10'000'000)),
2913 Paths(XRP),
2914 DeliverMin(btc(100'000)),
2915 Txflags(tfPartialPayment),
2916 Sendmax(XRP(100)));
2917 env.require(Balance(alice_, btc(100'000)));
2918 }
2919
2920 // IOU/MPT mix, similar to the above case
2921 {
2922 auto test = [&](auto&& issue1, auto&& issue2) {
2923 Env env(*this);
2924 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
2925 env.close();
2926 auto const usd = issue1(
2927 {.env = env,
2928 .token = "USD",
2929 .issuer = gw_,
2930 .holders = {alice_, bob_, carol_},
2931 .limit = 3000'000});
2932 auto const btc = issue2(
2933 {.env = env,
2934 .token = "BTC",
2935 .issuer = gw_,
2936 .holders = {alice_, bob_, carol_},
2937 .limit = 1'000'000});
2938
2939 env(pay(gw_, alice_, usd(10'000)));
2940 env(pay(gw_, bob_, usd(10'000)));
2941 env(pay(gw_, bob_, btc(1'200)));
2942 env.close();
2943
2944 AMM const ammBob(env, bob_, usd(1'000), btc(1'100));
2945 env(pay(alice_, alice_, btc(10'000)),
2946 Paths(usd),
2947 DeliverMin(btc(100)),
2948 Txflags(tfPartialPayment),
2949 Sendmax(usd(100)));
2950 env.require(Balance(alice_, btc(100)));
2951 };
2953 }
2954
2955 {
2956 Env env(*this, features);
2957 fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000));
2958
2959 MPTTester const btc(
2960 {.env = env, .issuer = gw_, .holders = {bob_, carol_}, .flags = kMptDexFlags});
2961
2962 env(pay(gw_, bob_, btc(1'200'000)));
2963 AMM const ammBob(env, bob_, XRP(5'500), btc(1'200'000));
2964 env(pay(alice_, carol_, btc(10'000'000)),
2965 Paths(XRP),
2966 DeliverMin(btc(200'000)),
2967 Txflags(tfPartialPayment),
2968 Sendmax(XRP(1'000)),
2970 env(pay(alice_, carol_, btc(10'000'000)),
2971 Paths(XRP),
2972 DeliverMin(btc(200'000)),
2973 Txflags(tfPartialPayment),
2974 Sendmax(XRP(1'100)));
2975 BEAST_EXPECT(ammBob.expectBalances(XRP(6'600), btc(1'000'000), ammBob.tokens()));
2976 env.require(Balance(carol_, btc(200'000)));
2977 }
2978
2979 // IOU/MPT mix, similar to the above case
2980 {
2981 auto test = [&](auto&& issue1, auto&& issue2) {
2982 Env env(*this);
2983 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
2984 env.close();
2985 auto const usd = issue1(
2986 {.env = env,
2987 .token = "USD",
2988 .issuer = gw_,
2989 .holders = {alice_, bob_, carol_},
2990 .limit = 3000'000});
2991 auto const btc = issue2(
2992 {.env = env,
2993 .token = "BTC",
2994 .issuer = gw_,
2995 .holders = {alice_, bob_, carol_},
2996 .limit = 1'000'000});
2997
2998 env(pay(gw_, alice_, usd(100'000)));
2999 env(pay(gw_, bob_, usd(100'000)));
3000 env(pay(gw_, carol_, usd(100'000)));
3001
3002 env(pay(gw_, bob_, btc(1'200)));
3003 env.close();
3004
3005 AMM const ammBob(env, bob_, usd(5'500), btc(1'200));
3006 env(pay(alice_, carol_, btc(10'000)),
3007 Paths(usd),
3008 DeliverMin(btc(200)),
3009 Txflags(tfPartialPayment),
3010 Sendmax(usd(1'000)),
3012 env(pay(alice_, carol_, btc(10'000)),
3013 Paths(usd),
3014 DeliverMin(btc(200)),
3015 Txflags(tfPartialPayment),
3016 Sendmax(usd(1'100)));
3017 BEAST_EXPECT(ammBob.expectBalances(usd(6'600), btc(1'000), ammBob.tokens()));
3018 env.require(Balance(carol_, btc(200)));
3019 };
3021 }
3022
3023 {
3024 auto const dan = Account("dan");
3025 Env env(*this, features);
3026 fund(env, gw_, {alice_, bob_, carol_, dan}, XRP(10'000));
3027
3028 MPTTester const btc(
3029 {.env = env, .issuer = gw_, .holders = {bob_, carol_, dan}, .flags = kMptDexFlags});
3030
3031 env(pay(gw_, bob_, btc(100'000'000)));
3032 env(pay(gw_, dan, btc(1'100'000'000)));
3033 env(offer(bob_, XRP(100), btc(100'000'000)));
3034 env(offer(bob_, XRP(1'000), btc(100'000'000)));
3035 AMM const ammDan(env, dan, XRP(1'000), btc(1'100'000'000));
3036
3037 env(pay(alice_, carol_, btc(10'000'000'000)),
3038 Paths(XRP),
3039 DeliverMin(btc(200'000'000)),
3040 Txflags(tfPartialPayment),
3041 Sendmax(XRPAmount(200'000'001)));
3042 env.require(Balance(bob_, btc(0)));
3043 env.require(Balance(carol_, btc(200'000'000)));
3044 BEAST_EXPECT(
3045 ammDan.expectBalances(XRPAmount{1'100'000'001}, btc(1000'000000), ammDan.tokens()));
3046 }
3047 }
3048
3049 void
3051 {
3052 testcase("Payment");
3053
3054 using namespace jtx;
3055 Account const becky{"becky"};
3056
3057 Env env(*this, features);
3058 fund(env, gw_, {alice_, becky}, XRP(5'000));
3059
3060 MPTTester const btc(
3061 {.env = env, .issuer = gw_, .holders = {alice_, becky}, .flags = kMptDexFlags});
3062
3063 env(pay(gw_, alice_, btc(500'000)));
3064 env.close();
3065
3066 AMM const ammAlice(env, alice_, XRP(100), btc(140'000));
3067
3068 // becky pays herself BTC (10'000) by consuming part of alice's offer.
3069 // Make sure the payment works if PaymentAuth is not involved.
3070 env(pay(becky, becky, btc(10'000)), Path(~MPT(btc)), Sendmax(XRP(10)));
3071 env.close();
3072 BEAST_EXPECT(
3073 ammAlice.expectBalances(XRPAmount(107'692'308), btc(130'000), ammAlice.tokens()));
3074
3075 // becky decides to require authorization for deposits.
3076 env(fset(becky, asfDepositAuth));
3077 env.close();
3078
3079 // becky pays herself again.
3080 env(pay(becky, becky, btc(10'000)), Path(~MPT(btc)), Sendmax(XRP(10)), Ter(tesSUCCESS));
3081
3082 env.close();
3083 }
3084
3085 void
3087 {
3088 // Exercise MPT payments and non-direct XRP payments to an account
3089 // that has the lsfDepositAuth flag set.
3090 testcase("Pay MPT");
3091
3092 using namespace jtx;
3093
3094 Env env(*this);
3095
3096 fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000));
3097
3098 MPTTester btc(
3099 {.env = env, .issuer = gw_, .holders = {alice_, bob_, carol_}, .flags = kMptDexFlags});
3100
3101 env(pay(gw_, alice_, btc(150'000)));
3102 env(pay(gw_, carol_, btc(150'000)));
3103 AMM const ammCarol(env, carol_, btc(100'000), XRPAmount(101));
3104
3105 env(pay(alice_, bob_, btc(50'000)));
3106 env.close();
3107
3108 // bob sets the lsfDepositAuth flag.
3109 env(fset(bob_, asfDepositAuth), Require(Flags(bob_, asfDepositAuth)));
3110 env.close();
3111
3112 // None of the following payments should succeed.
3113 auto failedMptPayments = [this, &env, &btc]() {
3114 env.require(Flags(bob_, asfDepositAuth));
3115
3116 // Capture bob's balances before hand to confirm they don't
3117 // change.
3118 PrettyAmount const bobXrpBalance{env.balance(bob_, XRP)};
3119 PrettyAmount const bobBTCBalance{env.balance(bob_, MPT(btc))};
3120
3121 env(pay(alice_, bob_, btc(50'000)), Ter(tecNO_PERMISSION));
3122 env.close();
3123
3124 // Note that even though alice is paying bob in XRP, the payment
3125 // is still not allowed since the payment passes through an
3126 // offer.
3127 env(pay(alice_, bob_, drops(1)), Sendmax(btc(1'000)), Ter(tecNO_PERMISSION));
3128 env.close();
3129
3130 BEAST_EXPECT(bobXrpBalance == env.balance(bob_, XRP));
3131 BEAST_EXPECT(bobBTCBalance == env.balance(bob_, MPT(btc)));
3132 };
3133
3134 // Test when bob has an XRP balance > base reserve.
3135 failedMptPayments();
3136
3137 // Set bob's XRP balance == base reserve. Also demonstrate that
3138 // bob can make payments while his lsfDepositAuth flag is set.
3139 env(pay(bob_, alice_, btc(25'000)));
3140 env.close();
3141
3142 {
3143 STAmount const bobPaysXRP{env.balance(bob_, XRP) - reserve(env, 1)};
3144 XRPAmount const bobPaysFee{reserve(env, 1) - reserve(env, 0)};
3145 env(pay(bob_, alice_, bobPaysXRP), Fee(bobPaysFee));
3146 env.close();
3147 }
3148
3149 // Test when bob's XRP balance == base reserve.
3150 BEAST_EXPECT(env.balance(bob_, XRP) == reserve(env, 0));
3151 BEAST_EXPECT(env.balance(bob_, MPT(btc)) == btc(25'000));
3152 failedMptPayments();
3153
3154 // Test when bob has an XRP balance == 0.
3155 env(noop(bob_), Fee(reserve(env, 0)));
3156 env.close();
3157
3158 BEAST_EXPECT(env.balance(bob_, XRP) == XRP(0));
3159 failedMptPayments();
3160
3161 // Give bob enough XRP for the fee to clear the lsfDepositAuth flag.
3162 env(pay(alice_, bob_, drops(env.current()->fees().base)));
3163
3164 // bob clears the lsfDepositAuth and the next payment succeeds.
3165 env(fclear(bob_, asfDepositAuth));
3166 env.close();
3167
3168 env(pay(alice_, bob_, btc(50'000)));
3169 env.close();
3170
3171 env(pay(alice_, bob_, drops(1)), Sendmax(btc(1'000)));
3172 env.close();
3173 BEAST_EXPECT(ammCarol.expectBalances(btc(101'000), XRPAmount(100), ammCarol.tokens()));
3174 }
3175
3176 void
3178 {
3179 testcase("Individual Lock");
3180
3181 using namespace test::jtx;
3182 Env env(*this, features);
3183
3184 Account const g1{"G1"};
3185 Account const alice{"alice"};
3186 Account const bob{"bob"};
3187
3188 env.fund(XRP(1'000), g1, alice, bob);
3189
3190 MPTTester btc(
3191 {.env = env,
3192 .issuer = g1,
3193 .holders = {alice, bob},
3194 .flags = tfMPTCanLock | kMptDexFlags});
3195
3196 env(pay(g1, bob, btc(10)));
3197 env(pay(g1, alice, btc(205)));
3198 env.close();
3199
3200 AMM const ammAlice(env, alice, XRP(500), btc(105));
3201
3202 env.require(Balance(bob, btc(10)));
3203 env.require(Balance(alice, btc(100)));
3204
3205 // Account with MPT unlocked (proving operations normally work)
3206 // can make Payment
3207 env(pay(alice, bob, btc(1)));
3208
3209 // can receive Payment
3210 env(pay(bob, alice, btc(1)));
3211 env.close();
3212
3213 // Lock MPT for bob
3214 btc.set({.holder = bob, .flags = tfMPTLock});
3215
3216 {
3217 env(offer(bob, btc(5), XRP(25)), Ter(tecLOCKED));
3218 env.close();
3219 BEAST_EXPECT(ammAlice.expectBalances(XRP(500), btc(105), ammAlice.tokens()));
3220 }
3221
3222 {
3223 // can not sell assets
3224 env(offer(bob, XRP(1), btc(5)), Ter(tecUNFUNDED_OFFER));
3225
3226 // different from IOU
3227 // can not receive Payment when locked
3228 env(pay(alice, bob, btc(1)), Ter(tecPATH_DRY));
3229
3230 // can not make Payment when locked
3231 env(pay(bob, alice, btc(1)), Ter(tecPATH_DRY));
3232
3233 env.require(Balance(bob, btc(10)));
3234 }
3235
3236 {
3237 // Unlock
3238 btc.set({.holder = bob, .flags = tfMPTUnlock});
3239 env(offer(bob, XRP(1), btc(5)));
3240 env(pay(bob, alice, btc(1)));
3241 env(pay(alice, bob, btc(1)));
3242 env.close();
3243 }
3244 }
3245
3246 void
3248 {
3249 testcase("Global Lock");
3250
3251 using namespace test::jtx;
3252 Env env(*this, features);
3253
3254 Account const g1{"G1"};
3255 Account a1{"A1"};
3256 Account a2{"A2"};
3257 Account a3{"A3"};
3258 Account a4{"A4"};
3259
3260 env.fund(XRP(12'000), g1);
3261 env.fund(XRP(1'000), a1);
3262 env.fund(XRP(20'000), a2, a3, a4);
3263
3264 MPTTester const eth(
3265 {.env = env,
3266 .issuer = g1,
3267 .holders = {a1, a2, a3, a4},
3268 .flags = tfMPTCanLock | kMptDexFlags});
3269
3270 MPTTester btc(
3271 {.env = env,
3272 .issuer = g1,
3273 .holders = {a1, a2, a3, a4},
3274 .flags = tfMPTCanLock | kMptDexFlags});
3275
3276 env(pay(g1, a1, eth(1'000)));
3277 env(pay(g1, a2, eth(100)));
3278 env(pay(g1, a3, btc(100)));
3279 env(pay(g1, a4, btc(100)));
3280 env.close();
3281
3282 AMM const ammG1(env, g1, XRP(10'000), eth(100));
3283 env(offer(a1, XRP(10'000), eth(100)), Txflags(tfPassive));
3284 env(offer(a2, eth(100), XRP(10'000)), Txflags(tfPassive));
3285 env.close();
3286
3287 {
3288 // Account without Global Lock (proving operations normally
3289 // work)
3290 // visible offers where taker_pays is unlocked issuer
3291 auto offers = getAccountOffers(env, a2)[jss::offers];
3292 if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
3293 return;
3294
3295 // visible offers where taker_gets is unlocked issuer
3296 offers = getAccountOffers(env, a1)[jss::offers];
3297 if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
3298 return;
3299 }
3300
3301 {
3302 // Offers/Payments
3303 // assets can be bought on the market
3304 AMM ammA3(env, a3, btc(1), XRP(1));
3305
3306 // assets can be sold on the market
3307 // AMM is bidirectional
3308 env(pay(g1, a2, eth(1)));
3309 env(pay(a2, g1, eth(1)));
3310 env(pay(a2, a1, eth(1)));
3311 env(pay(a1, a2, eth(1)));
3312 ammA3.withdrawAll(std::nullopt);
3313 }
3314
3315 {
3316 // Account with Global Lock
3317 // set Global Lock first
3318 btc.set({.flags = tfMPTLock});
3319
3320 // assets can't be bought on the market
3321 AMM const ammA3(env, a3, btc(1), XRP(1), Ter(tecLOCKED));
3322
3323 // direct issues can be sent
3324 env(pay(g1, a2, btc(1)));
3325 env(pay(a2, g1, btc(1)));
3326 // locked
3327 env(pay(a2, a1, btc(1)), Ter(tecPATH_DRY));
3328 env(pay(a1, a2, btc(1)), Ter(tecPATH_DRY));
3329 }
3330
3331 {
3332 auto offers = getAccountOffers(env, a2)[jss::offers];
3333 if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
3334 return;
3335
3336 offers = getAccountOffers(env, a1)[jss::offers];
3337 if (!BEAST_EXPECT(checkArraySize(offers, 1u)))
3338 return;
3339 }
3340 }
3341
3342 void
3344 {
3345 testcase("Offers for Locked MPTs");
3346
3347 using namespace test::jtx;
3348 Env env(*this, features);
3349
3350 Account const g1{"G1"};
3351 Account a2{"A2"};
3352 Account a3{"A3"};
3353 Account a4{"A4"};
3354
3355 env.fund(XRP(2'000), g1, a3, a4);
3356 env.fund(XRP(2'000), a2);
3357 env.close();
3358
3359 MPTTester btc(
3360 {.env = env,
3361 .issuer = g1,
3362 .holders = {a2, a3, a4},
3363 .flags = tfMPTCanLock | kMptDexFlags});
3364
3365 env(pay(g1, a3, btc(2'000)));
3366 env(pay(g1, a4, btc(2'001)));
3367 env.close();
3368
3369 AMM const ammA3(env, a3, XRP(1'000), btc(1'001));
3370
3371 // removal after successful payment
3372 // test: make a payment with partially consuming offer
3373 env(pay(a2, g1, btc(1)), Paths(MPT(btc)), Sendmax(XRP(1)));
3374 env.close();
3375
3376 BEAST_EXPECT(ammA3.expectBalances(XRP(1'001), btc(1'000), ammA3.tokens()));
3377
3378 // test: someone else creates an offer providing liquidity
3379 env(offer(a4, XRP(999), btc(999)));
3380 env.close();
3381 // The offer consumes AMM offer
3382 BEAST_EXPECT(ammA3.expectBalances(XRP(1'000), btc(1'001), ammA3.tokens()));
3383
3384 // test: AMM is Locked
3385 btc.set({.holder = ammA3.ammAccount(), .flags = tfMPTLock});
3386 auto const info = ammA3.ammRpcInfo();
3387 BEAST_EXPECT(info[jss::amm][jss::asset2_frozen].asBool());
3388 env.close();
3389
3390 // test: Can make a payment via the new offer
3391 env(pay(a2, g1, btc(1)), Paths(MPT(btc)), Sendmax(XRP(1)));
3392 env.close();
3393 // AMM is not consumed
3394 BEAST_EXPECT(ammA3.expectBalances(XRP(1'000), btc(1'001), ammA3.tokens()));
3395
3396 // removal buy successful OfferCreate
3397 // test: lock the new offer
3398 btc.set({.holder = a4, .flags = tfMPTUnlock});
3399 env.close();
3400
3401 // test: can no longer create a crossing offer
3402 env(offer(a2, btc(999), XRP(999)));
3403 env.close();
3404
3405 // test: offer was removed by offer_create
3406 auto offers = getAccountOffers(env, a4)[jss::offers];
3407 if (!BEAST_EXPECT(checkArraySize(offers, 0u)))
3408 return;
3409 }
3410
3411 void
3413 {
3414 testcase("Multisign AMM Transactions");
3415
3416 using namespace jtx;
3417 Env env{*this, features};
3418 Account const bogie{"bogie", KeyType::Secp256k1};
3419 Account const alice{"alice", KeyType::Secp256k1};
3420 Account const becky{"becky", KeyType::Ed25519};
3421 Account const zelda{"zelda", KeyType::Secp256k1};
3422 fund(env, gw_, {alice, becky, zelda}, XRP(20'000));
3423
3424 MPTTester const btc(
3425 {.env = env,
3426 .issuer = gw_,
3427 .holders = {alice, becky, zelda},
3428 .pay = 20'000'000'000,
3429 .flags = kMptDexFlags});
3430
3431 // alice uses a regular key with the master disabled.
3432 Account const alie{"alie", KeyType::Secp256k1};
3433 env(regkey(alice, alie));
3434 env(fset(alice, asfDisableMaster), Sig(alice));
3435
3436 // Attach signers to alice.
3437 env(signers(alice, 2, {{becky, 1}, {bogie, 1}}), Sig(alie));
3438 env.close();
3439 static constexpr int kSignerListOwners{2};
3440 env.require(Owners(alice, kSignerListOwners + 0));
3441
3442 Msig const ms{becky, bogie};
3443
3444 // Multisign all AMM transactions
3445 AMM ammAlice(
3446 env,
3447 alice,
3448 XRP(10'000),
3449 btc(10'000),
3450 false,
3451 0,
3452 ammCrtFee(env).drops(),
3453 std::nullopt,
3454 std::nullopt,
3455 ms,
3456 Ter(tesSUCCESS));
3457 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'000), ammAlice.tokens()));
3458
3459 ammAlice.deposit(alice, 1'000'000);
3460 BEAST_EXPECT(ammAlice.expectBalances(XRP(11'000), btc(11'000), IOUAmount{11'000'000, 0}));
3461
3462 ammAlice.withdraw(alice, 1'000'000);
3463 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'000), ammAlice.tokens()));
3464
3465 ammAlice.vote({}, 1'000);
3466 BEAST_EXPECT(ammAlice.expectTradingFee(1'000));
3467
3468 env(ammAlice.bid({.account = alice, .bidMin = 100}), ms).close();
3469 BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, IOUAmount{4'000}));
3470 // 4000 tokens burnt
3471 BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), btc(10'000), IOUAmount{9'996'000, 0}));
3472 }
3473
3474 void
3476 {
3477 testcase("To Strand");
3478
3479 using namespace jtx;
3480
3481 // cannot have more than one offer with the same output issue
3482 {
3483 auto test = [&](auto&& issue1, auto&& issue2) {
3484 Env env(*this);
3485 env.fund(XRP(30'000), alice_, bob_, carol_, gw_);
3486 env.close();
3487 auto const eth = issue1(
3488 {.env = env,
3489 .token = "ETH",
3490 .issuer = gw_,
3491 .holders = {alice_, bob_, carol_},
3492 .limit = 1'000'000});
3493 auto const btc = issue2(
3494 {.env = env,
3495 .token = "BTC",
3496 .issuer = gw_,
3497 .holders = {alice_, bob_, carol_},
3498 .limit = 1'000'000});
3499 env(pay(gw_, alice_, btc(50000)));
3500 env(pay(gw_, bob_, btc(50000)));
3501 env(pay(gw_, carol_, btc(50000)));
3502 env(pay(gw_, alice_, eth(50000)));
3503 env(pay(gw_, bob_, eth(50000)));
3504 env(pay(gw_, carol_, eth(50000)));
3505 env.close();
3506 AMM const bobXrpBtc(env, bob_, XRP(1'000), btc(1'000));
3507 AMM const bobBtcEth(env, bob_, btc(1'000), eth(1'000));
3508
3509 // payment path: XRP -> XRP/BTC -> BTC/ETH -> ETH/BTC
3510 env(pay(alice_, carol_, btc(100)),
3511 Path(~btc, ~eth, ~btc),
3512 Sendmax(XRP(200)),
3513 Txflags(tfNoRippleDirect),
3515 };
3517 }
3518 }
3519
3520 void
3522 {
3523 using namespace jtx;
3524 testcase("RIPD1373");
3525
3526 {
3527 Env env(*this, features);
3528 fund(env, gw_, {alice_, bob_}, XRP(10'000));
3529
3530 MPTTester btc(
3531 {.env = env,
3532 .issuer = bob_,
3533 .holders = {alice_, gw_},
3534 .pay = 100'000'000,
3535 .flags = kMptDexFlags});
3536
3537 MPTTester eth(
3538 {.env = env,
3539 .issuer = bob_,
3540 .holders = {alice_, gw_},
3541 .pay = 100'000'000,
3542 .flags = kMptDexFlags});
3543
3544 AMM const ammXrpBtc(env, bob_, XRP(100), btc(100'000));
3545 env(offer(gw_, XRP(100), btc(100'000)), Txflags(tfPassive));
3546
3547 AMM const ammBtcEth(env, bob_, btc(100'000), eth(100'000));
3548 env(offer(gw_, btc(100'000), eth(100'000)), Txflags(tfPassive));
3549
3550 TestPath const p = [&] {
3551 TestPath result;
3552 result.pushBack(allPathElements(gw_, MPT(btc)));
3553 result.pushBack(cpe(eth.issuanceID()));
3554 return result;
3555 }();
3556
3557 PathSet const paths(p);
3558
3559 env(pay(alice_, alice_, eth(1'000)),
3560 Json(paths.json()),
3561 Sendmax(XRP(10)),
3562 Txflags(tfNoRippleDirect | tfPartialPayment),
3563 Ter(temBAD_PATH));
3564 }
3565
3566 {
3567 Env env(*this, features);
3568
3569 fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000));
3570
3571 MPTTester const btc(
3572 {.env = env,
3573 .issuer = gw_,
3574 .holders = {alice_, bob_, carol_},
3575 .pay = 100'000,
3576 .flags = kMptDexFlags});
3577
3578 AMM const ammBob(env, bob_, XRP(100), btc(100));
3579
3580 // payment path: XRP -> XRP/BTC -> BTC/XRP
3581 env(pay(alice_, carol_, XRP(100)),
3582 Path(~MPT(btc), ~XRP),
3583 Txflags(tfNoRippleDirect),
3585 }
3586
3587 {
3588 Env env(*this, features);
3589
3590 fund(env, gw_, {alice_, bob_, carol_}, XRP(10'000));
3591
3592 MPTTester const btc(
3593 {.env = env,
3594 .issuer = gw_,
3595 .holders = {alice_, bob_, carol_},
3596 .pay = 100'000,
3597 .flags = kMptDexFlags});
3598
3599 AMM const ammBob(env, bob_, XRP(100), btc(100));
3600
3601 // payment path: XRP -> XRP/BTC -> BTC/XRP
3602 env(pay(alice_, carol_, XRP(100)),
3603 Path(~MPT(btc), ~XRP),
3604 Sendmax(XRP(200)),
3605 Txflags(tfNoRippleDirect),
3607 }
3608 }
3609
3610 void
3612 {
3613 testcase("test loop");
3614 using namespace jtx;
3615
3616 {
3617 Env env(*this, features);
3618
3619 env.fund(XRP(10'000), alice_, bob_, carol_, gw_);
3620
3621 MPTTester const btc(
3622 {.env = env,
3623 .issuer = gw_,
3624 .holders = {alice_, bob_, carol_},
3625 .flags = kMptDexFlags});
3626
3627 env(pay(gw_, bob_, btc(100'000'000)));
3628 env(pay(gw_, alice_, btc(100'000'000)));
3629 env.close();
3630
3631 AMM const ammBob(env, bob_, XRP(100), btc(100'000'000));
3632
3633 // payment path: BTC -> BTC/XRP -> XRP/BTC
3634 env(pay(alice_, carol_, btc(100'000'000)),
3635 Sendmax(btc(100'000'000)),
3636 Path(~XRP, ~MPT(btc)),
3637 Txflags(tfNoRippleDirect),
3639 }
3640
3641 {
3642 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
3643 Env env(*this, features);
3644
3645 env.fund(XRP(10'000), alice_, bob_, carol_, gw_);
3646 env.close();
3647
3648 auto const btc = issue1(
3649 {.env = env, .token = "BTC", .issuer = gw_, .holders = {alice_, bob_, carol_}});
3650 auto const eth = issue2(
3651 {.env = env, .token = "ETH", .issuer = gw_, .holders = {alice_, bob_, carol_}});
3652 auto const cny = issue3(
3653 {.env = env, .token = "CNY", .issuer = gw_, .holders = {alice_, bob_, carol_}});
3654
3655 env(pay(gw_, bob_, btc(200)));
3656 env(pay(gw_, bob_, eth(200)));
3657 env(pay(gw_, bob_, cny(100)));
3658 env.close();
3659
3660 AMM const ammBobXrpBtc(env, bob_, XRP(100), btc(100));
3661 AMM const ammBobBtcEth(env, bob_, btc(100), eth(100));
3662 AMM const ammBobEthCny(env, bob_, eth(100), cny(100));
3663
3664 // payment path: XRP->XRP/BTC->BTC/ETH->BTC/CNY
3665 env(pay(alice_, carol_, cny(100)),
3666 Sendmax(XRP(100)),
3667 Path(~btc, ~eth, ~btc, ~cny),
3668 Txflags(tfNoRippleDirect),
3670 };
3672 }
3673 }
3674
3675 void
3686
3687 void
3689 {
3690 using namespace jtx;
3691 FeatureBitset const all{testableAmendments()};
3692
3693 testFalseDry(all);
3694 testBookStep(all);
3698 }
3699
3700 void
3702 {
3703 using namespace jtx;
3704 FeatureBitset const all{testableAmendments()};
3705 testStepLimit(all);
3706 }
3707
3708 void
3710 {
3711 using namespace jtx;
3712 FeatureBitset const all{testableAmendments()};
3714 }
3715
3716 void
3718 {
3719 auto const supported{jtx::testableAmendments()};
3720 testPayment(supported);
3721 testPayMPT();
3722 }
3723
3724 void
3726 {
3727 using namespace test::jtx;
3728 auto const sa = testableAmendments();
3730 testGlobalLock(sa);
3732 }
3733
3734 void
3736 {
3737 using namespace jtx;
3738 auto const all = testableAmendments();
3739
3740 testTxMultisign(all);
3741 }
3742
3743 void
3745 {
3746 using namespace jtx;
3747 auto const all = testableAmendments();
3748
3749 testToStrand(all);
3750 testRIPD1373(all);
3751 testLoop(all);
3752 }
3753
3754 void
3755 run() override
3756 {
3757 testOffers();
3758 testPaths();
3759 testFlow();
3763 testLock();
3764 testMultisign();
3765 testPayStrand();
3766 }
3767};
3768
3769BEAST_DEFINE_TESTSUITE_PRIO(AMMExtendedMPT, app, xrpl, 1);
3770
3771} // namespace xrpl::test
A generic endpoint for log messages.
Definition Journal.h:44
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
Floating point representation of amounts with high dynamic range.
Definition IOUAmount.h:26
beast::Journal journal(std::string const &name)
Definition Log.cpp:137
bool modify(modify_type const &f)
Modify the open ledger.
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
A wrapper which makes credits unavailable to balances.
json::Value getJson(JsonOptions) const override
void pushBack(STPath const &e)
Definition STPathSet.h:549
bool empty() const
Definition STPathSet.h:543
Discardable, editable view to a ledger.
Definition Sandbox.h:18
void apply(RawView &to)
Definition Sandbox.h:38
virtual OpenLedger & getOpenLedger()=0
virtual Logs & getLogs()=0
SLE::pointer peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
json::Value json() const
Definition PathSet.h:183
TestPath & pushBack(Issue const &iss)
Definition PathSet.h:121
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 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
json::Value ammRpcInfo(std::optional< AccountID > const &account=std::nullopt, std::optional< std::string > const &ledgerIndex=std::nullopt, std::optional< Asset > const &asset1=std::nullopt, std::optional< Asset > const &asset2=std::nullopt, std::optional< AccountID > const &ammAccount=std::nullopt, bool ignoreParams=false, unsigned apiVersion=rpc::kApiInvalidVersion) const
Send amm_info RPC command.
Definition AMM.cpp:183
AccountID const & ammAccount() const
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
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
Sets the DeliverMin on a JTx.
Definition delivermin.h:16
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
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
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
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
void memoize(Account const &account)
Associate AccountID with account.
Definition Env.cpp:174
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
Set the fee on a JTx.
Definition fee.h:20
Match set account flags.
Definition flags.h:119
Inject raw JSON.
Definition jtx_json.h:16
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:447
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:353
Converts to MPT Issue or STAmount.
Set a multisignature on a JTx.
Definition multisign.h:53
Match the number of items in the account's owner directory.
Definition owners.h:55
Add a path.
Definition paths.h:47
Set Paths, SendMax on a JTx.
Definition paths.h:23
Check a set of conditions.
Definition require.h:49
Sets the SendMax on a JTx.
Definition sendmax.h:16
Set the regular signature on a JTx.
Definition sig.h:19
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
@ Array
array value (ordered list)
Definition json_value.h:28
Keylet offer(AccountID const &id, SeqProxy const &seq) noexcept
An offer from an account.
Definition Indexes.cpp:276
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
void nOffers(Env &env, std::size_t n, Account const &account, STAmount const &in, STAmount const &out)
static NoneT const kNone
Definition tags.h:9
auto const kMptDexFlags
Definition mpt.h:45
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)
json::Value regkey(Account const &account, DisabledT)
Disable the regular key.
Definition regkey.cpp:13
bool expectOffers(Env &env, AccountID const &account, std::uint16_t size, std::vector< Amounts > const &toMatch)
STPathElement allPathElements(AccountID const &a, Asset const &asset)
void testHelper2TokensMix(TTester &&tester)
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
json::Value noop(Account const &account)
The null transaction.
Definition noop.h:14
bool same(STPathSet const &st1, Args const &... args)
XRPAmount txFee(Env const &env, std::uint16_t n)
std::tuple< STPathSet, STAmount, STAmount > findPaths(jtx::Env &env, jtx::Account const &src, jtx::Account const &dst, STAmount const &saDstAmount, std::optional< STAmount > const &saSendMax, std::optional< PathAsset > const &srcAsset, std::optional< AccountID > const &srcIssuer, std::optional< uint256 > const &domain)
json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
Definition flags.h:110
FeatureBitset testableAmendments()
Definition Env.h:92
void testHelper3TokensMix(TTester &&tester)
STPathElement cpe(PathAsset const &pa)
std::tuple< STPathSet, STAmount, STAmount > findPathsByElement(jtx::Env &env, jtx::Account const &src, jtx::Account const &dst, STAmount const &saDstAmount, std::optional< STAmount > const &saSendMax, std::optional< STPathElement > const &srcElement, std::optional< AccountID > const &srcIssuer, std::optional< uint256 > const &domain)
STPathElement ipe(Asset const &asset)
static auto gAmmmpt
Definition AMMTest.h:45
bool equal(STAmount const &sa1, STAmount const &sa2)
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
json::Value ledgerEntryRoot(Env &env, Account const &acct)
OwnerCount< ltOFFER > offers
Match the number of offers in the account's owner directory.
Definition owners.h:137
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
STPath stpath(Args const &... args)
json::Value signers(Account const &account, std::uint32_t quorum, std::vector< Signer > const &v)
Definition multisign.cpp:31
bool checkArraySize(json::Value const &val, unsigned int size)
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
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
BEAST_DEFINE_TESTSUITE_PRIO(AccountDelete, app, xrpl, 2)
bool isOffer(jtx::Env &env, jtx::Account const &account, STAmount const &takerPays, STAmount const &takerGets)
An offer exists.
Definition PathSet.h:60
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
TAmounts< STAmount, STAmount > Amounts
Definition Quality.h:69
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:95
Seed generateSeed(std::string const &passPhrase)
Generate a seed deterministically.
Definition Seed.cpp:58
TER offerDelete(ApplyView &view, SLE::ref sle, beast::Journal j)
Delete an offer.
Currency const & xrpCurrency()
XRP currency.
Definition UintTypes.cpp:99
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
StrandResult< TInAmt, TOutAmt > flow(PaymentSandbox const &baseView, Strand const &strand, std::optional< TInAmt > const &maxIn, TOutAmt const &out, beast::Journal j)
Request out amount from a strand.
Definition StrandFlow.h:103
@ TapNone
Definition ApplyView.h:28
@ temBAD_PATH
Definition TER.h:84
@ temBAD_SEND_XRP_PATHS
Definition TER.h:91
@ temBAD_SEND_XRP_MAX
Definition TER.h:88
@ temBAD_PATH_LOOP
Definition TER.h:85
@ temBAD_AMOUNT
Definition TER.h:77
AccountID const & xrpAccount()
Compute AccountID from public key.
@ tecLOCKED
Definition TER.h:361
@ tecPATH_PARTIAL
Definition TER.h:285
@ tecPATH_DRY
Definition TER.h:297
@ tecNO_AUTH
Definition TER.h:303
@ tecUNFUNDED_OFFER
Definition TER.h:287
@ tecKILLED
Definition TER.h:319
@ tecNO_PERMISSION
Definition TER.h:308
@ tesSUCCESS
Definition TER.h:245
Tests of AMM MPT that use offers.
void testOfferCrossWithXRP(FeatureBitset features)
void testSellFlagBasic(FeatureBitset features)
void testSellWithFillOrKill(FeatureBitset features)
void testBridgedCross(FeatureBitset features)
void testCrossCurrencyEndXRP(FeatureBitset features)
void testCurrencyConversionEntire(FeatureBitset features)
void testTxMultisign(FeatureBitset features)
void testDirectToDirectPath(FeatureBitset features)
void testOffersWhenLocked(FeatureBitset features)
void testFalseDry(FeatureBitset features)
void testCrossCurrencyBridged(FeatureBitset features)
void testOfferFeesConsumeFunds(FeatureBitset features)
void testRequireAuth(FeatureBitset features)
void testCurrencyConversionInParts(FeatureBitset features)
void testOfferCreateThenCross(FeatureBitset features)
void run() override
Runs the suite.
void testStepLimit(FeatureBitset features)
void testSellFlagExceedLimit(FeatureBitset features)
void testFillModes(FeatureBitset features)
void testPayment(FeatureBitset features)
void testSelfIssueOffer(FeatureBitset features)
void testOfferCrossWithLimitOverride(FeatureBitset features)
void testTransferRateOffer(FeatureBitset features)
void testRmFundedOffer(FeatureBitset features)
void testLoop(FeatureBitset features)
void testConvertAllOfAnAsset(FeatureBitset features)
void testCrossCurrencyStartXRP(FeatureBitset features)
void testToStrand(FeatureBitset features)
void testGlobalLock(FeatureBitset features)
void testTransferRateNoOwnerFee(FeatureBitset features)
void testMissingAuth(FeatureBitset features)
void testGatewayCrossCurrency(FeatureBitset features)
void testRIPD1373(FeatureBitset features)
void testIndividualLock(FeatureBitset features)
void testBookStep(FeatureBitset features)
Represents an XRP, IOU, or MPT quantity This customizes the string conversion and supports XRP conver...
T tie(T... args)