xrpld
Loading...
Searching...
No Matches
OfferMPT_test.cpp
1#include <test/jtx/Env.h>
2#include <test/jtx/PathSet.h>
3#include <test/jtx/TestHelpers.h>
4#include <test/jtx/WSClient.h>
5#include <test/jtx/acctdelete.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/balance.h>
8#include <test/jtx/fee.h>
9#include <test/jtx/jtx_json.h>
10#include <test/jtx/mpt.h>
11#include <test/jtx/noop.h>
12#include <test/jtx/offer.h>
13#include <test/jtx/owners.h>
14#include <test/jtx/paths.h>
15#include <test/jtx/require.h>
16#include <test/jtx/sendmax.h>
17#include <test/jtx/tags.h>
18#include <test/jtx/ter.h>
19#include <test/jtx/ticket.h>
20#include <test/jtx/txflags.h>
21
22#include <xrpl/beast/unit_test/suite.h>
23#include <xrpl/core/ServiceRegistry.h>
24#include <xrpl/json/json_value.h>
25#include <xrpl/ledger/helpers/DirectoryHelpers.h>
26#include <xrpl/protocol/AccountID.h>
27#include <xrpl/protocol/Feature.h>
28#include <xrpl/protocol/Indexes.h>
29#include <xrpl/protocol/Issue.h>
30#include <xrpl/protocol/LedgerFormats.h>
31#include <xrpl/protocol/MPTIssue.h>
32#include <xrpl/protocol/Protocol.h>
33#include <xrpl/protocol/SField.h>
34#include <xrpl/protocol/STAmount.h>
35#include <xrpl/protocol/Seed.h>
36#include <xrpl/protocol/TER.h>
37#include <xrpl/protocol/TxFlags.h>
38#include <xrpl/protocol/UintTypes.h>
39#include <xrpl/protocol/XRPAmount.h>
40#include <xrpl/protocol/jss.h>
41
42#include <algorithm>
43#include <cstddef>
44#include <cstdint>
45#include <functional>
46#include <map>
47#include <optional>
48#include <string>
49#include <type_traits>
50#include <utility>
51#include <vector>
52
53namespace xrpl::test {
54
56{
57 static XRPAmount
59 {
60 return env.current()->fees().accountReserve(count);
61 }
62
63 static std::uint32_t
65 {
66 return env.current()->header().parentCloseTime.time_since_epoch().count();
67 }
68
69public:
70 void
72 {
73 testcase("Incorrect Removal of Funded Offers");
74
75 // We need at least two paths. One at good quality and one at bad
76 // quality. The bad quality path needs two offer books in a row.
77 // Each offer book should have two offers at the same quality, the
78 // offers should be completely consumed, and the payment should
79 // require both offers to be satisfied. The first offer must
80 // be "taker gets" XRP. Old, broken would remove the first
81 // "taker gets" xrp offer, even though the offer is still funded and
82 // not used for the payment.
83
84 using namespace jtx;
85 auto const gw = Account{"gateway"};
86 Account const alice{"alice"};
87 Account const bob{"bob"};
88 Account const carol{"carol"};
89
90 auto test = [&](auto&& issue1, auto&& issue2) {
91 Env env{*this, features};
92
93 env.fund(XRP(10'000), alice, bob, carol, gw);
94 env.close();
95
96 auto const usd =
97 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob, carol}});
98 auto const btc =
99 issue2({.env = env, .token = "BTC", .issuer = gw, .holders = {alice, bob, carol}});
100
101 env(pay(gw, alice, btc(1'000)));
102
103 env(pay(gw, carol, usd(1'000)));
104 env(pay(gw, carol, btc(1'000)));
105
106 // Must be two offers at the same quality
107 // "taker gets" must be XRP
108 // (Different amounts, so I can distinguish the offers)
109 env(offer(carol, btc(49), XRP(49)));
110 env(offer(carol, btc(51), XRP(51)));
111
112 // Offers for the poor quality path
113 // Must be two offers at the same quality
114 env(offer(carol, XRP(50), usd(50)));
115 env(offer(carol, XRP(50), usd(50)));
116
117 // Offers for the good quality path
118 env(offer(carol, btc(1), usd(100)));
119
120 PathSet const paths(TestPath(XRP, usd), TestPath(usd));
121
122 env(pay(alice, bob, usd(100)),
123 Json(paths.json()),
124 Sendmax(btc(1'000)),
125 Txflags(tfPartialPayment));
126
127 env.require(Balance(bob, usd(100)));
128 BEAST_EXPECT(
129 !isOffer(env, carol, btc(1), usd(100)) && isOffer(env, carol, btc(49), XRP(49)));
130 };
132 }
133
134 void
136 {
137 testcase("Removing Canceled Offers");
138
139 using namespace jtx;
140 Env env{*this, features};
141
142 auto const gw = Account{"gateway"};
143 auto const alice = Account{"alice"};
144
145 env.fund(XRP(10'000), alice, gw);
146 env.close();
147
148 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
149
150 env(pay(gw, alice, usd(50)));
151 env.close();
152
153 auto const offer1Seq = env.seq(alice);
154
155 env(offer(alice, XRP(500), usd(100)), Require(offers(alice, 1)));
156 env.close();
157
158 BEAST_EXPECT(isOffer(env, alice, XRP(500), usd(100)));
159
160 // cancel the offer above and replace it with a new offer
161 auto const offer2Seq = env.seq(alice);
162
163 env(offer(alice, XRP(300), usd(100)),
164 Json(jss::OfferSequence, offer1Seq),
165 Require(offers(alice, 1)));
166 env.close();
167
168 BEAST_EXPECT(
169 isOffer(env, alice, XRP(300), usd(100)) && !isOffer(env, alice, XRP(500), usd(100)));
170
171 // Test canceling non-existent offer.
172 // auto const offer3Seq = env.seq (alice);
173
174 env(offer(alice, XRP(400), usd(200)),
175 Json(jss::OfferSequence, offer1Seq),
176 Require(offers(alice, 2)));
177 env.close();
178
179 BEAST_EXPECT(
180 isOffer(env, alice, XRP(300), usd(100)) && isOffer(env, alice, XRP(400), usd(200)));
181
182 // Test cancellation now with OfferCancel tx
183 auto const offer4Seq = env.seq(alice);
184 env(offer(alice, XRP(222), usd(111)), Require(offers(alice, 3)));
185 env.close();
186
187 BEAST_EXPECT(isOffer(env, alice, XRP(222), usd(111)));
188 env(offerCancel(alice, offer4Seq));
189 env.close();
190 BEAST_EXPECT(env.seq(alice) == offer4Seq + 2);
191
192 BEAST_EXPECT(!isOffer(env, alice, XRP(222), usd(111)));
193
194 // Create an offer that both fails with a tecEXPIRED code and removes
195 // an offer. Show that the attempt to remove the offer fails.
196 env.require(offers(alice, 2));
197
198 env(offer(alice, XRP(5), usd(2)),
199 Json(sfExpiration.fieldName, lastClose(env)),
200 Json(jss::OfferSequence, offer2Seq),
201 Ter(TER{tecEXPIRED}));
202 env.close();
203
204 env.require(offers(alice, 2));
205 BEAST_EXPECT(isOffer(env, alice, XRP(300), usd(100))); // offer2
206 BEAST_EXPECT(!isOffer(env, alice, XRP(5), usd(2))); // expired
207 }
208
209 void
211 {
212 testcase("Tiny payments");
213
214 // Regression test for tiny payments
215 using namespace jtx;
216 using namespace std::chrono_literals;
217 auto const alice = Account{"alice"};
218 auto const bob = Account{"bob"};
219 auto const carol = Account{"carol"};
220 auto const gw = Account{"gw"};
221
222 auto test = [&](auto&& issue1, auto&& issue2) {
223 Env env{*this, features};
224
225 env.fund(XRP(10'000), alice, bob, carol, gw);
226 env.close();
227
228 auto const usd = issue1(
229 {.env = env,
230 .token = "USD",
231 .issuer = gw,
232 .holders = {alice, bob, carol},
233 .limit = 400'000'000});
234 auto const eur = issue2(
235 {.env = env,
236 .token = "EUR",
237 .issuer = gw,
238 .holders = {alice, bob, carol},
239 .limit = 400'000'000});
240
241 env(pay(gw, alice, usd(100'000'000)));
242 env(pay(gw, carol, eur(100'000'000)));
243
244 // Create more offers than the loop max count in DeliverNodeReverse
245 // Note: the DeliverNodeReverse code has been removed; however since
246 // this is a regression test the original test is being left as-is
247 // for now.
248 for (int i = 0; i < 101; ++i)
249 env(offer(carol, usd(1'000'000), eur(2'000'000)));
250
251 // Original Offer test sends EUR(10**-81). MPT is integral,
252 // therefore and integral value is sent respecting the exchange
253 // rate. I.e. if EUR(1) is sent then it'll result in USD(0).
254 env(pay(alice, bob, eur(2)), Path(~eur), Sendmax(usd(100)));
255 };
257 }
258
259 void
261 {
262 testcase("XRP Tiny payments");
263
264 // Regression test for tiny xrp payments
265 // In some cases, when the payment code calculates
266 // the amount of xrp needed as input to an xrp->iou offer
267 // it would incorrectly round the amount to zero (even when
268 // round-up was set to true).
269 // The bug would cause funded offers to be incorrectly removed
270 // because the code thought they were unfunded.
271 // The conditions to trigger the bug are:
272 // 1) When we calculate the amount of input xrp needed for an offer
273 // from xrp->iou, the amount is less than 1 drop (after rounding
274 // up the float representation).
275 // 2) There is another offer in the same book with a quality
276 // sufficiently bad that when calculating the input amount
277 // needed the amount is not set to zero.
278
279 using namespace jtx;
280 using namespace std::chrono_literals;
281 auto const alice = Account{"alice"};
282 auto const bob = Account{"bob"};
283 auto const carol = Account{"carol"};
284 auto const dan = Account{"dan"};
285 auto const erin = Account{"erin"};
286 auto const gw = Account{"gw"};
287
288 Env env{*this, features};
289
290 env.fund(XRP(10'000), alice, bob, carol, dan, erin, gw);
291 env.close();
292
293 MPT const usd = MPTTester(
294 {.env = env,
295 .issuer = gw,
296 .holders = {alice, bob, carol, dan, erin},
297 .pay = std::nullopt});
298 env(pay(gw, carol, usd(99'999)));
299 env(pay(gw, dan, usd(100'000)));
300 env(pay(gw, erin, usd(100'000)));
301 env.close();
302
303 // Carol doesn't quite have enough funds for this offer
304 // The amount left after this offer is taken will cause
305 // STAmount to incorrectly round to zero when the next offer
306 // (at a good quality) is considered. (when the now removed
307 // stAmountCalcSwitchover2 patch was inactive)
308 env(offer(carol, drops(1), usd(99'999)));
309 // Offer at a quality poor enough so when the input xrp is
310 // calculated in the reverse pass, the amount is not zero.
311 env(offer(dan, XRP(100), usd(1)));
312
313 env.close();
314 // This is the funded offer that will be incorrectly removed.
315 // It is considered after the offer from carol, which leaves a
316 // tiny amount left to pay. When calculating the amount of xrp
317 // needed for this offer, it will incorrectly compute zero in both
318 // the forward and reverse passes (when the now removed
319 // stAmountCalcSwitchover2 was inactive.)
320 env(offer(erin, drops(2), usd(100'000)));
321
322 env(pay(alice, bob, usd(100'000)),
323 Path(~usd),
324 Sendmax(XRP(102)),
325 Txflags(tfNoRippleDirect | tfPartialPayment));
326
327 env.require(offers(carol, 0), offers(dan, 1));
328
329 // offer was correctly consumed. There is still some
330 // liquidity left on that offer.
331 env.require(Balance(erin, usd(99'999)), offers(erin, 1));
332 }
333
334 void
336 {
337 testcase("Rm small increased q offers XRP");
338
339 // Carol places an offer, but cannot fully fund the offer. When her
340 // funding is taken into account, the offer's quality drops below its
341 // initial quality and has an input amount of 1 drop. This is removed as
342 // an offer that may block offer books.
343
344 using namespace jtx;
345 using namespace std::chrono_literals;
346 auto const alice = Account{"alice"};
347 auto const bob = Account{"bob"};
348 auto const carol = Account{"carol"};
349 auto const gw = Account{"gw"};
350
351 // Test offer crossing
352 for (auto crossBothOffers : {false, true})
353 {
354 Env env{*this, features};
355
356 env.fund(XRP(10'000), alice, bob, carol, gw);
357
358 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
359 // underfund carol's offer
360 auto initialCarolUSD = usd(499);
361 env(pay(gw, carol, initialCarolUSD));
362 env(pay(gw, bob, usd(100'000)));
363 env.close();
364 // This offer is underfunded
365 env(offer(carol, drops(1), usd(1'000)));
366 env.close();
367 // offer at a lower quality
368 env(offer(bob, drops(2), usd(1'000), tfPassive));
369 env.close();
370 env.require(offers(bob, 1), offers(carol, 1));
371
372 // alice places an offer that crosses carol's; depending on
373 // "crossBothOffers" it may cross bob's as well
374 auto aliceTakerGets = crossBothOffers ? drops(2) : drops(1);
375 env(offer(alice, usd(1'000), aliceTakerGets));
376 env.close();
377
378 env.require(
379 offers(carol, 0),
380 Balance(
381 carol,
382 initialCarolUSD)); // offer is removed but not taken
383 if (crossBothOffers)
384 {
385 env.require(
386 offers(alice, 0), Balance(alice, usd(1'000))); // alice's offer is crossed
387 }
388 else
389 {
390 env.require(
391 offers(alice, 1), Balance(alice, usd(0))); // alice's offer is not crossed
392 }
393 }
394
395 // Test payments
396 for (auto partialPayment : {false, true})
397 {
398 Env env{*this, features};
399
400 env.fund(XRP(10'000), alice, bob, carol, gw);
401 env.close();
402
403 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
404 auto const initialCarolUSD = usd(999);
405 env(pay(gw, carol, initialCarolUSD));
406 env.close();
407 env(pay(gw, bob, usd(100'000)));
408 env.close();
409 env(offer(carol, drops(1), usd(1'000)));
410 env.close();
411 env(offer(bob, drops(2), usd(2'000), tfPassive));
412 env.close();
413 env.require(offers(bob, 1), offers(carol, 1));
414
415 std::uint32_t const flags =
416 partialPayment ? (tfNoRippleDirect | tfPartialPayment) : tfNoRippleDirect;
417
418 TER const expectedTer = partialPayment ? TER{tesSUCCESS} : TER{tecPATH_PARTIAL};
419
420 env(pay(alice, bob, usd(5'000)),
421 Path(~usd),
422 Sendmax(XRP(1)),
423 Txflags(flags),
424 Ter(expectedTer));
425 env.close();
426
427 if (expectedTer == tesSUCCESS)
428 {
429 env.require(offers(carol, 0));
430 env.require(Balance(carol,
431 initialCarolUSD)); // offer is removed but not taken
432 }
433 else
434 {
435 // TODO: Offers are not removed when payments fail
436 // If that is addressed, the test should show that carol's
437 // offer is removed but not taken, as in the other branch of
438 // this if statement
439 }
440 }
441 }
442
443 void
445 {
446 testcase("Rm small increased q offers MPT");
447
448 // Carol places an offer, but cannot fully fund the offer. When her
449 // funding is taken into account, the offer's quality drops below its
450 // initial quality and has an input amount of 1 drop. This is removed as
451 // an offer that may block offer books.
452
453 using namespace jtx;
454 using namespace std::chrono_literals;
455 auto const alice = Account{"alice"};
456 auto const bob = Account{"bob"};
457 auto const carol = Account{"carol"};
458 auto const gw = Account{"gw"};
459
460 auto test = [&](auto&& issue1, auto&& issue2) {
461 auto tinyAmount = [&]<typename T>(T const& token) -> PrettyAmount {
462 if constexpr (std::is_same_v<T, IOU>)
463 {
464 STAmount const amt(
465 token,
466 /*mantissa*/ 1,
467 /*exponent*/ -81);
468 return PrettyAmount(amt, token.account.name());
469 }
470 else
471 {
472 STAmount const amt(
473 token,
474 /*mantissa*/ 1,
475 /*exponent*/ 0);
476 return PrettyAmount(amt, "MPT");
477 }
478 };
479
480 // Test offer crossing
481 for (auto crossBothOffers : {false, true})
482 {
483 Env env{*this, features};
484
485 env.fund(XRP(10'000), alice, bob, carol, gw);
486 env.close();
487
488 auto const usd = issue1(
489 {.env = env,
490 .token = "USD",
491 .issuer = gw,
492 .holders = {alice, bob, carol},
493 .limit = 100'000'000});
494 auto const eur = issue2(
495 {.env = env,
496 .token = "EUR",
497 .issuer = gw,
498 .holders = {alice, bob, carol},
499 .limit = 100'000'000});
500 // underfund carol's offer
501 auto initialCarolUSD = tinyAmount(usd);
502 env(pay(gw, carol, initialCarolUSD));
503 env(pay(gw, bob, usd(100'000)));
504 env(pay(gw, alice, eur(100'000)));
505 env.close();
506 // This offer is underfunded
507 env(offer(carol, eur(10), usd(10'000)));
508 env.close();
509 // offer at a lower quality
510 env(offer(bob, eur(10), usd(5'000), tfPassive));
511 env.close();
512 env.require(offers(bob, 1), offers(carol, 1));
513
514 // alice places an offer that crosses carol's; depending on
515 // "crossBothOffers" it may cross bob's as well
516 // Whatever
517 auto aliceTakerGets = crossBothOffers ? eur(2) : eur(1);
518 env(offer(alice, usd(1'000), aliceTakerGets));
519 env.close();
520
521 // carol's offer can be partially crossed when EUR is IOU:
522 // 10e-3EUR/1USD
523 using tEUR = std::decay_t<decltype(eur)>;
524 static constexpr bool kIsEuriou = std::is_same_v<tEUR, IOU>;
525 // partially crossed if IOU, removed but not taken if MPT
526 auto const balanceCarolUSD = kIsEuriou ? usd(0) : initialCarolUSD;
527
528 env.require(offers(carol, 0), Balance(carol, balanceCarolUSD));
529 if (crossBothOffers)
530 {
531 env.require(
532 offers(alice, 0), Balance(alice, usd(1'000))); // alice's offer is crossed
533 }
534 else
535 {
536 // partially crossed if IOU, not crossed if MPT
537 auto const balanceAliceUSD = kIsEuriou ? usd(1) : usd(0);
538 env.require(offers(alice, 1), Balance(alice, balanceAliceUSD));
539 }
540 }
541
542 // Test payments
543 for (auto partialPayment : {false, true})
544 {
545 Env env{*this, features};
546
547 env.fund(XRP(10'000), alice, bob, carol, gw);
548 env.close();
549
550 auto const usd = issue1(
551 {.env = env,
552 .token = "USD",
553 .issuer = gw,
554 .holders = {alice, bob, carol},
555 .limit = 100'000'000});
556 auto const eur = issue2(
557 {.env = env,
558 .token = "EUR",
559 .issuer = gw,
560 .holders = {alice, bob, carol},
561 .limit = 100'000'000});
562 // underfund carol's offer
563 auto const initialCarolUSD = tinyAmount(usd);
564 env(pay(gw, carol, initialCarolUSD));
565 env(pay(gw, bob, usd(100'000)));
566 env(pay(gw, alice, eur(100'000)));
567 env.close();
568 // This offer is underfunded
569 env(offer(carol, eur(10), usd(2'000)));
570 env.close();
571 env(offer(bob, eur(20), usd(4'000), tfPassive));
572 env.close();
573 env.require(offers(bob, 1), offers(carol, 1));
574
575 std::uint32_t const flags =
576 partialPayment ? (tfNoRippleDirect | tfPartialPayment) : tfNoRippleDirect;
577
578 TER const expectedTer = partialPayment ? TER{tesSUCCESS} : TER{tecPATH_PARTIAL};
579
580 env(pay(alice, bob, usd(5'000)),
581 Path(~usd),
582 Sendmax(eur(100)),
583 Txflags(flags),
584 Ter(expectedTer));
585 env.close();
586
587 if (expectedTer == tesSUCCESS)
588 {
589 // carol's offer can be partially crossed when EUR is IOU:
590 // 10e-3EUR/1USD
591 using tEUR = std::decay_t<decltype(eur)>;
592 static constexpr bool kIsEuriou = std::is_same_v<tEUR, IOU>;
593 // partially crossed if IOU, removed but not taken if MPT
594 auto const balanceCarolUSD = kIsEuriou ? usd(0) : initialCarolUSD;
595 env.require(offers(carol, 0));
596 env.require(Balance(carol, balanceCarolUSD));
597 }
598 else
599 {
600 // TODO: Offers are not removed when payments fail
601 // If that is addressed, the test should show that carol's
602 // offer is removed but not taken, as in the other branch of
603 // this if statement
604 }
605 }
606 };
608 }
609
610 void
612 {
613 testcase("Insufficient Reserve");
614
615 // If an account places an offer and its balance
616 // *before* the transaction began isn't high enough
617 // to meet the reserve *after* the transaction runs,
618 // then no offer should go on the books but if the
619 // offer partially or fully crossed the tx succeeds.
620
621 using namespace jtx;
622
623 auto const gw = Account{"gateway"};
624 auto const alice = Account{"alice"};
625 auto const bob = Account{"bob"};
626 auto const carol = Account{"carol"};
627
628 auto const xrpOffer = XRP(1'000);
629
630 // No crossing:
631 {
632 Env env{*this, features};
633
634 env.fund(XRP(1'000'000), gw);
635
636 auto const f = env.current()->fees().base;
637 auto const r = reserve(env, 0);
638
639 env.fund(r + f, alice);
640
641 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
642
643 auto const usdOffer = usd(1'000);
644
645 env(pay(gw, alice, usdOffer), Ter(tesSUCCESS));
646 env(offer(alice, xrpOffer, usdOffer), Ter(tecINSUF_RESERVE_OFFER));
647
648 env.require(Balance(alice, r - f), Owners(alice, 1));
649 }
650
651 // Partial cross:
652 {
653 Env env{*this, features};
654
655 env.fund(XRP(1'000'000), gw);
656
657 auto const f = env.current()->fees().base;
658 auto const r = reserve(env, 0);
659
660 env.fund(r + f, alice);
661 env.fund(r + 2 * f + xrpOffer, bob);
662
663 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
664
665 auto const usdOffer = usd(1'000);
666 auto const usdOffer2 = usd(500);
667 auto const xrpOffer2 = XRP(500);
668
669 env(offer(bob, usdOffer2, xrpOffer2), Ter(tesSUCCESS));
670
671 env(pay(gw, alice, usdOffer), Ter(tesSUCCESS));
672 env(offer(alice, xrpOffer, usdOffer), Ter(tesSUCCESS));
673
674 env.require(
675 Balance(alice, r - f + xrpOffer2),
676 Balance(alice, usdOffer2),
677 Owners(alice, 1),
678 Balance(bob, r + xrpOffer2),
679 Balance(bob, usdOffer2),
680 Owners(bob, 1));
681 }
682
683 // Account has enough reserve as is, but not enough
684 // if an offer were added. Attempt to sell MPTs to
685 // buy XRP. If it fully crosses, we succeed.
686 {
687 Env env{*this, features};
688
689 env.fund(XRP(1'000'000), gw);
690
691 auto const f = env.current()->fees().base;
692 auto const r = reserve(env, 0);
693
694 env.fund(r + f, alice);
695
696 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
697
698 auto const usdOffer = usd(1'000);
699 auto const usdOffer2 = usd(500);
700 auto const xrpOffer2 = XRP(500);
701
702 env.fund(r + f + xrpOffer, bob, carol);
703 env(offer(bob, usdOffer2, xrpOffer2), Ter(tesSUCCESS));
704 env(offer(carol, usdOffer, xrpOffer), Ter(tesSUCCESS));
705
706 env(pay(gw, alice, usdOffer), Ter(tesSUCCESS));
707 env(offer(alice, xrpOffer, usdOffer), Ter(tesSUCCESS));
708
709 env.require(
710 Balance(alice, r - f + xrpOffer),
711 Balance(alice, usd(0)),
712 Owners(alice, 1),
713 Balance(bob, r + xrpOffer2),
714 Balance(bob, usdOffer2),
715 Owners(bob, 1),
716 Balance(carol, r + xrpOffer2),
717 Balance(carol, usdOffer2),
718 Owners(carol, 2));
719 }
720 }
721
722 // Helper function that returns the Offers on an account.
725 {
727 forEachItem(*env.current(), account, [&result](SLE::const_ref sle) {
728 if (sle->getType() == ltOFFER)
729 result.push_back(sle);
730 });
731 return result;
732 }
733
734 void
736 {
737 testcase("Fill Modes");
738
739 using namespace jtx;
740
741 auto const startBalance = XRP(1'000'000);
742 auto const gw = Account{"gateway"};
743 auto const alice = Account{"alice"};
744 auto const bob = Account{"bob"};
745
746 // Fill or Kill - unless we fully cross, just charge a fee and don't
747 // place the offer on the books. But also clean up expired offers
748 // that are discovered along the way.
749 //
750 {
751 Env env{*this, features};
752
753 auto const f = env.current()->fees().base;
754
755 env.fund(startBalance, gw, alice, bob);
756
757 MPTTester musd({.env = env, .issuer = gw});
758 MPT const usd = musd["USD"];
759
760 // bob creates an offer that expires before the next ledger close.
761 env(offer(bob, usd(500), XRP(500)),
762 Json(sfExpiration.fieldName, lastClose(env) + 1),
763 Ter(tesSUCCESS));
764
765 // The offer expires (it's not removed yet).
766 env.close();
767 env.require(Owners(bob, 1), offers(bob, 1));
768
769 // bob creates the offer that will be crossed.
770 env(offer(bob, usd(500), XRP(500)), Ter(tesSUCCESS));
771 env.close();
772 env.require(Owners(bob, 2), offers(bob, 2));
773
774 musd.authorize({.account = alice});
775 env(pay(gw, alice, usd(1'000)), Ter(tesSUCCESS));
776
777 // Order that can't be filled but will remove bob's expired offer:
778 env(offer(alice, XRP(1'000), usd(1'000)), Txflags(tfFillOrKill), Ter(tecKILLED));
779
780 env.require(
781 Balance(alice, startBalance - (f * 2)),
782 Balance(alice, usd(1'000)),
783 Owners(alice, 1),
784 offers(alice, 0),
785 Balance(bob, startBalance - (f * 2)),
786 Balance(bob, usd(kNone)),
787 Owners(bob, 1),
788 offers(bob, 1));
789
790 // Order that can be filled
791 env(offer(alice, XRP(500), usd(500)), Txflags(tfFillOrKill), Ter(tesSUCCESS));
792
793 env.require(
794 Balance(alice, startBalance - (f * 3) + XRP(500)),
795 Balance(alice, usd(500)),
796 Owners(alice, 1),
797 offers(alice, 0),
798 Balance(bob, startBalance - (f * 2) - XRP(500)),
799 Balance(bob, usd(500)),
800 Owners(bob, 1),
801 offers(bob, 0));
802 }
803
804 // Immediate or Cancel - cross as much as possible
805 // and add nothing on the books:
806 {
807 Env env{*this, features};
808
809 auto const f = env.current()->fees().base;
810
811 env.fund(startBalance, gw, alice, bob);
812
813 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
814
815 env(pay(gw, alice, usd(1'000)), Ter(tesSUCCESS));
816
817 // No cross:
818 {
819 env(offer(alice, XRP(1'000), usd(1000)),
820 Txflags(tfImmediateOrCancel),
821 Ter(tecKILLED));
822 }
823
824 env.require(
825 Balance(alice, startBalance - f - f),
826 Balance(alice, usd(1000)),
827 Owners(alice, 1),
828 offers(alice, 0));
829
830 // Partially cross:
831 env(offer(bob, usd(50), XRP(50)), Ter(tesSUCCESS));
832 env(offer(alice, XRP(1000), usd(1000)), Txflags(tfImmediateOrCancel), Ter(tesSUCCESS));
833
834 env.require(
835 Balance(alice, startBalance - f - f - f + XRP(50)),
836 Balance(alice, usd(950)),
837 Owners(alice, 1),
838 offers(alice, 0),
839 Balance(bob, startBalance - f - XRP(50)),
840 Balance(bob, usd(50)),
841 Owners(bob, 1),
842 offers(bob, 0));
843
844 // Fully cross:
845 env(offer(bob, usd(50), XRP(50)), Ter(tesSUCCESS));
846 env(offer(alice, XRP(50), usd(50)), Txflags(tfImmediateOrCancel), Ter(tesSUCCESS));
847
848 env.require(
849 Balance(alice, startBalance - f - f - f - f + XRP(100)),
850 Balance(alice, usd(900)),
851 Owners(alice, 1),
852 offers(alice, 0),
853 Balance(bob, startBalance - f - f - XRP(100)),
854 Balance(bob, usd(100)),
855 Owners(bob, 1),
856 offers(bob, 0));
857 }
858
859 // tfPassive -- place the offer without crossing it.
860 {
861 Env env(*this, features);
862
863 env.fund(startBalance, gw, alice, bob);
864 env.close();
865
866 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {bob}});
867
868 env(pay(gw, bob, usd(1'000)));
869 env.close();
870
871 env(offer(alice, usd(1'000), XRP(2'000)));
872 env.close();
873
874 auto const aliceOffers = offersOnAccount(env, alice);
875 BEAST_EXPECT(aliceOffers.size() == 1);
876 for (auto const& offerPtr : aliceOffers)
877 {
878 auto const& offer = *offerPtr;
879 BEAST_EXPECT(offer[sfTakerGets] == XRP(2'000));
880 BEAST_EXPECT(offer[sfTakerPays] == usd(1'000));
881 }
882
883 // bob creates a passive offer that could cross alice's.
884 // bob's offer should stay in the ledger.
885 env(offer(bob, XRP(2'000), usd(1'000), tfPassive));
886 env.close();
887 env.require(offers(alice, 1));
888
889 auto const bobOffers = offersOnAccount(env, bob);
890 BEAST_EXPECT(bobOffers.size() == 1);
891 for (auto const& offerPtr : bobOffers)
892 {
893 auto const& offer = *offerPtr;
894 BEAST_EXPECT(offer[sfTakerGets] == usd(1'000));
895 BEAST_EXPECT(offer[sfTakerPays] == XRP(2'000));
896 }
897
898 // It should be possible for gw to cross both of those offers.
899 env(offer(gw, XRP(2'000), usd(1'000)));
900 env.close();
901 env.require(offers(alice, 0));
902 env.require(offers(gw, 0));
903 env.require(offers(bob, 1));
904
905 env(offer(gw, usd(1'000), XRP(2'000)));
906 env.close();
907 env.require(offers(bob, 0));
908 env.require(offers(gw, 0));
909 }
910
911 // tfPassive -- cross only offers of better quality.
912 {
913 Env env(*this, features);
914
915 env.fund(startBalance, gw, "alice", "bob");
916 env.close();
917
918 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {bob}});
919
920 env(pay(gw, "bob", usd(10'000)));
921 env(offer("alice", usd(5'000), XRP(1'001)));
922 env.close();
923
924 env(offer("alice", usd(5'000), XRP(1'000)));
925 env.close();
926
927 auto const aliceOffers = offersOnAccount(env, "alice");
928 BEAST_EXPECT(aliceOffers.size() == 2);
929
930 // bob creates a passive offer. That offer should cross one
931 // of alice's (the one with better quality) and leave alice's
932 // other offer untouched.
933 env(offer("bob", XRP(2'000), usd(10'000), tfPassive));
934 env.close();
935 env.require(offers("alice", 1));
936
937 auto const bobOffers = offersOnAccount(env, "bob");
938 BEAST_EXPECT(bobOffers.size() == 1);
939 for (auto const& offerPtr : bobOffers)
940 {
941 auto const& offer = *offerPtr;
942 BEAST_EXPECT(offer[sfTakerGets] == usd(4'995));
943 BEAST_EXPECT(offer[sfTakerPays] == XRP(999));
944 }
945 }
946 }
947
948 void
950 {
951 testcase("Malformed Detection");
952
953 using namespace jtx;
954
955 auto const startBalance = XRP(1'000'000);
956 auto const gw = Account{"gateway"};
957 auto const alice = Account{"alice"};
958
959 Env env{*this, features};
960
961 env.fund(startBalance, gw, alice);
962
963 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
964
965 // Sell and buy the same asset
966 {
967 // Alice tries an MPT to MPT order:
968 env(pay(gw, alice, usd(1'000)), Ter(tesSUCCESS));
969 env(offer(alice, usd(1'000), usd(1'000)), Ter(temREDUNDANT));
970 env.require(Owners(alice, 1), offers(alice, 0));
971 }
972
973 // Offers with negative amounts
974 {
975 env(offer(alice, -usd(1'000), XRP(1'000)), Ter(temBAD_AMOUNT));
976 env.require(Owners(alice, 1), offers(alice, 0));
977 }
978
979 // Bad MPT
980 {
981 auto const bad = MPT(badMPT());
982
983 env(offer(alice, XRP(1'000), bad(1'000)), Ter(temBAD_CURRENCY));
984 env.require(Owners(alice, 1), offers(alice, 0));
985 }
986 }
987
988 void
990 {
991 testcase("Offer Expiration");
992
993 using namespace jtx;
994
995 auto const gw = Account{"gateway"};
996 auto const alice = Account{"alice"};
997 auto const bob = Account{"bob"};
998
999 auto const startBalance = XRP(1'000'000);
1000 auto const xrpOffer = XRP(1'000);
1001
1002 Env env{*this, features};
1003
1004 env.fund(startBalance, gw, alice, bob);
1005 env.close();
1006
1007 auto const f = env.current()->fees().base;
1008
1009 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
1010 auto const usdOffer = usd(1'000);
1011
1012 env(pay(gw, alice, usdOffer), Ter(tesSUCCESS));
1013 env.close();
1014 env.require(
1015 Balance(alice, startBalance - f),
1016 Balance(alice, usdOffer),
1017 offers(alice, 0),
1018 Owners(alice, 1));
1019
1020 // Place an offer that should have already expired.
1021 env(offer(alice, xrpOffer, usdOffer),
1022 Json(sfExpiration.fieldName, lastClose(env)),
1023 Ter(TER{tecEXPIRED}));
1024
1025 env.require(
1026 Balance(alice, startBalance - f - f),
1027 Balance(alice, usdOffer),
1028 offers(alice, 0),
1029 Owners(alice, 1));
1030 env.close();
1031
1032 // Add an offer that expires before the next ledger close
1033 env(offer(alice, xrpOffer, usdOffer),
1034 Json(sfExpiration.fieldName, lastClose(env) + 1),
1035 Ter(tesSUCCESS));
1036 env.require(
1037 Balance(alice, startBalance - f - f - f),
1038 Balance(alice, usdOffer),
1039 offers(alice, 1),
1040 Owners(alice, 2));
1041
1042 // The offer expires (it's not removed yet)
1043 env.close();
1044 env.require(
1045 Balance(alice, startBalance - f - f - f),
1046 Balance(alice, usdOffer),
1047 offers(alice, 1),
1048 Owners(alice, 2));
1049
1050 // Add offer - the expired offer is removed
1051 env(offer(bob, usdOffer, xrpOffer), Ter(tesSUCCESS));
1052
1053 env.require(
1054 Balance(alice, startBalance - f - f - f),
1055 Balance(alice, usdOffer),
1056 offers(alice, 0),
1057 Owners(alice, 1),
1058 Balance(bob, startBalance - f),
1059 Balance(bob, usd(kNone)),
1060 offers(bob, 1),
1061 Owners(bob, 1));
1062 }
1063
1064 void
1066 {
1067 testcase("Unfunded Crossing");
1068
1069 using namespace jtx;
1070
1071 auto const gw = Account{"gateway"};
1072
1073 auto const xrpOffer = XRP(1'000);
1074
1075 Env env{*this, features};
1076
1077 env.fund(XRP(1'000'000), gw);
1078
1079 // The fee that's charged for transactions
1080 auto const f = env.current()->fees().base;
1081
1082 // Account is at the reserve, and will dip below once
1083 // fees are subtracted.
1084 env.fund(reserve(env, 0), "alice");
1085 MPT const usd = MPTTester({.env = env, .issuer = gw});
1086 auto const usdOffer = usd(1'000);
1087 env(offer("alice", usdOffer, xrpOffer), Ter(tecUNFUNDED_OFFER));
1088 env.require(Balance("alice", reserve(env, 0) - f), Owners("alice", 0));
1089
1090 // Account has just enough for the reserve and the
1091 // fee.
1092 env.fund(reserve(env, 0) + f, "bob");
1093 env(offer("bob", usdOffer, xrpOffer), Ter(tecUNFUNDED_OFFER));
1094 env.require(Balance("bob", reserve(env, 0)), Owners("bob", 0));
1095
1096 // Account has enough for the reserve, the fee and
1097 // the offer, and a bit more, but not enough for the
1098 // reserve after the offer is placed.
1099 env.fund(reserve(env, 0) + f + XRP(1), "carol");
1100 env(offer("carol", usdOffer, xrpOffer), Ter(tecINSUF_RESERVE_OFFER));
1101 env.require(Balance("carol", reserve(env, 0) + XRP(1)), Owners("carol", 0));
1102
1103 // Account has enough for the reserve plus one
1104 // offer, and the fee.
1105 env.fund(reserve(env, 1) + f, "dan");
1106 env(offer("dan", usdOffer, xrpOffer), Ter(tesSUCCESS));
1107 env.require(Balance("dan", reserve(env, 1)), Owners("dan", 1));
1108
1109 // Account has enough for the reserve plus one
1110 // offer, the fee and the entire offer amount.
1111 env.fund(reserve(env, 1) + f + xrpOffer, "eve");
1112 env(offer("eve", usdOffer, xrpOffer), Ter(tesSUCCESS));
1113 env.require(Balance("eve", reserve(env, 1) + xrpOffer), Owners("eve", 1));
1114 }
1115
1116 void
1117 testSelfCross(bool usePartner, FeatureBitset features)
1118 {
1119 testcase(std::string("Self-crossing") + (usePartner ? ", with partner account" : ""));
1120
1121 using namespace jtx;
1122 auto const gw = Account{"gateway"};
1123 auto const partner = Account{"partner"};
1124
1125 auto test = [&](auto&& issue1, auto&& issue2) {
1126 Env env{*this, features};
1127 env.close();
1128
1129 env.fund(XRP(10'000), gw);
1130 auto const usd = issue1({.env = env, .token = "USD", .issuer = gw});
1131 auto const btc = issue2({.env = env, .token = "BTC", .issuer = gw});
1132 using tUSD = std::decay_t<decltype(usd)>;
1133 using tBTC = std::decay_t<decltype(btc)>;
1134 if (usePartner)
1135 {
1136 env.fund(XRP(10'000), partner);
1137 if constexpr (std::is_same_v<tUSD, IOU>)
1138 {
1139 env(trust(partner, usd(100)));
1140 }
1141 else
1142 {
1143 MPTTester musd(env, gw, usd);
1144 musd.authorize({.account = partner});
1145 }
1146 if constexpr (std::is_same_v<tBTC, IOU>)
1147 {
1148 env(trust(partner, btc(500)));
1149 }
1150 else
1151 {
1152 MPTTester mbtc(env, gw, btc);
1153 mbtc.authorize({.account = partner});
1154 }
1155 env(pay(gw, partner, usd(100)));
1156 env(pay(gw, partner, btc(500)));
1157 }
1158 auto const& accountToTest = usePartner ? partner : gw;
1159
1160 env.close();
1161 env.require(offers(accountToTest, 0));
1162
1163 // PART 1:
1164 // we will make two offers that can be used to bridge BTC to USD
1165 // through XRP
1166 env(offer(accountToTest, btc(250), XRP(1'000)));
1167 env.require(offers(accountToTest, 1));
1168
1169 // validate that the book now shows a BTC for XRP offer
1170 BEAST_EXPECT(isOffer(env, accountToTest, btc(250), XRP(1'000)));
1171
1172 auto const secondLegSeq = env.seq(accountToTest);
1173 env(offer(accountToTest, XRP(1'000), usd(50)));
1174 env.require(offers(accountToTest, 2));
1175
1176 // validate that the book also shows a XRP for USD offer
1177 BEAST_EXPECT(isOffer(env, accountToTest, XRP(1'000), usd(50)));
1178
1179 // now make an offer that will cross and auto-bridge, meaning
1180 // the outstanding offers will be taken leaving us with none
1181 env(offer(accountToTest, usd(50), btc(250)));
1182
1183 auto jrr = getBookOffers(env, usd, btc);
1184 BEAST_EXPECT(jrr[jss::offers].isArray());
1185 BEAST_EXPECT(jrr[jss::offers].size() == 0);
1186
1187 jrr = getBookOffers(env, btc, XRP);
1188 BEAST_EXPECT(jrr[jss::offers].isArray());
1189 BEAST_EXPECT(jrr[jss::offers].size() == 0);
1190
1191 // At this point, all offers are expected to be consumed.
1192 {
1193 auto acctOffers = offersOnAccount(env, accountToTest);
1194
1195 // No stale offers
1196 BEAST_EXPECT(acctOffers.empty());
1197 for (auto const& offerPtr : acctOffers)
1198 {
1199 auto const& offer = *offerPtr;
1200 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
1201 BEAST_EXPECT(offer[sfTakerGets] == usd(0));
1202 BEAST_EXPECT(offer[sfTakerPays] == XRP(0));
1203 }
1204 }
1205
1206 // cancel that lingering second offer so that it doesn't interfere
1207 // with the next set of offers we test. This will not be needed once
1208 // the bridging bug is fixed
1209 env(offerCancel(accountToTest, secondLegSeq));
1210 env.require(offers(accountToTest, 0));
1211
1212 // PART 2:
1213 // simple direct crossing BTC to USD and then USD to BTC which
1214 // causes the first offer to be replaced
1215 env(offer(accountToTest, btc(250), usd(50)));
1216 env.require(offers(accountToTest, 1));
1217
1218 // validate that the book shows one BTC for USD offer and no USD for
1219 // BTC offers
1220 BEAST_EXPECT(isOffer(env, accountToTest, btc(250), usd(50)));
1221
1222 jrr = getBookOffers(env, usd, btc);
1223 BEAST_EXPECT(jrr[jss::offers].isArray());
1224 BEAST_EXPECT(jrr[jss::offers].size() == 0);
1225
1226 // this second offer would self-cross directly, so it causes the
1227 // first offer by the same owner/taker to be removed
1228 env(offer(accountToTest, usd(50), btc(250)));
1229 env.require(offers(accountToTest, 1));
1230
1231 // validate that we now have just the second offer...the first
1232 // was removed
1233 jrr = getBookOffers(env, btc, usd);
1234 BEAST_EXPECT(jrr[jss::offers].isArray());
1235 BEAST_EXPECT(jrr[jss::offers].size() == 0);
1236
1237 BEAST_EXPECT(isOffer(env, accountToTest, usd(50), btc(250)));
1238 };
1240 }
1241
1242 void
1244 {
1245 // This test creates an offer test for negative balance
1246 // with transfer fees and miniscule funds.
1247 testcase("Negative Balance");
1248
1249 using namespace jtx;
1250 FeatureBitset const localFeatures = features | fixReducedOffersV2;
1251
1252 Env env{*this, localFeatures};
1253
1254 auto const gw = Account{"gateway"};
1255 auto const alice = Account{"alice"};
1256 auto const bob = Account{"bob"};
1257
1258 // these *interesting* amounts were taken
1259 // from the original JS test that was ported here
1260 auto const gwInitialBalance = drops(1'149'999'730);
1261 auto const aliceInitialBalance = drops(499'946'999'680);
1262 auto const bobInitialBalance = drops(10'199'999'920);
1263
1264 env.fund(gwInitialBalance, gw);
1265 env.fund(aliceInitialBalance, alice);
1266 env.fund(bobInitialBalance, bob);
1267
1268 MPTTester const musd(
1269 {.env = env, .issuer = gw, .holders = {alice, bob}, .transferFee = 5'000});
1270 MPT const usd = musd;
1271 auto const smallAmount = STAmount{usd, 1};
1272
1273 env(pay(gw, alice, usd(50)));
1274 env(pay(gw, bob, smallAmount));
1275
1276 env(offer(alice, usd(50), XRP(150'000)));
1277
1278 // unfund the offer
1279 env(pay(alice, gw, usd(50)));
1280
1281 // verify balances
1282 auto jrr = ledgerEntryMPT(env, alice, usd);
1283 // this represents 0 since MPTAmount is a default field
1284 BEAST_EXPECT(!jrr[jss::node].isMember(sfMPTAmount.fieldName));
1285
1286 jrr = ledgerEntryMPT(env, bob, usd);
1287 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "1");
1288
1289 // create crossing offer
1290 std::uint32_t const bobOfferSeq = env.seq(bob);
1291 env(offer(bob, XRP(2000), usd(1)));
1292
1293 // With the rounding introduced by fixReducedOffersV2, bob's
1294 // offer does not cross alice's offer and goes straight into
1295 // the ledger.
1296 jrr = ledgerEntryMPT(env, bob, usd);
1297 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "1");
1298
1299 json::Value const bobOffer = ledgerEntryOffer(env, bob, bobOfferSeq)[jss::node];
1300 BEAST_EXPECT(bobOffer[sfTakerGets.jsonName][jss::value] == "1");
1301 BEAST_EXPECT(bobOffer[sfTakerPays.jsonName] == "2000000000");
1302 }
1303
1304 void
1305 testOfferCrossWithXRP(bool reverseOrder, FeatureBitset features)
1306 {
1307 testcase(
1308 std::string("Offer Crossing with XRP, ") + (reverseOrder ? "Reverse" : "Normal") +
1309 " order");
1310
1311 using namespace jtx;
1312
1313 Env env{*this, features};
1314
1315 auto const gw = Account{"gateway"};
1316 auto const alice = Account{"alice"};
1317 auto const bob = Account{"bob"};
1318
1319 env.fund(XRP(10'000), gw, alice, bob);
1320
1321 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
1322
1323 env(pay(gw, alice, usd(500)));
1324
1325 if (reverseOrder)
1326 env(offer(bob, usd(1), XRP(4'000)));
1327
1328 env(offer(alice, XRP(150'000), usd(50)));
1329
1330 if (!reverseOrder)
1331 env(offer(bob, usd(1), XRP(4000)));
1332
1333 // Existing offer pays better than this wants.
1334 // Fully consume existing offer.
1335 // Pay 1 USD, get 4000 XRP.
1336
1337 auto jrr = ledgerEntryMPT(env, bob, usd);
1338 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "1");
1339 jrr = ledgerEntryRoot(env, bob);
1340 BEAST_EXPECT(
1341 jrr[jss::node][sfBalance.fieldName] ==
1342 to_string(
1343 (XRP(10000) - XRP(reverseOrder ? 4000 : 3000) - env.current()->fees().base * 2)
1344 .xrp()));
1345
1346 jrr = ledgerEntryMPT(env, alice, usd);
1347 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "499");
1348 jrr = ledgerEntryRoot(env, alice);
1349 BEAST_EXPECT(
1350 jrr[jss::node][sfBalance.fieldName] ==
1351 to_string(
1352 (XRP(10000) + XRP(reverseOrder ? 4000 : 3000) - env.current()->fees().base * 2)
1353 .xrp()));
1354 }
1355
1356 void
1358 {
1359 testcase("Offer Crossing with Limit Override");
1360
1361 using namespace jtx;
1362
1363 Env env{*this, features};
1364
1365 auto const gw = Account{"gateway"};
1366 auto const alice = Account{"alice"};
1367 auto const bob = Account{"bob"};
1368
1369 env.fund(XRP(100000), gw, alice, bob);
1370
1371 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
1372
1373 env(pay(gw, alice, usd(500)));
1374
1375 env(offer(alice, XRP(150'000), usd(50)));
1376 env(offer(bob, usd(1), XRP(3'000)));
1377
1378 auto jrr = ledgerEntryMPT(env, bob, usd);
1379 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "1");
1380 jrr = ledgerEntryRoot(env, bob);
1381 BEAST_EXPECT(
1382 jrr[jss::node][sfBalance.fieldName] ==
1383 to_string((XRP(100'000) - XRP(3'000) - env.current()->fees().base * 1).xrp()));
1384
1385 jrr = ledgerEntryMPT(env, alice, usd);
1386 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "499");
1387 jrr = ledgerEntryRoot(env, alice);
1388 BEAST_EXPECT(
1389 jrr[jss::node][sfBalance.fieldName] ==
1390 to_string((XRP(100'000) + XRP(3'000) - env.current()->fees().base * 2).xrp()));
1391 }
1392
1393 void
1395 {
1396 testcase("Offer Accept then Cancel.");
1397
1398 using namespace jtx;
1399
1400 Env env{*this, features};
1401
1402 MPT const usd = MPTTester({.env = env, .issuer = env.master});
1403
1404 auto const nextOfferSeq = env.seq(env.master);
1405 env(offer(env.master, XRP(500), usd(100)));
1406 env.close();
1407
1408 env(offerCancel(env.master, nextOfferSeq));
1409 BEAST_EXPECT(env.seq(env.master) == nextOfferSeq + 2);
1410
1411 // ledger_accept, call twice and verify no odd behavior
1412 env.close();
1413 env.close();
1414 BEAST_EXPECT(env.seq(env.master) == nextOfferSeq + 2);
1415 }
1416
1417 void
1419 {
1420 testcase("Currency Conversion: Entire Offer");
1421
1422 using namespace jtx;
1423
1424 Env env{*this, features};
1425
1426 auto const gw = Account{"gateway"};
1427 auto const alice = Account{"alice"};
1428 auto const bob = Account{"bob"};
1429
1430 env.fund(XRP(10'000), gw, alice, bob);
1431 env.require(Owners(bob, 0));
1432
1433 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
1434
1435 env.require(Owners(alice, 1), Owners(bob, 1));
1436
1437 env(pay(gw, alice, usd(100)));
1438 auto const bobOfferSeq = env.seq(bob);
1439 env(offer(bob, usd(100), XRP(500)));
1440
1441 env.require(Owners(alice, 1), Owners(bob, 2));
1442 auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
1443 BEAST_EXPECT(jro[jss::node][jss::TakerGets] == XRP(500).value().getText());
1444 BEAST_EXPECT(
1445 jro[jss::node][jss::TakerPays] == usd(100).value().getJson(JsonOptions::Values::None));
1446
1447 env(pay(alice, alice, XRP(500)), Sendmax(usd(100)));
1448
1449 auto jrr = ledgerEntryMPT(env, alice, usd);
1450 BEAST_EXPECT(!jrr[jss::node].isMember(sfMPTAmount.fieldName));
1451 jrr = ledgerEntryRoot(env, alice);
1452 BEAST_EXPECT(
1453 jrr[jss::node][sfBalance.fieldName] ==
1454 to_string((XRP(10'000) + XRP(500) - env.current()->fees().base * 2).xrp()));
1455
1456 jrr = ledgerEntryMPT(env, bob, usd);
1457 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "100");
1458
1459 jro = ledgerEntryOffer(env, bob, bobOfferSeq);
1460 BEAST_EXPECT(jro[jss::error] == "entryNotFound");
1461
1462 env.require(Owners(alice, 1), Owners(bob, 1));
1463 }
1464
1465 void
1467 {
1468 testcase("Currency Conversion: Offerer Into Debt");
1469
1470 using namespace jtx;
1471 auto const alice = Account{"alice"};
1472 auto const bob = Account{"bob"};
1473 auto const carol = Account{"carol"};
1474
1475 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
1476 Env env{*this, features};
1477
1478 env.fund(XRP(10'000), alice, bob, carol);
1479
1480 auto const usd =
1481 issue1({.env = env, .token = "USD", .issuer = alice, .holders = {bob}});
1482 auto const eurc =
1483 issue2({.env = env, .token = "EUC", .issuer = carol, .holders = {alice}});
1484 auto const eurb =
1485 issue3({.env = env, .token = "EUB", .issuer = bob, .holders = {carol}});
1486
1487 auto const bobOfferSeq = env.seq(bob);
1488 env(offer(bob, usd(50), eurc(200)), Ter(tecUNFUNDED_OFFER));
1489
1490 env(offer(alice, eurc(200), usd(50)));
1491
1492 auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
1493 BEAST_EXPECT(jro[jss::error] == "entryNotFound");
1494 };
1496 }
1497
1498 void
1500 {
1501 testcase("Currency Conversion: In Parts");
1502
1503 using namespace jtx;
1504
1505 Env env{*this, features};
1506
1507 auto const gw = Account{"gateway"};
1508 auto const alice = Account{"alice"};
1509 auto const bob = Account{"bob"};
1510
1511 env.fund(XRP(10'000), gw, alice, bob);
1512
1513 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
1514
1515 env(pay(gw, alice, usd(200)));
1516
1517 auto const bobOfferSeq = env.seq(bob);
1518 env(offer(bob, usd(100), XRP(500)));
1519
1520 env(pay(alice, alice, XRP(200)), Sendmax(usd(100)));
1521
1522 // The previous payment reduced the remaining offer amount by 200 XRP
1523 auto jro = ledgerEntryOffer(env, bob, bobOfferSeq);
1524 BEAST_EXPECT(jro[jss::node][jss::TakerGets] == XRP(300).value().getText());
1525 BEAST_EXPECT(
1526 jro[jss::node][jss::TakerPays] == usd(60).value().getJson(JsonOptions::Values::None));
1527
1528 // the balance between alice and gw is 160 USD..200 less the 40 taken
1529 // by the offer
1530 auto jrr = ledgerEntryMPT(env, alice, usd);
1531 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "160");
1532 // alice now has 200 more XRP from the payment
1533 jrr = ledgerEntryRoot(env, alice);
1534 BEAST_EXPECT(
1535 jrr[jss::node][sfBalance.fieldName] ==
1536 to_string((XRP(10'000) + XRP(200) - env.current()->fees().base * 2).xrp()));
1537
1538 // bob got 40 USD from partial consumption of the offer
1539 jrr = ledgerEntryMPT(env, bob, usd);
1540 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "40");
1541
1542 // Alice converts USD to XRP which should fail
1543 // due to PartialPayment.
1544 env(pay(alice, alice, XRP(600)), Sendmax(usd(100)), Ter(tecPATH_PARTIAL));
1545
1546 // Alice converts USD to XRP, should succeed because
1547 // we permit partial payment
1548 env(pay(alice, alice, XRP(600)), Sendmax(usd(100)), Txflags(tfPartialPayment));
1549
1550 // Verify the offer was consumed
1551 jro = ledgerEntryOffer(env, bob, bobOfferSeq);
1552 BEAST_EXPECT(jro[jss::error] == "entryNotFound");
1553
1554 // verify balances look right after the partial payment
1555 // only 300 XRP should have been payed since that's all
1556 // that remained in the offer from bob. The alice balance is now
1557 // 100 USD because another 60 USD were transferred to bob in the second
1558 // payment
1559 jrr = ledgerEntryMPT(env, alice, usd);
1560 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "100");
1561 jrr = ledgerEntryRoot(env, alice);
1562 BEAST_EXPECT(
1563 jrr[jss::node][sfBalance.fieldName] ==
1564 to_string((XRP(10'000) + XRP(200) + XRP(300) - env.current()->fees().base * 4).xrp()));
1565
1566 // bob now has 100 USD - 40 from the first payment and 60 from the
1567 // second (partial) payment
1568 jrr = ledgerEntryMPT(env, bob, usd);
1569 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "100");
1570 }
1571
1572 void
1574 {
1575 testcase("Cross Currency Payment: Start with XRP");
1576
1577 using namespace jtx;
1578
1579 Env env{*this, features};
1580
1581 auto const gw = Account{"gateway"};
1582 auto const alice = Account{"alice"};
1583 auto const bob = Account{"bob"};
1584 auto const carol = Account{"carol"};
1585
1586 env.fund(XRP(10'000), gw, alice, bob, carol);
1587
1588 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {carol, bob}});
1589
1590 env(pay(gw, carol, usd(500)));
1591
1592 auto const carolOfferSeq = env.seq(carol);
1593 env(offer(carol, XRP(500), usd(50)));
1594
1595 env(pay(alice, bob, usd(25)), Sendmax(XRP(333)));
1596
1597 auto jrr = ledgerEntryMPT(env, bob, usd);
1598 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "25");
1599
1600 jrr = ledgerEntryMPT(env, carol, usd);
1601 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "475");
1602
1603 auto jro = ledgerEntryOffer(env, carol, carolOfferSeq);
1604 BEAST_EXPECT(
1605 jro[jss::node][jss::TakerGets] == usd(25).value().getJson(JsonOptions::Values::None));
1606 BEAST_EXPECT(jro[jss::node][jss::TakerPays] == XRP(250).value().getText());
1607 }
1608
1609 void
1611 {
1612 testcase("Cross Currency Payment: End with XRP");
1613
1614 using namespace jtx;
1615
1616 Env env{*this, features};
1617
1618 auto const gw = Account{"gateway"};
1619 auto const alice = Account{"alice"};
1620 auto const bob = Account{"bob"};
1621 auto const carol = Account{"carol"};
1622
1623 env.fund(XRP(10'000), gw, alice, bob, carol);
1624
1625 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}});
1626
1627 env(pay(gw, alice, usd(500)));
1628
1629 auto const carolOfferSeq = env.seq(carol);
1630 env(offer(carol, usd(50), XRP(500)));
1631
1632 env(pay(alice, bob, XRP(250)), Sendmax(usd(333)));
1633
1634 auto jrr = ledgerEntryMPT(env, alice, usd);
1635 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "475");
1636
1637 jrr = ledgerEntryMPT(env, carol, usd);
1638 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "25");
1639
1640 jrr = ledgerEntryRoot(env, bob);
1641 BEAST_EXPECT(
1642 jrr[jss::node][sfBalance.fieldName] ==
1643 std::to_string(XRP(10'000).value().mantissa() + XRP(250).value().mantissa()));
1644
1645 auto jro = ledgerEntryOffer(env, carol, carolOfferSeq);
1646 BEAST_EXPECT(jro[jss::node][jss::TakerGets] == XRP(250).value().getText());
1647 BEAST_EXPECT(
1648 jro[jss::node][jss::TakerPays] == usd(25).value().getJson(JsonOptions::Values::None));
1649 }
1650
1651 void
1653 {
1654 testcase("Cross Currency Payment: Bridged");
1655
1656 using namespace jtx;
1657 auto const gw1 = Account{"gateway_1"};
1658 auto const gw2 = Account{"gateway_2"};
1659 auto const alice = Account{"alice"};
1660 auto const bob = Account{"bob"};
1661 auto const carol = Account{"carol"};
1662 auto const dan = Account{"dan"};
1663
1664 auto test = [&](auto&& issue1, auto&& issue2) {
1665 Env env{*this, features};
1666
1667 env.fund(XRP(10'000), gw1, gw2, alice, bob, carol, dan);
1668
1669 auto const usd =
1670 issue1({.env = env, .token = "USD", .issuer = gw1, .holders = {alice, carol}});
1671 auto const eur =
1672 issue1({.env = env, .token = "EUR", .issuer = gw2, .holders = {bob, dan}});
1673
1674 env(pay(gw1, alice, usd(500)));
1675 env(pay(gw2, dan, eur(400)));
1676
1677 auto const carolOfferSeq = env.seq(carol);
1678 env(offer(carol, usd(50), XRP(500)));
1679
1680 auto const danOfferSeq = env.seq(dan);
1681 env(offer(dan, XRP(500), eur(50)));
1682
1684 jtp[0u][0u][jss::currency] = "XRP";
1685 env(pay(alice, bob, eur(30)), Json(jss::Paths, jtp), Sendmax(usd(333)));
1686
1687 BEAST_EXPECT(env.balance(alice, usd) == usd(470));
1688 BEAST_EXPECT(env.balance(bob, eur) == eur(30));
1689 BEAST_EXPECT(env.balance(carol, usd) == usd(30));
1690 BEAST_EXPECT(env.balance(dan, eur) == eur(370));
1691
1692 auto jro = ledgerEntryOffer(env, carol, carolOfferSeq);
1693 BEAST_EXPECT(jro[jss::node][jss::TakerGets] == XRP(200).value().getText());
1694 BEAST_EXPECT(
1695 jro[jss::node][jss::TakerPays] ==
1696 usd(20).value().getJson(JsonOptions::Values::None));
1697
1698 jro = ledgerEntryOffer(env, dan, danOfferSeq);
1699 BEAST_EXPECT(
1700 jro[jss::node][jss::TakerGets] ==
1701 eur(20).value().getJson(JsonOptions::Values::None));
1702 BEAST_EXPECT(jro[jss::node][jss::TakerPays] == XRP(200).value().getText());
1703 };
1705 }
1706
1707 void
1709 {
1710 // At least with Taker bridging, a sensitivity was identified if the
1711 // second leg goes dry before the first one. This test exercises that
1712 // case.
1713 testcase("Auto Bridged Second Leg Dry");
1714
1715 using namespace jtx;
1716 Account const alice{"alice"};
1717 Account const bob{"bob"};
1718 Account const carol{"carol"};
1719 Account const gw{"gateway"};
1720
1721 auto test = [&](auto&& issue1, auto&& issue2) {
1722 Env env(*this, features);
1723
1724 env.fund(XRP(100'000'000), alice, bob, carol, gw);
1725 env.close();
1726
1727 auto const usd =
1728 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, carol}});
1729 auto const eur = issue1({.env = env, .token = "EUR", .issuer = gw, .holders = {bob}});
1730
1731 env(pay(gw, alice, usd(10)));
1732 env(pay(gw, carol, usd(3)));
1733
1734 env(offer(alice, eur(2), XRP(1)));
1735 env(offer(alice, eur(2), XRP(1)));
1736
1737 env(offer(alice, XRP(1), usd(4)));
1738 env(offer(carol, XRP(1), usd(3)));
1739 env.close();
1740
1741 // Bob offers to buy 10 USD for 10 EUR.
1742 // 1. He spends 2 EUR taking Alice's auto-bridged offers and
1743 // gets 4 USD for that.
1744 // 2. He spends another 2 EUR taking Alice's last EUR->XRP offer
1745 // and
1746 // Carol's XRP-USD offer. He gets 3 USD for that.
1747 // The key for this test is that Alice's XRP->USD leg goes dry
1748 // before Alice's EUR->XRP. The XRP->USD leg is the second leg
1749 // which showed some sensitivity.
1750 env(pay(gw, bob, eur(10)));
1751 env.close();
1752 env(offer(bob, usd(10), eur(10)));
1753 env.close();
1754
1755 env.require(Balance(bob, usd(7)));
1756 env.require(Balance(bob, eur(6)));
1757 env.require(offers(bob, 1));
1758 env.require(Owners(bob, 3));
1759
1760 env.require(Balance(alice, usd(6)));
1761 env.require(Balance(alice, eur(4)));
1762 env.require(offers(alice, 0));
1763 env.require(Owners(alice, 2));
1764
1765 env.require(Balance(carol, usd(0)));
1766 env.require(Balance(carol, eur(kNone)));
1767
1768 env.require(offers(carol, 0));
1769 env.require(Owners(carol, 1));
1770 };
1772 }
1773
1774 void
1776 {
1777 testcase("Offer Fees Consume Funds");
1778
1779 using namespace jtx;
1780 auto const gw1 = Account{"gateway_1"};
1781 auto const gw2 = Account{"gateway_2"};
1782 auto const gw3 = Account{"gateway_3"};
1783 auto const alice = Account{"alice"};
1784 auto const bob = Account{"bob"};
1785
1786 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
1787 Env env{*this, features};
1788
1789 // Provide micro amounts to compensate for fees to make results
1790 // round nice. reserve: Alice has 3 entries in the ledger, via trust
1791 // lines fees:
1792 // 1 for each trust limit == 3 (alice < mtgox/amazon/bitstamp) +
1793 // 1 for payment == 4
1794 auto const base = env.current()->fees().base;
1795 auto const startingXrp = XRP(100) + env.current()->fees().accountReserve(3) + base * 4;
1796
1797 env.fund(startingXrp, gw1, gw2, gw3, alice, bob);
1798 env.close();
1799
1800 auto const usD1 =
1801 issue1({.env = env, .token = "US1", .issuer = gw1, .holders = {alice, bob}});
1802 auto const usD2 =
1803 issue2({.env = env, .token = "US2", .issuer = gw2, .holders = {alice, bob}});
1804 auto const usD3 =
1805 issue3({.env = env, .token = "US3", .issuer = gw3, .holders = {alice}});
1806
1807 env(pay(gw1, bob, usD1(500)));
1808
1809 env(offer(bob, XRP(200), usD1(200)));
1810 // Alice has 350 fees - a reserve of 50 = 250 reserve = 100
1811 // available. Ask for more than available to prove reserve works.
1812 env(offer(alice, usD1(200), XRP(200)));
1813
1814 BEAST_EXPECT(env.balance(alice, usD1) == usD1(100));
1815 BEAST_EXPECT(env.balance(alice) == STAmount(env.current()->fees().accountReserve(3)));
1816
1817 BEAST_EXPECT(env.balance(bob, usD1) == usD1(400));
1818 };
1820 }
1821
1822 void
1824 {
1825 testcase("Offer Create, then Cross");
1826
1827 using namespace jtx;
1828
1829 Env env{*this, features};
1830
1831 auto const gw = Account{"gateway"};
1832 auto const alice = Account{"alice"};
1833 auto const bob = Account{"bob"};
1834
1835 env.fund(XRP(10'000), gw, alice, bob);
1836
1837 MPT const cur =
1838 MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .transferFee = 5'000});
1839
1840 env(pay(gw, bob, cur(100)));
1841
1842 env(offer(alice, cur(50'000), XRP(150'000)));
1843 env(offer(bob, XRP(100), cur(100)));
1844
1845 auto jrr = ledgerEntryMPT(env, alice, cur);
1846 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "34");
1847
1848 jrr = ledgerEntryMPT(env, bob, cur);
1849 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "64");
1850 }
1851
1852 void
1854 {
1855 testcase("Offer tfSell: Basic Sell");
1856
1857 using namespace jtx;
1858
1859 Env env{*this, features};
1860
1861 auto const gw = Account{"gateway"};
1862 auto const alice = Account{"alice"};
1863 auto const bob = Account{"bob"};
1864
1865 auto const startingXrp =
1866 XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2;
1867
1868 env.fund(startingXrp, gw, alice, bob);
1869
1870 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
1871
1872 env(pay(gw, bob, usd(500)));
1873
1874 env(offer(bob, XRP(200), usd(200)), Json(jss::Flags, tfSell));
1875 // Alice has 350 + fees - a reserve of 50 = 250 reserve = 100 available.
1876 // Alice has 350 + fees - a reserve of 50 = 250 reserve = 100 available.
1877 // Ask for more than available to prove reserve works.
1878 env(offer(alice, usd(200), XRP(200)), Json(jss::Flags, tfSell));
1879
1880 auto jrr = ledgerEntryMPT(env, alice, usd);
1881 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "100");
1882 jrr = ledgerEntryRoot(env, alice);
1883 BEAST_EXPECT(
1884 jrr[jss::node][sfBalance.fieldName] ==
1885 STAmount(env.current()->fees().accountReserve(1)).getText());
1886
1887 jrr = ledgerEntryMPT(env, bob, usd);
1888 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "400");
1889 }
1890
1891 void
1893 {
1894 testcase("Offer tfSell: 2x Sell Exceed Limit");
1895
1896 using namespace jtx;
1897
1898 Env env{*this, features};
1899
1900 auto const gw = Account{"gateway"};
1901 auto const alice = Account{"alice"};
1902 auto const bob = Account{"bob"};
1903
1904 auto const startingXrp =
1905 XRP(100) + env.current()->fees().accountReserve(1) + env.current()->fees().base * 2;
1906
1907 env.fund(startingXrp, gw, alice, bob);
1908
1909 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
1910
1911 env(pay(gw, bob, usd(500)));
1912
1913 env(offer(bob, XRP(100), usd(200)));
1914 // Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available.
1915 // Ask for more than available to prove reserve works.
1916 // Taker pays 100 USD for 100 XRP.
1917 // Selling XRP.
1918 // Will sell all 100 XRP and get more USD than asked for.
1919 env(offer(alice, usd(100), XRP(100)), Json(jss::Flags, tfSell));
1920
1921 auto jrr = ledgerEntryMPT(env, alice, usd);
1922 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "200");
1923 jrr = ledgerEntryRoot(env, alice);
1924 BEAST_EXPECT(
1925 jrr[jss::node][sfBalance.fieldName] ==
1926 STAmount(env.current()->fees().accountReserve(1)).getText());
1927
1928 jrr = ledgerEntryMPT(env, bob, usd);
1929 BEAST_EXPECT(jrr[jss::node][sfMPTAmount.fieldName] == "300");
1930 }
1931
1932 void
1934 {
1935 testcase("Client Issue #535: Gateway Cross Currency");
1936
1937 using namespace jtx;
1938 auto const gw = Account{"gateway"};
1939 auto const alice = Account{"alice"};
1940 auto const bob = Account{"bob"};
1941
1942 auto test = [&](auto&& issue1, auto&& issue2) {
1943 Env env{*this, features};
1944
1945 auto const base = env.current()->fees().base;
1946 auto const startingXrp =
1947 XRP(100.1) + env.current()->fees().accountReserve(1) + base * 2;
1948
1949 env.fund(startingXrp, gw, alice, bob);
1950 env.close();
1951
1952 auto const xts =
1953 issue1({.env = env, .token = "XTS", .issuer = gw, .holders = {alice, bob}});
1954 auto const xxx =
1955 issue2({.env = env, .token = "XXX", .issuer = gw, .holders = {alice, bob}});
1956 env.close();
1957
1958 env(pay(gw, alice, xts(1'000)));
1959 env(pay(gw, alice, xxx(100)));
1960 env(pay(gw, bob, xts(1'000)));
1961 env(pay(gw, bob, xxx(100)));
1962
1963 env(offer(alice, xts(1'000), xxx(100)));
1964
1965 // WS client is used here because the RPC client could not
1966 // be convinced to pass the build_path argument
1967 auto wsc = makeWSClient(env.app().config());
1968 json::Value payment;
1969 payment[jss::secret] = toBase58(generateSeed("bob"));
1970 payment[jss::id] = env.seq(bob);
1971 payment[jss::build_path] = true;
1972 payment[jss::tx_json] = pay(bob, bob, xxx(1));
1973 payment[jss::tx_json][jss::Sequence] =
1974 env.current()->read(keylet::account(bob.id()))->getFieldU32(sfSequence);
1975 payment[jss::tx_json][jss::Fee] = to_string(env.current()->fees().base);
1976 payment[jss::tx_json][jss::SendMax] =
1977 xts(15).value().getJson(JsonOptions::Values::None);
1978 auto jrr = wsc->invoke("submit", payment);
1979 BEAST_EXPECT(jrr[jss::status] == "success");
1980 BEAST_EXPECT(jrr[jss::result][jss::engine_result] == "tesSUCCESS");
1981 if (wsc->version() == 2)
1982 {
1983 BEAST_EXPECT(jrr.isMember(jss::jsonrpc) && jrr[jss::jsonrpc] == "2.0");
1984 BEAST_EXPECT(jrr.isMember(jss::ripplerpc) && jrr[jss::ripplerpc] == "2.0");
1985 BEAST_EXPECT(jrr.isMember(jss::id) && jrr[jss::id] == 5);
1986 }
1987
1988 BEAST_EXPECT(env.balance(alice, xts) == xts(1010));
1989 BEAST_EXPECT(env.balance(alice, xxx) == xxx(99));
1990
1991 BEAST_EXPECT(env.balance(bob, xts) == xts(990));
1992 BEAST_EXPECT(env.balance(bob, xxx) == xxx(101));
1993 };
1995 }
1996
1997 void
1999 {
2000 // Test a number of different corner cases regarding adding a
2001 // possibly crossable offer to an account. The test is table
2002 // driven so it should be easy to add or remove tests.
2003 testcase("Partial Crossing");
2004
2005 using namespace jtx;
2006
2007 auto const gw = Account("gateway");
2008
2009 Env env{*this, features};
2010
2011 env.fund(XRP(10'000'000), gw);
2012 env.close();
2013
2014 auto musd = MPTTester({.env = env, .issuer = gw});
2015 MPT const usd = musd;
2016
2017 // The fee that's charged for transactions
2018 auto const f = env.current()->fees().base;
2019
2020 // To keep things simple all offers are 1 : 1 for XRP : USD.
2021 enum class PreAuthType { NoPreAuth, AcctPreAuth };
2022 struct TestData
2023 {
2024 std::string account; // Account operated on
2025 STAmount fundXrp; // Account funded with
2026 int bookAmount; // USD -> XRP offer on the books
2027 PreAuthType preAuth; // If true, pre-auth MPToken
2028 int offerAmount; // Account offers this much XRP -> USD
2029 TER tec; // Returned tec code
2030 STAmount spentXrp; // Amount removed from fundXrp
2031 PrettyAmount balanceUsd; // Balance on account end
2032 int offers; // Offers on account
2033 int owners; // Owners on account
2034 int scale = 1; // Scale MPT
2035 };
2036
2037 // clang-format off
2038 TestData const tests[]{
2039 // acct fundXrp bookAmt preTrust offerAmt tec spentXrp balanceUSD offers owners scale
2040 {.account="ann", .fundXrp=reserve(env, 0) + 0 * f, .bookAmount=1, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tecUNFUNDED_OFFER, .spentXrp=f, .balanceUsd=usd( 0), .offers=0, .owners=0}, // Account is at the reserve, and will dip below once fees are subtracted.
2041 {.account="bev", .fundXrp=reserve(env, 0) + 1 * f, .bookAmount=1, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tecUNFUNDED_OFFER, .spentXrp=f, .balanceUsd=usd( 0), .offers=0, .owners=0}, // Account has just enough for the reserve and the fee.
2042 {.account="cam", .fundXrp=reserve(env, 0) + 2 * f, .bookAmount=0, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tecINSUF_RESERVE_OFFER, .spentXrp=f, .balanceUsd=usd( 0), .offers=0, .owners=0}, // Account has enough for the reserve, the fee and the offer, and a bit more, but not enough for the reserve after the offer is placed.
2043 {.account="deb", .fundXrp=reserve(env, 0) + 2 * f, .bookAmount=1, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=2 * f, .balanceUsd=usd( 1), .offers=0, .owners=1, .scale=100000}, // Account has enough to buy a little USD then the offer runs dry.
2044 {.account="eve", .fundXrp=reserve(env, 1) + 0 * f, .bookAmount=0, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=f, .balanceUsd=usd( 0), .offers=1, .owners=1}, // No offer to cross
2045 {.account="flo", .fundXrp=reserve(env, 1) + 0 * f, .bookAmount=1, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 1) + f, .balanceUsd=usd( 1), .offers=0, .owners=1},
2046 {.account="gay", .fundXrp=reserve(env, 1) + 1 * f, .bookAmount=1000, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 50) + f, .balanceUsd=usd( 50), .offers=0, .owners=1},
2047 {.account="hye", .fundXrp=XRP(1000) + 1 * f, .bookAmount=1000, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 800) + f, .balanceUsd=usd( 800), .offers=0, .owners=1},
2048 {.account="ivy", .fundXrp=XRP( 1) + reserve(env, 1) + 1 * f, .bookAmount=1, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 1) + f, .balanceUsd=usd( 1), .offers=0, .owners=1},
2049 {.account="joy", .fundXrp=XRP( 1) + reserve(env, 2) + 1 * f, .bookAmount=1, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 1) + f, .balanceUsd=usd( 1), .offers=1, .owners=2},
2050 {.account="kim", .fundXrp=XRP( 900) + reserve(env, 2) + 1 * f, .bookAmount=999, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 999) + f, .balanceUsd=usd( 999), .offers=0, .owners=1},
2051 {.account="liz", .fundXrp=XRP( 998) + reserve(env, 0) + 1 * f, .bookAmount=999, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 998) + f, .balanceUsd=usd( 998), .offers=0, .owners=1},
2052 {.account="meg", .fundXrp=XRP( 998) + reserve(env, 1) + 1 * f, .bookAmount=999, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 999) + f, .balanceUsd=usd( 999), .offers=0, .owners=1},
2053 {.account="nia", .fundXrp=XRP( 998) + reserve(env, 2) + 1 * f, .bookAmount=999, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 999) + f, .balanceUsd=usd( 999), .offers=1, .owners=2},
2054 {.account="ova", .fundXrp=XRP( 999) + reserve(env, 0) + 1 * f, .bookAmount=1000, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 999) + f, .balanceUsd=usd( 999), .offers=0, .owners=1},
2055 {.account="pam", .fundXrp=XRP( 999) + reserve(env, 1) + 1 * f, .bookAmount=1000, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP(1000) + f, .balanceUsd=usd( 1000), .offers=0, .owners=1},
2056 {.account="rae", .fundXrp=XRP( 999) + reserve(env, 2) + 1 * f, .bookAmount=1000, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP(1000) + f, .balanceUsd=usd( 1000), .offers=0, .owners=1},
2057 {.account="sue", .fundXrp=XRP(1000) + reserve(env, 2) + 1 * f, .bookAmount=0, .preAuth=PreAuthType::NoPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=f, .balanceUsd=usd( 0), .offers=1, .owners=1},
2058 //---------------- Pre-created MPT ---------------------
2059 // Unlike from IOU, an issuer can't pre-create MPToken for an account (see similar tests in Offer_test.cpp)
2060 {.account="ned", .fundXrp=reserve(env, 1) + 0 * f, .bookAmount=1, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tecUNFUNDED_OFFER, .spentXrp=2 * f, .balanceUsd=usd( 0), .offers=0, .owners=1},
2061 {.account="ole", .fundXrp=reserve(env, 1) + 1 * f, .bookAmount=1, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tecUNFUNDED_OFFER, .spentXrp=2 * f, .balanceUsd=usd( 0), .offers=0, .owners=1},
2062 {.account="pat", .fundXrp=reserve(env, 1) + 2 * f, .bookAmount=0, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tecUNFUNDED_OFFER, .spentXrp=2 * f, .balanceUsd=usd( 0), .offers=0, .owners=1},
2063 {.account="quy", .fundXrp=reserve(env, 1) + 2 * f, .bookAmount=1, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tecUNFUNDED_OFFER, .spentXrp=2 * f, .balanceUsd=usd( 0), .offers=0, .owners=1},
2064 {.account="ron", .fundXrp=reserve(env, 1) + 3 * f, .bookAmount=0, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tecINSUF_RESERVE_OFFER, .spentXrp=2 * f, .balanceUsd=usd( 0), .offers=0, .owners=1},
2065 {.account="syd", .fundXrp=reserve(env, 1) + 3 * f, .bookAmount=1, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=3 * f, .balanceUsd=usd( 1), .offers=0, .owners=1, .scale=100000},
2066 {.account="ted", .fundXrp=XRP( 20) + reserve(env, 1) + 2 * f, .bookAmount=1000, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP(20) + 2 * f, .balanceUsd=usd( 20), .offers=0, .owners=1},
2067 {.account="uli", .fundXrp=reserve(env, 2) + 0 * f, .bookAmount=0, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tecINSUF_RESERVE_OFFER, .spentXrp=2 * f, .balanceUsd=usd( 0), .offers=0, .owners=1},
2068 {.account="vic", .fundXrp=reserve(env, 2) + 0 * f, .bookAmount=1, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 1) + 2 * f, .balanceUsd=usd( 1), .offers=0, .owners=1},
2069 {.account="wes", .fundXrp=reserve(env, 2) + 1 * f, .bookAmount=0, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=2 * f, .balanceUsd=usd( 0), .offers=1, .owners=2},
2070 {.account="xan", .fundXrp=reserve(env, 2) + 1 * f, .bookAmount=1, .preAuth=PreAuthType::AcctPreAuth, .offerAmount=1000, .tec=tesSUCCESS, .spentXrp=XRP( 1) + 2 * f, .balanceUsd=usd( 1), .offers=1, .owners=2},
2071 };
2072 // clang-format on
2073
2074 for (auto const& t : tests)
2075 {
2076 auto const acct = Account(t.account);
2077 env.fund(t.fundXrp, acct);
2078 env.close();
2079
2080 // Make sure gateway has no current offers.
2081 env.require(offers(gw, 0));
2082
2083 // The gateway optionally creates an offer that would be crossed.
2084 auto const book = t.bookAmount;
2085 if (book != 0)
2086 env(offer(gw, XRP(book), usd(book * t.scale)));
2087 env.close();
2088 std::uint32_t const gwOfferSeq = env.seq(gw) - 1;
2089
2090 // Optionally pre-authorize MPT for acct.
2091 // Note this is not really part of the test, so we expect there
2092 // to be enough XRP reserve for acct to create the trust line.
2093 if (t.preAuth == PreAuthType::AcctPreAuth)
2094 musd.authorize({.account = acct});
2095 env.close();
2096
2097 {
2098 // Acct creates an offer. This is the heart of the test.
2099 auto const acctOffer = t.offerAmount;
2100 env(offer(acct, usd(acctOffer * t.scale), XRP(acctOffer)), Ter(t.tec));
2101 env.close();
2102 }
2103 std::uint32_t const acctOfferSeq = env.seq(acct) - 1;
2104
2105 auto const expBalanceUsd = [&]() {
2106 if (t.scale == 1)
2107 return t.balanceUsd;
2108 // crossed offer has XRP available balance of 1 fee
2109 // mpt to XRP ratio is 10
2110 return usd(f.value() / 10);
2111 }();
2112 BEAST_EXPECT(env.balance(acct, usd) == expBalanceUsd);
2113 BEAST_EXPECT(env.balance(acct, xrpIssue()) == t.fundXrp - t.spentXrp);
2114 env.require(offers(acct, t.offers));
2115 env.require(Owners(acct, t.owners));
2116
2117 auto acctOffers = offersOnAccount(env, acct);
2118 BEAST_EXPECT(acctOffers.size() == t.offers);
2119 if (!acctOffers.empty() && t.offers != 0)
2120 {
2121 auto const& acctOffer = *(acctOffers.front());
2122
2123 auto const leftover = t.offerAmount - t.bookAmount;
2124 BEAST_EXPECT(acctOffer[sfTakerGets] == XRP(leftover));
2125 BEAST_EXPECT(acctOffer[sfTakerPays] == usd(leftover));
2126 }
2127
2128 if (t.preAuth == PreAuthType::NoPreAuth)
2129 {
2130 if (t.balanceUsd.value().signum() != 0)
2131 {
2132 // Verify the correct contents of MPT
2133 BEAST_EXPECT(env.balance(acct, usd) == expBalanceUsd);
2134 }
2135 else
2136 {
2137 // Verify that no MPT was created.
2138 auto const sle = env.le(keylet::mptoken(usd.issuanceID, acct));
2139 BEAST_EXPECT(!sle);
2140 }
2141 }
2142
2143 // Give the next loop a clean slate by canceling any left-overs
2144 // in the offers.
2145 env(offerCancel(acct, acctOfferSeq));
2146 env(offerCancel(gw, gwOfferSeq));
2147 env.close();
2148 }
2149 }
2150
2151 void
2153 {
2154 testcase("XRP Direct Crossing");
2155
2156 using namespace jtx;
2157
2158 auto const gw = Account("gateway");
2159 auto const alice = Account("alice");
2160 auto const bob = Account("bob");
2161
2162 Env env{*this, features};
2163
2164 env.fund(XRP(1'000'000), gw, bob);
2165 env.close();
2166
2167 // The fee that's charged for transactions.
2168 auto const fee = env.current()->fees().base;
2169
2170 // alice's account has enough for the reserve, one trust line plus two
2171 // offers, and two fees.
2172 env.fund(reserve(env, 2) + fee * 2, alice);
2173 env.close();
2174
2175 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
2176
2177 auto const usdOffer = usd(1'000);
2178 auto const xrpOffer = XRP(1'000);
2179
2180 env(pay(gw, alice, usdOffer));
2181 env.close();
2182 env.require(Balance(alice, usdOffer), offers(alice, 0), offers(bob, 0));
2183
2184 // The scenario:
2185 // o alice has USD but wants XRP.
2186 // o bob has XRP but wants USD.
2187 auto const aliceXRP = env.balance(alice);
2188 auto const bobsXRP = env.balance(bob);
2189
2190 env(offer(alice, xrpOffer, usdOffer));
2191 env.close();
2192 env(offer(bob, usdOffer, xrpOffer));
2193
2194 env.close();
2195 env.require(
2196 Balance(alice, usd(0)),
2197 Balance(bob, usdOffer),
2198 Balance(alice, aliceXRP + xrpOffer - fee),
2199 Balance(bob, bobsXRP - xrpOffer - fee),
2200 offers(alice, 0),
2201 offers(bob, 0));
2202
2203 BEAST_EXPECT(env.balance(bob, usd) == usdOffer);
2204
2205 // Make two more offers that leave one of the offers non-dry.
2206 env(offer(alice, usd(999), XRP(999)));
2207 env(offer(bob, xrpOffer, usdOffer));
2208
2209 env.close();
2210 env.require(Balance(alice, usd(999)));
2211 env.require(Balance(bob, usd(1)));
2212 env.require(offers(alice, 0));
2213 BEAST_EXPECT(env.balance(bob, usd) == usd(1));
2214 {
2215 auto const bobsOffers = offersOnAccount(env, bob);
2216 BEAST_EXPECT(bobsOffers.size() == 1);
2217 auto const& bobsOffer = *(bobsOffers.front());
2218
2219 BEAST_EXPECT(bobsOffer[sfLedgerEntryType] == ltOFFER);
2220 BEAST_EXPECT(bobsOffer[sfTakerGets] == usd(1));
2221 BEAST_EXPECT(bobsOffer[sfTakerPays] == XRP(1));
2222 }
2223 }
2224
2225 void
2227 {
2228 testcase("Direct Crossing");
2229
2230 using namespace jtx;
2231 auto const gw = Account("gateway");
2232 auto const alice = Account("alice");
2233 auto const bob = Account("bob");
2234
2235 auto test = [&](auto&& issue1, auto&& issue2) {
2236 Env env{*this, features};
2237
2238 env.fund(XRP(1000000), gw);
2239 env.close();
2240
2241 // The fee that's charged for transactions.
2242 auto const fee = env.current()->fees().base;
2243
2244 // Each account has enough for the reserve, two MPT's, one
2245 // offer, and two fees.
2246 env.fund(reserve(env, 3) + fee * 3, alice);
2247 env.fund(reserve(env, 3) + fee * 2, bob);
2248 env.close();
2249
2250 auto const usd = issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice}});
2251 auto const eur = issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {bob}});
2252
2253 auto const usdOffer = usd(1'000);
2254 auto const eurOffer = eur(1'000);
2255
2256 env(pay(gw, alice, usdOffer));
2257 env(pay(gw, bob, eurOffer));
2258 env.close();
2259
2260 env.require(Balance(alice, usdOffer), Balance(bob, eurOffer));
2261
2262 // The scenario:
2263 // o alice has USD but wants EUR.
2264 // o bob has EUR but wants USD.
2265 env(offer(alice, eurOffer, usdOffer));
2266 env(offer(bob, usdOffer, eurOffer));
2267
2268 env.close();
2269 env.require(
2270 Balance(alice, eurOffer), Balance(bob, usdOffer), offers(alice, 0), offers(bob, 0));
2271
2272 // Alice's offer crossing created a default EUR trustline and
2273 // Bob's offer crossing created a default USD trustline:
2274 BEAST_EXPECT(env.balance(alice, eur) == eurOffer);
2275 BEAST_EXPECT(env.balance(bob, usd) == usdOffer);
2276
2277 // Make two more offers that leave one of the offers non-dry.
2278 // Guarantee the order of application by putting a close()
2279 // between them.
2280 env(offer(bob, eurOffer, usdOffer));
2281 env.close();
2282
2283 env(offer(alice, usd(999), eurOffer));
2284 env.close();
2285
2286 env.require(offers(alice, 0));
2287 env.require(offers(bob, 1));
2288
2289 env.require(Balance(alice, usd(999)));
2290 env.require(Balance(alice, eur(1)));
2291 env.require(Balance(bob, usd(1)));
2292 env.require(Balance(bob, eur(999)));
2293
2294 {
2295 auto bobsOffers = offersOnAccount(env, bob);
2296 if (BEAST_EXPECT(bobsOffers.size() == 1))
2297 {
2298 auto const& bobsOffer = *(bobsOffers.front());
2299
2300 BEAST_EXPECT(bobsOffer[sfTakerGets] == usd(1));
2301 BEAST_EXPECT(bobsOffer[sfTakerPays] == eur(1));
2302 }
2303 }
2304
2305 // alice makes one more offer that cleans out bob's offer.
2306 env(offer(alice, usd(1), eur(1)));
2307 env.close();
2308
2309 env.require(Balance(alice, usd(1'000)));
2310 env.require(Balance(alice, eur(kNone)));
2311 env.require(Balance(bob, usd(kNone)));
2312 env.require(Balance(bob, eur(1'000)));
2313 env.require(offers(alice, 0));
2314 env.require(offers(bob, 0));
2315
2316 // The two MPT that were generated by the offers still here
2317 // Unlike IOU, MPToken is not automatically deleted
2318 if constexpr (std::is_same_v<std::decay_t<decltype(eur)>, MPT>)
2319 {
2320 BEAST_EXPECT(env.le(keylet::mptoken(eur.issuanceID, alice)));
2321 auto meur = MPTTester(env, gw, eur, {bob});
2322 // Delete created MPToken to free up reserve
2323 meur.authorize({.account = alice, .flags = tfMPTUnauthorize});
2324 }
2325 if constexpr (std::is_same_v<std::decay_t<decltype(usd)>, MPT>)
2326 {
2327 BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID, bob)));
2328 auto musd = MPTTester(env, gw, usd, {alice});
2329 // Delete created MPToken to free up reserve
2330 musd.authorize({.account = bob, .flags = tfMPTUnauthorize});
2331 }
2332
2333 // Make two more offers that leave one of the offers non-dry. We
2334 // need to properly sequence the transactions:
2335 env(offer(alice, eur(999), usdOffer));
2336 env.close();
2337
2338 env(offer(bob, usdOffer, eurOffer));
2339 env.close();
2340
2341 env.require(offers(alice, 0));
2342 env.require(offers(bob, 0));
2343
2344 env.require(Balance(alice, usd(0)));
2345 env.require(Balance(alice, eur(999)));
2346 env.require(Balance(bob, usd(1'000)));
2347 env.require(Balance(bob, eur(1)));
2348 };
2350 }
2351
2352 void
2354 {
2355 testcase("Bridged Crossing");
2356
2357 using namespace jtx;
2358 auto const gw = Account("gateway");
2359 auto const alice = Account("alice");
2360 auto const bob = Account("bob");
2361 auto const carol = Account("carol");
2362
2363 auto test = [&](auto&& issue1, auto&& issue2) {
2364 Env env{*this, features};
2365
2366 env.fund(XRP(1'000'000), gw, alice, bob, carol);
2367 env.close();
2368
2369 auto const usd = issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice}});
2370 auto const eur = issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {carol}});
2371
2372 auto const usdOffer = usd(1'000);
2373 auto const eurOffer = eur(1'000);
2374
2375 env(pay(gw, alice, usdOffer));
2376 env(pay(gw, carol, eurOffer));
2377 env.close();
2378
2379 // The scenario:
2380 // o alice has USD but wants XRP.
2381 // o bob has XRP but wants EUR.
2382 // o carol has EUR but wants USD.
2383 // Note that carol's offer must come last. If carol's offer is
2384 // placed before bob's or alice's, then autobridging will not occur.
2385 env(offer(alice, XRP(1'000), usdOffer));
2386 env(offer(bob, eurOffer, XRP(1'000)));
2387 auto const bobXrpBalance = env.balance(bob);
2388 env.close();
2389
2390 // carol makes an offer that partially consumes alice and bob's
2391 // offers.
2392 env(offer(carol, usd(400), eur(400)));
2393 env.close();
2394
2395 env.require(
2396 Balance(alice, usd(600)),
2397 Balance(bob, eur(400)),
2398 Balance(carol, usd(400)),
2399 Balance(bob, bobXrpBalance - XRP(400)),
2400 offers(carol, 0));
2401 BEAST_EXPECT(env.balance(bob, eur) == eur(400));
2402 BEAST_EXPECT(env.balance(carol, usd) == usd(400));
2403 {
2404 auto const aliceOffers = offersOnAccount(env, alice);
2405 BEAST_EXPECT(aliceOffers.size() == 1);
2406 auto const& aliceOffer = *(aliceOffers.front());
2407
2408 BEAST_EXPECT(aliceOffer[sfLedgerEntryType] == ltOFFER);
2409 BEAST_EXPECT(aliceOffer[sfTakerGets] == usd(600));
2410 BEAST_EXPECT(aliceOffer[sfTakerPays] == XRP(600));
2411 }
2412 {
2413 auto const bobsOffers = offersOnAccount(env, bob);
2414 BEAST_EXPECT(bobsOffers.size() == 1);
2415 auto const& bobsOffer = *(bobsOffers.front());
2416
2417 BEAST_EXPECT(bobsOffer[sfLedgerEntryType] == ltOFFER);
2418 BEAST_EXPECT(bobsOffer[sfTakerGets] == XRP(600));
2419 BEAST_EXPECT(bobsOffer[sfTakerPays] == eur(600));
2420 }
2421
2422 // carol makes an offer that exactly consumes alice and bob's
2423 // offers.
2424 env(offer(carol, usd(600), eur(600)));
2425 env.close();
2426
2427 env.require(
2428 Balance(alice, usd(0)),
2429 Balance(bob, eurOffer),
2430 Balance(carol, usdOffer),
2431 Balance(bob, bobXrpBalance - XRP(1'000)),
2432 offers(bob, 0),
2433 offers(carol, 0));
2434 BEAST_EXPECT(env.balance(bob, eur) == eur(1'000));
2435 BEAST_EXPECT(env.balance(carol, usd) == usd(1'000));
2436
2437 // In pre-flow code alice's offer is left empty in the ledger.
2438 auto const aliceOffers = offersOnAccount(env, alice);
2439 if (!aliceOffers.empty())
2440 {
2441 BEAST_EXPECT(aliceOffers.size() == 1);
2442 auto const& aliceOffer = *(aliceOffers.front());
2443
2444 BEAST_EXPECT(aliceOffer[sfLedgerEntryType] == ltOFFER);
2445 BEAST_EXPECT(aliceOffer[sfTakerGets] == usd(0));
2446 BEAST_EXPECT(aliceOffer[sfTakerPays] == XRP(0));
2447 }
2448 };
2450 }
2451
2452 void
2454 {
2455 // Test a number of different corner cases regarding offer crossing
2456 // when the tfSell flag is set. The test is table driven so it
2457 // should be easy to add or remove tests.
2458 testcase("Sell Offer");
2459
2460 using namespace jtx;
2461
2462 auto const gw = Account("gateway");
2463
2464 Env env{*this, features};
2465
2466 env.fund(XRP(10'000'000), gw);
2467
2468 auto musd = MPTTester({.env = env, .issuer = gw});
2469 MPT const usd = musd;
2470
2471 // The fee that's charged for transactions
2472 auto const f = env.current()->fees().base;
2473
2474 // To keep things simple all offers are 1 : 1 for XRP : USD.
2475 struct TestData
2476 {
2477 std::string account; // Account operated on
2478 STAmount fundXrp; // XRP acct funded with
2479 STAmount fundUSD; // USD acct funded with
2480 STAmount gwGets; // gw's offer
2481 STAmount gwPays; //
2482 STAmount acctGets; // acct's offer
2483 STAmount acctPays; //
2484 TER tec; // Returned tec code
2485 STAmount spentXrp; // Amount removed from fundXrp
2486 STAmount finalUsd; // Final USD balance on acct
2487 int offers; // Offers on acct
2488 int owners; // Owners on acct
2489 STAmount takerGets; // Remainder of acct's offer
2490 STAmount takerPays; //
2491
2492 // Constructor with takerGets/takerPays
2493 TestData(
2494 std::string&& account, // Account operated on
2495 STAmount fundXrp, // XRP acct funded with
2496 STAmount fundUsd, // USD acct funded with
2497 STAmount gwGets, // gw's offer
2498 STAmount gwPays, //
2499 STAmount acctGets, // acct's offer
2500 STAmount acctPays, //
2501 TER tec, // Returned tec code
2502 STAmount spentXrp, // Amount removed from fundXrp
2503 STAmount finalUsd, // Final USD balance on acct
2504 int offers, // Offers on acct
2505 int owners, // Owners on acct
2506 STAmount takerGets, // Remainder of acct's offer
2507 STAmount takerPays) //
2508 : account(std::move(account))
2509 , fundXrp(std::move(fundXrp))
2510 , fundUSD(std::move(fundUsd))
2511 , gwGets(std::move(gwGets))
2512 , gwPays(std::move(gwPays))
2513 , acctGets(std::move(acctGets))
2514 , acctPays(std::move(acctPays))
2515 , tec(tec)
2516 , spentXrp(std::move(spentXrp))
2517 , finalUsd(std::move(finalUsd))
2518 , offers(offers)
2519 , owners(owners)
2520 , takerGets(std::move(takerGets))
2521 , takerPays(std::move(takerPays))
2522 {
2523 }
2524
2525 // Constructor without takerGets/takerPays
2526 TestData(
2527 std::string&& account, // Account operated on
2528 STAmount const& fundXrp, // XRP acct funded with
2529 STAmount const& fundUsd, // USD acct funded with
2530 STAmount const& gwGets, // gw's offer
2531 STAmount const& gwPays, //
2532 STAmount const& acctGets, // acct's offer
2533 STAmount const& acctPays, //
2534 TER tec, // Returned tec code
2535 STAmount const& spentXrp, // Amount removed from fundXrp
2536 STAmount const& finalUsd, // Final USD balance on acct
2537 int offers, // Offers on acct
2538 int owners) // Owners on acct
2539 : TestData(
2540 std::move(account),
2541 fundXrp,
2542 fundUsd,
2543 gwGets,
2544 gwPays,
2545 acctGets,
2546 acctPays,
2547 tec,
2548 spentXrp,
2549 finalUsd,
2550 offers,
2551 owners,
2552 STAmount{0},
2553 STAmount{0})
2554 {
2555 }
2556 };
2557
2558 // clang-format off
2559 TestData const tests[]{
2560 // acct pays XRP
2561 // acct fundXrp fundUSD gwGets gwPays acctGets acctPays tec spentXrp finalUSD offers owners takerGets takerPays
2562 {"ann", XRP(10) + reserve(env, 0) + 1 * f, usd( 0), XRP(10), usd( 5), usd(10), XRP(10), tecINSUF_RESERVE_OFFER, XRP( 0) + (1 * f), usd( 0), 0, 0},
2563 {"bev", XRP(10) + reserve(env, 1) + 1 * f, usd( 0), XRP(10), usd( 5), usd(10), XRP(10), tesSUCCESS, XRP( 0) + (1 * f), usd( 0), 1, 1, XRP(10), usd(10)},
2564 {"cam", XRP(10) + reserve(env, 0) + 1 * f, usd( 0), XRP(10), usd(10), usd(10), XRP(10), tesSUCCESS, XRP( 10) + (1 * f), usd(10), 0, 1},
2565 {"deb", XRP(10) + reserve(env, 0) + 1 * f, usd( 0), XRP(10), usd(20), usd(10), XRP(10), tesSUCCESS, XRP( 10) + (1 * f), usd(20), 0, 1},
2566 {"eve", XRP(10) + reserve(env, 0) + 1 * f, usd( 0), XRP(10), usd(20), usd( 5), XRP( 5), tesSUCCESS, XRP( 5) + (1 * f), usd(10), 0, 1},
2567 {"flo", XRP(10) + reserve(env, 0) + 1 * f, usd( 0), XRP(10), usd(20), usd(20), XRP(20), tesSUCCESS, XRP( 10) + (1 * f), usd(20), 0, 1},
2568 {"gay", XRP(20) + reserve(env, 1) + 1 * f, usd( 0), XRP(10), usd(20), usd(20), XRP(20), tesSUCCESS, XRP( 10) + (1 * f), usd(20), 0, 1},
2569 {"hye", XRP(20) + reserve(env, 2) + 1 * f, usd( 0), XRP(10), usd(20), usd(20), XRP(20), tesSUCCESS, XRP( 10) + (1 * f), usd(20), 1, 2, XRP(10), usd(10)},
2570 // acct pays USD
2571 {"meg", reserve(env, 1) + 2 * f, usd(10), usd(10), XRP( 5), XRP(10), usd(10), tecINSUF_RESERVE_OFFER, XRP( 0) + (2 * f), usd(10), 0, 1},
2572 {"nia", reserve(env, 2) + 2 * f, usd(10), usd(10), XRP( 5), XRP(10), usd(10), tesSUCCESS, XRP( 0) + (2 * f), usd(10), 1, 2, usd(10), XRP(10)},
2573 {"ova", reserve(env, 1) + 2 * f, usd(10), usd(10), XRP(10), XRP(10), usd(10), tesSUCCESS, XRP(-10) + (2 * f), usd( 0), 0, 1},
2574 {"pam", reserve(env, 1) + 2 * f, usd(10), usd(10), XRP(20), XRP(10), usd(10), tesSUCCESS, XRP(-20) + (2 * f), usd( 0), 0, 1},
2575 {"qui", reserve(env, 1) + 2 * f, usd(10), usd(20), XRP(40), XRP(10), usd(10), tesSUCCESS, XRP(-20) + (2 * f), usd( 0), 0, 1},
2576 {"rae", reserve(env, 2) + 2 * f, usd(10), usd( 5), XRP( 5), XRP(10), usd(10), tesSUCCESS, XRP( -5) + (2 * f), usd( 5), 1, 2, usd( 5), XRP( 5)},
2577 {"sue", reserve(env, 2) + 2 * f, usd(10), usd( 5), XRP(10), XRP(10), usd(10), tesSUCCESS, XRP(-10) + (2 * f), usd( 5), 1, 2, usd( 5), XRP( 5)},
2578 };
2579 // clang-format on
2580
2581 auto const zeroUsd = usd(0);
2582 for (auto const& t : tests)
2583 {
2584 // Make sure gateway has no current offers.
2585 env.require(offers(gw, 0));
2586
2587 auto const acct = Account(t.account);
2588
2589 env.fund(t.fundXrp, acct);
2590 env.close();
2591
2592 // Optionally give acct some USD. This is not part of the test,
2593 // so we assume that acct has sufficient USD to cover the reserve
2594 // on the trust line.
2595 if (t.fundUSD != zeroUsd)
2596 {
2597 musd.authorize({.account = acct});
2598 env.close();
2599 env(pay(gw, acct, t.fundUSD));
2600 env.close();
2601 }
2602
2603 env(offer(gw, t.gwGets, t.gwPays));
2604 env.close();
2605 std::uint32_t const gwOfferSeq = env.seq(gw) - 1;
2606
2607 // Acct creates a tfSell offer. This is the heart of the test.
2608 env(offer(acct, t.acctGets, t.acctPays, tfSell), Ter(t.tec));
2609 env.close();
2610 std::uint32_t const acctOfferSeq = env.seq(acct) - 1;
2611
2612 // Check results
2613 BEAST_EXPECT(env.balance(acct, usd) == t.finalUsd);
2614 BEAST_EXPECT(env.balance(acct, xrpIssue()) == t.fundXrp - t.spentXrp);
2615 env.require(offers(acct, t.offers));
2616 env.require(Owners(acct, t.owners));
2617
2618 if (t.offers != 0)
2619 {
2620 auto const acctOffers = offersOnAccount(env, acct);
2621 if (!acctOffers.empty())
2622 {
2623 BEAST_EXPECT(acctOffers.size() == 1);
2624 auto const& acctOffer = *(acctOffers.front());
2625
2626 BEAST_EXPECT(acctOffer[sfLedgerEntryType] == ltOFFER);
2627 BEAST_EXPECT(acctOffer[sfTakerGets] == t.takerGets);
2628 BEAST_EXPECT(acctOffer[sfTakerPays] == t.takerPays);
2629 }
2630 }
2631
2632 // Give the next loop a clean slate by canceling any left-overs
2633 // in the offers.
2634 env(offerCancel(acct, acctOfferSeq));
2635 env(offerCancel(gw, gwOfferSeq));
2636 env.close();
2637 }
2638 }
2639
2640 void
2642 {
2643 // Test a number of different corner cases regarding offer crossing
2644 // when both the tfSell flag and tfFillOrKill flags are set.
2645 testcase("Combine tfSell with tfFillOrKill");
2646
2647 using namespace jtx;
2648
2649 auto const gw = Account("gateway");
2650 auto const alice = Account("alice");
2651 auto const bob = Account("bob");
2652
2653 Env env{*this, features};
2654
2655 env.fund(XRP(10'000'000), gw, alice, bob);
2656
2657 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {bob}});
2658
2659 // bob offers XRP for USD.
2660 env(pay(gw, bob, usd(100)));
2661 env.close();
2662 env(offer(bob, XRP(2'000), usd(20)));
2663 env.close();
2664 {
2665 // alice submits a tfSell | tfFillOrKill offer that does not cross.
2666 env(offer(alice, usd(21), XRP(2'100), tfSell | tfFillOrKill), Ter(tecKILLED));
2667 env.close();
2668 env.require(Balance(alice, usd(kNone)));
2669 env.require(offers(alice, 0));
2670 env.require(Balance(bob, usd(100)));
2671 }
2672 {
2673 // alice submits a tfSell | tfFillOrKill offer that crosses.
2674 // Even though tfSell is present it doesn't matter this time.
2675 env(offer(alice, usd(20), XRP(2'000), tfSell | tfFillOrKill));
2676 env.close();
2677 env.require(Balance(alice, usd(20)));
2678 env.require(offers(alice, 0));
2679 env.require(Balance(bob, usd(80)));
2680 }
2681 {
2682 // alice submits a tfSell | tfFillOrKill offer that crosses and
2683 // returns more than was asked for (because of the tfSell flag).
2684 env(offer(bob, XRP(2'000), usd(20)));
2685 env.close();
2686 env(offer(alice, usd(10), XRP(1'500), tfSell | tfFillOrKill));
2687 env.close();
2688 env.require(Balance(alice, usd(35)));
2689 env.require(offers(alice, 0));
2690 env.require(Balance(bob, usd(65)));
2691 }
2692 {
2693 // alice submits a tfSell | tfFillOrKill offer that doesn't cross.
2694 // This would have succeeded with a regular tfSell, but the
2695 // fillOrKill prevents the transaction from crossing since not
2696 // all of the offer is consumed.
2697
2698 // We're using bob's left-over offer for XRP(500), USD(5)
2699 env(offer(alice, usd(1), XRP(501), tfSell | tfFillOrKill), Ter(tecKILLED));
2700 env.close();
2701 env.require(Balance(alice, usd(35)));
2702 env.require(offers(alice, 0));
2703 env.require(Balance(bob, usd(65)));
2704 }
2705 {
2706 // Alice submits a tfSell | tfFillOrKill offer that finishes
2707 // off the remainder of bob's offer.
2708
2709 // We're using bob's left-over offer for XRP(500), USD(5)
2710 env(offer(alice, usd(1), XRP(500), tfSell | tfFillOrKill));
2711 env.close();
2712 env.require(Balance(alice, usd(40)));
2713 env.require(offers(alice, 0));
2714 env.require(Balance(bob, usd(60)));
2715 }
2716 }
2717
2718 void
2720 {
2721 testcase("Transfer Rate Offer");
2722
2723 using namespace jtx;
2724 auto const gw1 = Account("gateway1");
2725
2726 auto test = [&](auto&& issue1, auto&& issue2) {
2727 Env env{*this, features};
2728
2729 // The fee that's charged for transactions.
2730 auto const fee = env.current()->fees().base;
2731
2732 env.fund(XRP(100'000), gw1);
2733 env.close();
2734
2735 auto const usd =
2736 issue1({.env = env, .token = "USD", .issuer = gw1, .transferFee = 25'000});
2737 using tUSD = std::decay_t<decltype(usd)>;
2738 {
2739 auto const ann = Account("ann");
2740 auto const bob = Account("bob");
2741 env.fund(XRP(100) + reserve(env, 2) + (fee * 2), ann, bob);
2742 env.close();
2743
2744 if constexpr (std::is_same_v<tUSD, MPT>)
2745 {
2746 auto musd = MPTTester(env, gw1, usd);
2747 musd.authorize({.account = ann});
2748 musd.authorize({.account = bob});
2749 }
2750 else
2751 {
2752 env(trust(ann, usd(20'000)));
2753 env(trust(bob, usd(20'000)));
2754 env.close();
2755 }
2756
2757 env(pay(gw1, bob, usd(12'500)));
2758 env.close();
2759
2760 // bob offers to sell USD(100) for XRP. alice takes bob's
2761 // offer. Notice that although bob only offered USD(100),
2762 // USD(125) was removed from his account due to the gateway fee.
2763 //
2764 // A comparable payment would look like this:
2765 // env (pay (bob, alice, USD(100)), Sendmax(USD(125)))
2766 env(offer(bob, XRP(1), usd(10'000)));
2767 env.close();
2768
2769 env(offer(ann, usd(10'000), XRP(1)));
2770 env.close();
2771
2772 env.require(Balance(ann, usd(10'000)));
2773 env.require(Balance(ann, XRP(99) + reserve(env, 2)));
2774 env.require(offers(ann, 0));
2775
2776 env.require(Balance(bob, usd(0)));
2777 env.require(Balance(bob, XRP(101) + reserve(env, 2)));
2778 env.require(offers(bob, 0));
2779 }
2780 {
2781 // Reverse the order, so the offer in the books is to sell XRP
2782 // in return for USD. Gateway rate should still apply
2783 // identically.
2784 auto const che = Account("che");
2785 auto const deb = Account("deb");
2786 env.fund(XRP(100) + reserve(env, 2) + (fee * 2), che, deb);
2787 env.close();
2788
2789 if constexpr (std::is_same_v<tUSD, MPT>)
2790 {
2791 auto musd = MPTTester(env, gw1, usd);
2792 musd.authorize({.account = che});
2793 musd.authorize({.account = deb});
2794 }
2795 else
2796 {
2797 env(trust(che, usd(20'000)));
2798 env(trust(deb, usd(20'000)));
2799 env.close();
2800 }
2801
2802 env(pay(gw1, deb, usd(12'500)));
2803 env.close();
2804
2805 env(offer(che, usd(10'000), XRP(1)));
2806 env.close();
2807
2808 env(offer(deb, XRP(1), usd(10'000)));
2809 env.close();
2810
2811 env.require(Balance(che, usd(10'000)));
2812 env.require(Balance(che, XRP(99) + reserve(env, 2)));
2813 env.require(offers(che, 0));
2814
2815 env.require(Balance(deb, usd(0)));
2816 env.require(Balance(deb, XRP(101) + reserve(env, 2)));
2817 env.require(offers(deb, 0));
2818 }
2819 {
2820 auto const eve = Account("eve");
2821 auto const fyn = Account("fyn");
2822
2823 env.fund(XRP(20'000) + (fee * 2), eve, fyn);
2824 env.close();
2825
2826 if constexpr (std::is_same_v<tUSD, MPT>)
2827 {
2828 auto musd = MPTTester(env, gw1, usd);
2829 musd.authorize({.account = eve});
2830 musd.authorize({.account = fyn});
2831 }
2832 else
2833 {
2834 env(trust(eve, usd(20'000)));
2835 env(trust(fyn, usd(20'000)));
2836 env.close();
2837 }
2838
2839 env(pay(gw1, eve, usd(10'000)));
2840 env(pay(gw1, fyn, usd(10'000)));
2841 env.close();
2842
2843 // This test verifies that the amount removed from an offer
2844 // accounts for the transfer fee that is removed from the
2845 // account but not from the remaining offer.
2846 env(offer(eve, usd(1'000), XRP(4'000)));
2847 env.close();
2848 std::uint32_t const eveOfferSeq = env.seq(eve) - 1;
2849
2850 env(offer(fyn, XRP(2'000), usd(500)));
2851 env.close();
2852
2853 env.require(Balance(eve, usd(10'500)));
2854 env.require(Balance(eve, XRP(18'000)));
2855 auto const evesOffers = offersOnAccount(env, eve);
2856 BEAST_EXPECT(evesOffers.size() == 1);
2857 if (!evesOffers.empty())
2858 {
2859 auto const& evesOffer = *(evesOffers.front());
2860 BEAST_EXPECT(evesOffer[sfLedgerEntryType] == ltOFFER);
2861 BEAST_EXPECT(evesOffer[sfTakerGets] == XRP(2'000));
2862 BEAST_EXPECT(evesOffer[sfTakerPays] == usd(500));
2863 }
2864 env(offerCancel(eve, eveOfferSeq)); // For later tests
2865
2866 env.require(Balance(fyn, usd(9'375)));
2867 env.require(Balance(fyn, XRP(22'000)));
2868 env.require(offers(fyn, 0));
2869 }
2870 // Start messing with two non-native currencies.
2871 auto const gw2 = Account("gateway2");
2872
2873 env.fund(XRP(100'000), gw2);
2874 env.close();
2875
2876 auto const eur =
2877 issue2({.env = env, .token = "EUR", .issuer = gw2, .transferFee = 50'000});
2878 using tEUR = std::decay_t<decltype(eur)>;
2879 {
2880 // Remove XRP from the equation. Give the two currencies two
2881 // different transfer rates so we can see both transfer rates
2882 // apply in the same transaction.
2883 auto const gay = Account("gay");
2884 auto const hal = Account("hal");
2885 env.fund(reserve(env, 3) + (fee * 3), gay, hal);
2886 env.close();
2887
2888 if constexpr (std::is_same_v<tUSD, MPT>)
2889 {
2890 auto musd = MPTTester(env, gw1, usd);
2891 musd.authorize({.account = gay});
2892 musd.authorize({.account = hal});
2893 }
2894 else
2895 {
2896 env(trust(gay, usd(20'000)));
2897 env(trust(hal, usd(20'000)));
2898 env.close();
2899 }
2900 if constexpr (std::is_same_v<tEUR, MPT>)
2901 {
2902 auto meur = MPTTester(env, gw2, eur);
2903 meur.authorize({.account = gay});
2904 meur.authorize({.account = hal});
2905 }
2906 else
2907 {
2908 env(trust(gay, eur(20'000)));
2909 env(trust(hal, eur(20'000)));
2910 env.close();
2911 }
2912
2913 env(pay(gw1, gay, usd(12'500)));
2914 env(pay(gw2, hal, eur(150)));
2915 env.close();
2916
2917 env(offer(gay, eur(100), usd(10'000)));
2918 env.close();
2919
2920 env(offer(hal, usd(10'000), eur(100)));
2921 env.close();
2922
2923 env.require(Balance(gay, usd(0)));
2924 env.require(Balance(gay, eur(100)));
2925 env.require(Balance(gay, reserve(env, 3)));
2926 env.require(offers(gay, 0));
2927
2928 env.require(Balance(hal, usd(10'000)));
2929 env.require(Balance(hal, eur(0)));
2930 env.require(Balance(hal, reserve(env, 3)));
2931 env.require(offers(hal, 0));
2932 }
2933
2934 {
2935 // Make sure things work right when we're auto-bridging as well.
2936 auto const ova = Account("ova");
2937 auto const pat = Account("pat");
2938 auto const qae = Account("qae");
2939 env.fund(XRP(2) + reserve(env, 3) + (fee * 3), ova, pat, qae);
2940 env.close();
2941
2942 // o ova has USD but wants XRP.
2943 // o pat has XRP but wants EUR.
2944 // o qae has EUR but wants USD.
2945 if constexpr (std::is_same_v<tUSD, MPT>)
2946 {
2947 auto musd = MPTTester(env, gw1, usd);
2948 musd.authorize({.account = ova});
2949 musd.authorize({.account = pat});
2950 musd.authorize({.account = qae});
2951 }
2952 else
2953 {
2954 env(trust(ova, usd(20'000)));
2955 env(trust(pat, usd(20'000)));
2956 env(trust(qae, usd(20'000)));
2957 env.close();
2958 }
2959 if constexpr (std::is_same_v<tEUR, MPT>)
2960 {
2961 auto meur = MPTTester(env, gw2, eur);
2962 meur.authorize({.account = ova});
2963 meur.authorize({.account = pat});
2964 meur.authorize({.account = qae});
2965 }
2966 else
2967 {
2968 env(trust(ova, eur(20'000)));
2969 env(trust(pat, eur(20'000)));
2970 env(trust(qae, eur(20'000)));
2971 env.close();
2972 }
2973
2974 env(pay(gw1, ova, usd(12'500)));
2975 env(pay(gw2, qae, eur(150)));
2976 env.close();
2977
2978 env(offer(ova, XRP(2), usd(10'000)));
2979 env(offer(pat, eur(100), XRP(2)));
2980 env.close();
2981
2982 env(offer(qae, usd(10'000), eur(100)));
2983 env.close();
2984
2985 env.require(Balance(ova, usd(0)));
2986 env.require(Balance(ova, eur(0)));
2987 env.require(Balance(ova, XRP(4) + reserve(env, 3)));
2988
2989 // In pre-flow code ova's offer is left empty in the ledger.
2990 auto const ovasOffers = offersOnAccount(env, ova);
2991 if (!ovasOffers.empty())
2992 {
2993 BEAST_EXPECT(ovasOffers.size() == 1);
2994 auto const& ovasOffer = *(ovasOffers.front());
2995
2996 BEAST_EXPECT(ovasOffer[sfLedgerEntryType] == ltOFFER);
2997 BEAST_EXPECT(ovasOffer[sfTakerGets] == usd(0));
2998 BEAST_EXPECT(ovasOffer[sfTakerPays] == XRP(0));
2999 }
3000
3001 env.require(Balance(pat, usd(0)));
3002 env.require(Balance(pat, eur(100)));
3003 env.require(Balance(pat, XRP(0) + reserve(env, 3)));
3004 env.require(offers(pat, 0));
3005
3006 env.require(Balance(qae, usd(10'000)));
3007 env.require(Balance(qae, eur(0)));
3008 env.require(Balance(qae, XRP(2) + reserve(env, 3)));
3009 env.require(offers(qae, 0));
3010 }
3011 };
3013
3014 // Payment trIn: MPT transfer fee must be charged when the payment
3015 // destination is the MPT issuer and MPT crosses the DEX (1-hop).
3016 // Bug: rate() returned parity because strandDst_ == MPT issuer.
3017 // Fix: parity only when this asset IS the final delivered asset.
3018 {
3019 auto const gw = Account("gw_tr1");
3020 auto const alice = Account("alice_tr1");
3021 auto const bob = Account("bob_tr1");
3022
3023 Env env{*this, features};
3024 env.fund(XRP(10'000), gw, alice, bob);
3025 env.close();
3026
3027 MPT const usd = MPTTester(
3028 {.env = env, .issuer = gw, .holders = {alice, bob}, .transferFee = 25'000});
3029
3030 // alice needs MPT(1250): MPT(1000) to bob's offer + MPT(250) transfer fee (25%)
3031 env(pay(gw, alice, usd(1'250)));
3032 // bob's offer: give XRP(1000), want MPT(1000)
3033 env(offer(bob, usd(1'000), XRP(1'000)));
3034 env.close();
3035
3036 // alice pays gw (MPT issuer) XRP(1000) using MPT as source
3037 // strand: alice -> [MPT/XRP BookStep] -> gw
3038 // strandDst_ = gw = MPT issuer, strandDeliver_ = XRP
3039 // trIn = rate(MPT, gw): fix charges 25% (MPT != strandDeliver_)
3040 env(pay(alice, gw, XRP(1'000)), Path(~XRP), Sendmax(usd(1'250)));
3041 env.close();
3042
3043 // alice consumed all MPT(1250): MPT(1000) to bob + MPT(250) fee
3044 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
3045 // bob received MPT(1000) net
3046 BEAST_EXPECT(env.balance(bob, usd) == usd(1'000));
3047 }
3048
3049 // Payment trIn (2-hop): MPT transfer fee must be charged when MPT is
3050 // intermediate and the destination is the MPT issuer.
3051 // BookStep2(MPT/XRP) prevStep=BookStep1 returns redeems direction
3052 // (ownerPaysTransferFee_=false for Payment), so trIn applies.
3053 // Bug: parity because strandDst_ == MPT issuer.
3054 // Fix: 25% fee because MPT != strandDeliver_(XRP).
3055 {
3056 auto const gw = Account("gw_tr2");
3057 auto const gw2 = Account("gw2_tr2");
3058 auto const alice = Account("alice_tr2");
3059 auto const bob = Account("bob_tr2");
3060 auto const carol = Account("carol_tr2");
3061
3062 Env env{*this, features};
3063 env.fund(XRP(10'000), gw, gw2, alice, bob, carol);
3064 env.close();
3065
3066 MPT const musd = MPTTester(
3067 {.env = env, .issuer = gw, .holders = {bob, carol}, .transferFee = 25'000});
3068 auto const gusd = gw2["USD"];
3069
3070 env(trust(alice, gusd(10'000)));
3071 env(trust(bob, gusd(10'000)));
3072 env.close();
3073
3074 env(pay(gw2, alice, gusd(1'000)));
3075 env(pay(gw, bob, musd(1'000)));
3076 env.close();
3077
3078 // bob's offer: give MPT(1000), want GUSD(1000)
3079 env(offer(bob, gusd(1'000), musd(1'000)));
3080 // carol's offer: give XRP(800), want MPT(800)
3081 env(offer(carol, musd(800), XRP(800)));
3082 env.close();
3083
3084 // Payment: alice GUSD -> [BookStep1: GUSD/MUSD] -> [BookStep2: MUSD/XRP] -> gw XRP
3085 // strandDst_ = gw = MPT issuer, strandDeliver_ = XRP
3086 // BookStep2 trIn: fix = 1.25 -> upstream needs MUSD(1000) for carol's MUSD(800) offer
3087 // => alice must provide full GUSD(1000) to bob's offer; without fix alice only pays
3088 // GUSD(800)
3089 env(pay(alice, gw, XRP(800)), Path(~musd), Sendmax(gusd(1'000)));
3090 env.close();
3091
3092 // alice spent all GUSD(1000); bug would leave GUSD(200) unspent
3093 BEAST_EXPECT(env.balance(alice, gusd) == gusd(0));
3094 // bob gave MPT(1000) and received GUSD(1000)
3095 BEAST_EXPECT(env.balance(bob, musd) == musd(0));
3096 // carol received MPT(800) net (MPT(200) went to gw as fee)
3097 BEAST_EXPECT(env.balance(carol, musd) == musd(800));
3098 }
3099 }
3100
3101 void
3103 {
3104 // The following test verifies some correct but slightly surprising
3105 // behavior in offer crossing. The scenario:
3106 //
3107 // o An entity has created one or more offers.
3108 // o The entity creates another offer that can be directly crossed
3109 // (not autobridged) by the previously created offer(s).
3110 // o Rather than self crossing the offers, delete the old offer(s).
3111 //
3112 // See a more complete explanation in the comments for
3113 // BookOfferCrossingStep::limitSelfCrossQuality().
3114 //
3115 // Note that, in this particular example, one offer causes several
3116 // crossable offers (worth considerably more than the new offer)
3117 // to be removed from the book.
3118 using namespace jtx;
3119
3120 auto const gw = Account("gateway");
3121
3122 Env env{*this, features};
3123
3124 // The fee that's charged for transactions.
3125 auto const fee = env.current()->fees().base;
3126 auto const startBalance = XRP(1'000'000);
3127
3128 env.fund(startBalance + (fee * 5), gw);
3129 env.close();
3130
3131 MPT const usd = MPTTester({.env = env, .issuer = gw});
3132
3133 env(offer(gw, usd(60), XRP(600)));
3134 env.close();
3135 env(offer(gw, usd(60), XRP(600)));
3136 env.close();
3137 env(offer(gw, usd(60), XRP(600)));
3138 env.close();
3139
3140 // three offers + MPTokenIssuance
3141 env.require(Owners(gw, 4));
3142 env.require(Balance(gw, startBalance + fee));
3143
3144 auto gwOffers = offersOnAccount(env, gw);
3145 BEAST_EXPECT(gwOffers.size() == 3);
3146 for (auto const& offerPtr : gwOffers)
3147 {
3148 auto const& offer = *offerPtr;
3149 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
3150 BEAST_EXPECT(offer[sfTakerGets] == XRP(600));
3151 BEAST_EXPECT(offer[sfTakerPays] == usd(60));
3152 }
3153
3154 // Since this offer crosses the first offers, the previous offers
3155 // will be deleted and this offer will be put on the order book.
3156 env(offer(gw, XRP(1'000), usd(100)));
3157 env.close();
3158 env.require(Owners(gw, 2));
3159 env.require(offers(gw, 1));
3160 env.require(Balance(gw, startBalance));
3161
3162 gwOffers = offersOnAccount(env, gw);
3163 BEAST_EXPECT(gwOffers.size() == 1);
3164 for (auto const& offerPtr : gwOffers)
3165 {
3166 auto const& offer = *offerPtr;
3167 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
3168 BEAST_EXPECT(offer[sfTakerGets] == usd(100));
3169 BEAST_EXPECT(offer[sfTakerPays] == XRP(1'000));
3170 }
3171 }
3172
3173 void
3175 {
3176 using namespace jtx;
3177
3178 auto const gw1 = Account("gateway1");
3179 auto const gw2 = Account("gateway2");
3180 auto const alice = Account("alice");
3181
3182 auto test = [&](auto&& issue1, auto&& issue2) {
3183 Env env{*this, features};
3184
3185 env.fund(XRP(1'000'000), gw1, gw2);
3186 env.close();
3187
3188 auto const usd = issue1({.env = env, .token = "USD", .issuer = gw1});
3189 using tUSD = std::decay_t<decltype(usd)>;
3190 auto const eur = issue2({.env = env, .token = "EUR", .issuer = gw2});
3191 using tEUR = std::decay_t<decltype(eur)>;
3192
3193 // The fee that's charged for transactions.
3194 auto const f = env.current()->fees().base;
3195
3196 // Test cases
3197 struct TestData
3198 {
3199 std::string acct; // Account operated on
3200 STAmount fundXRP; // XRP acct funded with
3201 STAmount fundUSD; // USD acct funded with
3202 STAmount fundEUR; // EUR acct funded with
3203 TER firstOfferTec; // tec code on first offer
3204 TER secondOfferTec; // tec code on second offer
3205 };
3206
3207 // clang-format off
3208 TestData const tests[]{
3209 // acct fundXRP fundUSD fundEUR firstOfferTec secondOfferTec
3210 {"ann", reserve(env, 3) + f * 4, usd(1000), eur(1000), tesSUCCESS, tesSUCCESS},
3211 {"bev", reserve(env, 3) + f * 4, usd( 1), eur(1000), tesSUCCESS, tesSUCCESS},
3212 {"cam", reserve(env, 3) + f * 4, usd(1000), eur( 1), tesSUCCESS, tesSUCCESS},
3213 {"deb", reserve(env, 3) + f * 4, usd( 0), eur( 1), tesSUCCESS, tecUNFUNDED_OFFER},
3214 {"eve", reserve(env, 3) + f * 4, usd( 1), eur( 0), tecUNFUNDED_OFFER, tesSUCCESS},
3215 {"flo", reserve(env, 3) + 0, usd(1000), eur(1000), tecINSUF_RESERVE_OFFER, tecINSUF_RESERVE_OFFER},
3216 };
3217 //clang-format on
3218
3219 for (auto const& t : tests)
3220 {
3221 auto const acct = Account{t.acct};
3222 env.fund(t.fundXRP, acct);
3223 env.close();
3224
3225 if constexpr (std::is_same_v<tUSD, MPT>)
3226 {
3227 auto musd = MPTTester(env, gw1, usd);
3228 musd.authorize({.account = acct});
3229 }
3230 else
3231 {
3232 env(trust(acct, usd(1'000)));
3233 env.close();
3234 }
3235 if constexpr (std::is_same_v<tEUR, MPT>)
3236 {
3237 auto meur = MPTTester(env, gw2, eur);
3238 meur.authorize({.account = acct});
3239 }
3240 else
3241 {
3242 env(trust(acct, eur(1'000)));
3243 env.close();
3244 }
3245
3246 if (t.fundUSD > usd(0))
3247 env(pay(gw1, acct, t.fundUSD));
3248 if (t.fundEUR > eur(0))
3249 env(pay(gw2, acct, t.fundEUR));
3250 env.close();
3251
3252 env(offer(acct, usd(500), eur(600)), Ter(t.firstOfferTec));
3253 env.close();
3254 std::uint32_t const firstOfferSeq = env.seq(acct) - 1;
3255
3256 int offerCount = t.firstOfferTec == tesSUCCESS ? 1 : 0;
3257 env.require(Owners(acct, 2 + offerCount));
3258 env.require(Balance(acct, t.fundUSD));
3259 env.require(Balance(acct, t.fundEUR));
3260
3261 auto acctOffers = offersOnAccount(env, acct);
3262 BEAST_EXPECT(acctOffers.size() == offerCount);
3263 for (auto const& offerPtr : acctOffers)
3264 {
3265 auto const& offer = *offerPtr;
3266 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
3267 BEAST_EXPECT(offer[sfTakerGets] == eur(600));
3268 BEAST_EXPECT(offer[sfTakerPays] == usd(500));
3269 }
3270
3271 env(offer(acct, eur(600), usd(500)), Ter(t.secondOfferTec));
3272 env.close();
3273 std::uint32_t const secondOfferSeq = env.seq(acct) - 1;
3274
3275 offerCount = t.secondOfferTec == tesSUCCESS ? 1 : offerCount;
3276 env.require(Owners(acct, 2 + offerCount));
3277 env.require(Balance(acct, t.fundUSD));
3278 env.require(Balance(acct, t.fundEUR));
3279
3280 acctOffers = offersOnAccount(env, acct);
3281 BEAST_EXPECT(acctOffers.size() == offerCount);
3282 for (auto const& offerPtr : acctOffers)
3283 {
3284 auto const& offer = *offerPtr;
3285 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
3286 if (offer[sfSequence] == firstOfferSeq)
3287 {
3288 BEAST_EXPECT(offer[sfTakerGets] == eur(600));
3289 BEAST_EXPECT(offer[sfTakerPays] == usd(500));
3290 }
3291 else
3292 {
3293 BEAST_EXPECT(offer[sfTakerGets] == usd(500));
3294 BEAST_EXPECT(offer[sfTakerPays] == eur(600));
3295 }
3296 }
3297
3298 // Remove any offers from acct for the next pass.
3299 env(offerCancel(acct, firstOfferSeq));
3300 env.close();
3301 env(offerCancel(acct, secondOfferSeq));
3302 env.close();
3303 }
3304 };
3306 }
3307
3308 void
3310 {
3311 testcase("Self Cross Offer");
3312 testSelfCrossOffer1(features);
3313 testSelfCrossOffer2(features);
3314 }
3315
3316 void
3318 {
3319 // Folks who issue their own currency have, in effect, as many
3320 // funds as they are trusted for. This test used to fail because
3321 // self-issuing was not properly checked. Verify that it works
3322 // correctly now.
3323 using namespace jtx;
3324
3325 Env env{*this, features};
3326
3327 auto const alice = Account("alice");
3328 auto const bob = Account("bob");
3329 auto const f = env.current()->fees().base;
3330
3331 env.fund(XRP(50'000) + f, alice, bob);
3332 env.close();
3333
3334 MPT const usd = MPTTester({.env = env, .issuer = bob});
3335
3336 env(offer(alice, usd(5'000), XRP(50'000)));
3337 env.close();
3338
3339 // This offer should take alice's offer up to Alice's reserve.
3340 env(offer(bob, XRP(50'000), usd(5'000)));
3341 env.close();
3342
3343 // alice's offer should have been removed, since she's down to her
3344 // XRP reserve.
3345 env.require(Balance(alice, XRP(250)));
3346 env.require(Owners(alice, 1));
3347 env.require(mptokens(alice, 1));
3348
3349 // However bob's offer should be in the ledger, since it was not
3350 // fully crossed.
3351 auto const bobOffers = offersOnAccount(env, bob);
3352 BEAST_EXPECT(bobOffers.size() == 1);
3353 for (auto const& offerPtr : bobOffers)
3354 {
3355 auto const& offer = *offerPtr;
3356 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
3357 BEAST_EXPECT(offer[sfTakerGets] == usd(25));
3358 BEAST_EXPECT(offer[sfTakerPays] == XRP(250));
3359 }
3360 }
3361
3362 void
3364 {
3365 // The offer crossing code expects that a DirectStep is always
3366 // preceded by a BookStep. In one instance the default path
3367 // was not matching that assumption. Here we recreate that case
3368 // so we can prove the bug stays fixed.
3369 testcase("Direct to Direct path");
3370
3371 using namespace jtx;
3372 auto const ann = Account("ann");
3373 auto const bob = Account("bob");
3374 auto const cam = Account("cam");
3375
3376 auto test = [&](auto&& issue1, auto&& issue2) {
3377 Env env{*this, features};
3378
3379 auto const fee = env.current()->fees().base;
3380 env.fund(reserve(env, 4) + (fee * 5), ann, bob, cam);
3381 env.close();
3382
3383 auto const aBux = issue1(
3384 {.env = env, .token = "AUX", .issuer = ann, .holders = {cam}});
3385 auto const bBux = issue2(
3386 {.env = env,
3387 .token = "BUX",
3388 .issuer = bob,
3389 .holders = {ann, cam}});
3390
3391 env(pay(ann, cam, aBux(35)));
3392 env(pay(bob, cam, bBux(35)));
3393
3394 env(offer(bob, aBux(30), bBux(30)));
3395 env.close();
3396
3397 // cam puts an offer on the books that her upcoming offer could
3398 // cross. But this offer should be deleted, not crossed, by her
3399 // upcoming offer.
3400 env(offer(cam, aBux(29), bBux(30), tfPassive));
3401 env.close();
3402 env.require(Balance(cam, aBux(35)));
3403 env.require(Balance(cam, bBux(35)));
3404 env.require(offers(cam, 1));
3405
3406 // This offer caused the assert.
3407 env(offer(cam, bBux(30), aBux(30)));
3408 env.close();
3409
3410 env.require(Balance(bob, aBux(30)));
3411 env.require(Balance(cam, aBux(5)));
3412 env.require(Balance(cam, bBux(65)));
3413 env.require(offers(cam, 0));
3414 };
3416 }
3417
3418 void
3420 {
3421 // The Flow offer crossing code used to assert if an offer was made
3422 // for more XRP than the offering account held. This unit test
3423 // reproduces that failing case.
3424 testcase("Self crossing low quality offer");
3425
3426 using namespace jtx;
3427
3428 Env env{*this, features};
3429
3430 auto const ann = Account("ann");
3431 auto const gw = Account("gateway");
3432
3433 auto const fee = env.current()->fees().base;
3434 env.fund(reserve(env, 2) + drops(9999640) + fee, ann);
3435 env.fund(reserve(env, 2) + (fee * 4), gw);
3436 env.close();
3437
3438 MPT const btc = MPTTester(
3439 {.env = env, .issuer = gw, .holders = {ann}, .transferFee = 2'000});
3440
3441 env(pay(gw, ann, btc(2'856)));
3442 env.close();
3443
3444 env(offer(ann, drops(365'611'702'030), btc(5'713)));
3445 env.close();
3446
3447 // This offer caused the assert.
3448 env(offer(ann, btc(687), drops(20'000'000'000)),
3450 }
3451
3452 void
3454 {
3455 // The Flow offer crossing code had a case where it was not rounding
3456 // the offer crossing correctly after a partial crossing. The
3457 // failing case was found on the network. Here we add the case to
3458 // the unit tests.
3459 testcase("Offer In Scaling");
3460
3461 using namespace jtx;
3462
3463 Env env{*this, features};
3464
3465 auto const gw = Account("gateway");
3466 auto const alice = Account("alice");
3467 auto const bob = Account("bob");
3468
3469 auto const fee = env.current()->fees().base;
3470 env.fund(reserve(env, 2) + drops(400'000'000'000) + fee, alice, bob);
3471 env.fund(reserve(env, 2) + (fee * 4), gw);
3472 env.close();
3473
3474 MPT const cny = MPTTester({.env = env, .issuer = gw, .holders = {bob}});
3475
3476 env(pay(gw, bob, cny(3'000'000)));
3477 env.close();
3478
3479 env(offer(bob, drops(5'400'000'000), cny(2'160'540)));
3480 env.close();
3481
3482 // This offer did not round result of partial crossing correctly.
3483 env(offer(alice, cny(135'620'001), drops(339'000'000'000)));
3484 env.close();
3485
3486 auto const aliceOffers = offersOnAccount(env, alice);
3487 BEAST_EXPECT(aliceOffers.size() == 1);
3488 for (auto const& offerPtr : aliceOffers)
3489 {
3490 auto const& offer = *offerPtr;
3491 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
3492 BEAST_EXPECT(offer[sfTakerGets] == drops(333'599'446'582));
3493 BEAST_EXPECT(offer[sfTakerPays] == cny(13'3459'461));
3494 }
3495 }
3496
3497 void
3499 {
3500 // After adding the previous case, there were still failing rounding
3501 // cases in Flow offer crossing. This one was because the gateway
3502 // transfer rate was not being correctly handled.
3503 testcase("Offer In Scaling With Xfer Rate");
3504
3505 using namespace jtx;
3506 auto const gw = Account("gateway");
3507 auto const alice = Account("alice");
3508 auto const bob = Account("bob");
3509
3510 auto test = [&](auto&& issue1, auto&& issue2) {
3511 Env env{*this, features};
3512
3513 auto const fee = env.current()->fees().base;
3514 env.fund(
3515 reserve(env, 2) + drops(400'000'000'000) + fee,
3516 alice,
3517 bob);
3518 env.fund(reserve(env, 2) + (fee * 4), gw);
3519 env.close();
3520
3521 auto const jpy = issue1(
3522 {.env = env,
3523 .token = "JPY",
3524 .issuer = gw,
3525 .holders = {alice},
3527 .transferFee = 2'000});
3528 auto const btc = issue2(
3529 {.env = env,
3530 .token = "BTC",
3531 .issuer = gw,
3532 .holders = {bob},
3534 .transferFee = 2'000});
3535
3536 env(pay(gw, alice, jpy(3'699'034'802'280'317)));
3537 env(pay(gw, bob, btc(115'672'255'914'031'100)));
3538 env.close();
3539
3540 env(offer(
3541 bob, jpy(1'241'913'390'770'747), btc(1'969'825'690'469'254)));
3542 env.close();
3543
3544 // This offer did not round result of partial crossing correctly.
3545 env(offer(
3546 alice, btc(5'507'568'706'427'876), jpy(3'472'696'773'391'072)));
3547 env.close();
3548
3549 auto const aliceOffers = offersOnAccount(env, alice);
3550 BEAST_EXPECT(aliceOffers.size() == 1);
3551 for (auto const& offerPtr : aliceOffers)
3552 {
3553 auto const& offer = *offerPtr;
3554 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
3555 // This test is similar to corresponding Offer_test, except
3556 // that JPY is scaled by 10**12 and BTC is scaled by 10**17.
3557 // There is a difference in the expected results.
3558 // Offer_test expects values
3559 // takerGets:2230.682446713524, takerPays: 0.035378
3560 // MPT test has the same order of magnitude for the scaled
3561 // values and the first 5 digits match. Is the difference due to
3562 // int arithmetics?
3563 BEAST_EXPECT(offer[sfTakerGets] == jpy(2'230'659'191'281'247));
3564 BEAST_EXPECT(offer[sfTakerPays] == btc(3'537'743'015'958'622));
3565 }
3566 };
3568 }
3569
3570 void
3572 {
3573 testcase("Self Pay Xfer Fee");
3574 // The old offer crossing code does not charge a transfer fee
3575 // if alice pays alice. That's different from how payments work.
3576 // Payments always charge a transfer fee even if the money is staying
3577 // in the same hands.
3578 //
3579 // What's an example where alice pays alice? There are three actors:
3580 // gw, alice, and bob.
3581 //
3582 // 1. gw issues BTC and USD. gw charges a 0.2% transfer fee.
3583 //
3584 // 2. alice makes an offer to buy XRP and sell USD.
3585 // 3. bob makes an offer to buy BTC and sell XRP.
3586 //
3587 // 4. alice now makes an offer to sell BTC and buy USD.
3588 //
3589 // This last offer crosses using auto-bridging.
3590 // o alice's last offer sells BTC to...
3591 // o bob' offer which takes alice's BTC and sells XRP to...
3592 // o alice's first offer which takes bob's XRP and sells USD to...
3593 // o alice's last offer.
3594 //
3595 // So alice sells USD to herself.
3596 //
3597 // There are six cases that we need to test:
3598 // o alice crosses her own offer on the first leg (BTC).
3599 // o alice crosses her own offer on the second leg (USD).
3600 // o alice crosses her own offers on both legs.
3601 // All three cases need to be tested:
3602 // o In reverse (alice has enough BTC to cover her offer) and
3603 // o Forward (alice owns less BTC than is in her final offer.
3604 //
3605 // It turns out that two of the forward cases fail for a different
3606 // reason. They are therefore commented out here, But they are
3607 // revisited in the testSelfPayUnlimitedFunds() unit test.
3608
3609 using namespace jtx;
3610 auto const gw = Account("gw");
3611
3612 auto test = [&](auto&& issue1, auto&& issue2) {
3613 Env env{*this, features};
3614 auto const baseFee = env.current()->fees().base.drops();
3615
3616 auto const startXrpBalance = XRP(4'000'000);
3617
3618 env.fund(startXrpBalance, gw);
3619 env.close();
3620
3621 auto const btc = issue1(
3622 {.env = env,
3623 .token = "BTC",
3624 .issuer = gw,
3625 .transferFee = 25'000});
3626 using tBTC = std::decay_t<decltype(btc)>;
3627 env.close();
3628 auto const usd = issue2(
3629 {.env = env,
3630 .token = "USD",
3631 .issuer = gw,
3632 .transferFee = 25'000});
3633 using tUSD = std::decay_t<decltype(usd)>;
3634 env.close();
3635
3636 // Test cases
3637 struct Actor
3638 {
3639 Account acct;
3640 int offers{}; // offers on account after crossing
3641 PrettyAmount xrp; // final expected after crossing
3642 PrettyAmount btc; // final expected after crossing
3643 PrettyAmount usd; // final expected after crossing
3644 };
3645 struct TestData
3646 {
3647 // The first three integers give the *index* in actors
3648 // to assign each of the three roles. By using indices it is
3649 // easy for alice to own the offer in the first leg, the second
3650 // leg, or both.
3651 std::size_t self{};
3652 std::size_t leg0{};
3653 std::size_t leg1{};
3654 PrettyAmount btcStart;
3655 std::vector<Actor> actors;
3656 };
3657
3658 // clang-format off
3659 TestData const tests[]{
3660 // btcStart --------------------- actor[0] --------------------- -------------------- actor[1] -------------------
3661 {0, 0, 1, btc(200), {{"ann", 0, drops(3900000'000000 - (4 * baseFee)), btc(200), usd(3000)}, {"abe", 0, drops(4100000'000000 - (3 * baseFee)), btc( 0), usd(750)}}}, // no BTC xfer fee
3662 {0, 1, 0, btc(200), {{"bev", 0, drops(4100000'000000 - (4 * baseFee)), btc( 75), usd(2000)}, {"bob", 0, drops(3900000'000000 - (3 * baseFee)), btc(100), usd( 0)}}}, // no USD xfer fee
3663 {0, 0, 0, btc(200), {{"cam", 0, drops(4000000'000000 - (5 * baseFee)), btc(200), usd(2000)} }}, // no xfer fee
3664 {0, 1, 0, btc( 50), {{"deb", 1, drops(4040000'000000 - (4 * baseFee)), btc( 0), usd(2000)}, {"dan", 1, drops(3960000'000000 - (3 * baseFee)), btc( 40), usd( 0)}}}, // no USD xfer fee
3665 };
3666 // clang-format on
3667
3668 for (auto const& t : tests)
3669 {
3670 Account const& self = t.actors[t.self].acct;
3671 Account const& leg0 = t.actors[t.leg0].acct;
3672 Account const& leg1 = t.actors[t.leg1].acct;
3673
3674 for (auto const& actor : t.actors)
3675 {
3676 env.fund(XRP(4'000'000), actor.acct);
3677 env.close();
3678
3679 if constexpr (std::is_same_v<tBTC, MPT>)
3680 {
3681 auto mbtc = MPTTester(env, gw, btc);
3682 mbtc.authorize({.account = actor.acct});
3683 }
3684 else
3685 {
3686 env(trust(actor.acct, btc(400)));
3687 env.close();
3688 }
3689 if constexpr (std::is_same_v<tUSD, MPT>)
3690 {
3691 auto musd = MPTTester(env, gw, usd);
3692 musd.authorize({.account = actor.acct});
3693 }
3694 else
3695 {
3696 env(trust(actor.acct, usd(8000)));
3697 env.close();
3698 }
3699 env.close();
3700 }
3701
3702 env(pay(gw, self, t.btcStart));
3703 env(pay(gw, self, usd(2'000)));
3704 if (self.id() != leg1.id())
3705 env(pay(gw, leg1, usd(2'000)));
3706 env.close();
3707
3708 // Get the initial offers in place. Remember their sequences
3709 // so we can delete them later.
3710 env(offer(leg0, btc(100), XRP(100'000), tfPassive));
3711 env.close();
3712 std::uint32_t const leg0OfferSeq = env.seq(leg0) - 1;
3713
3714 env(offer(leg1, XRP(100'000), usd(1'000), tfPassive));
3715 env.close();
3716 std::uint32_t const leg1OfferSeq = env.seq(leg1) - 1;
3717
3718 // This is the offer that matters.
3719 env(offer(self, usd(1'000), btc(100)));
3720 env.close();
3721 std::uint32_t const selfOfferSeq = env.seq(self) - 1;
3722
3723 // Verify results.
3724 for (auto const& actor : t.actors)
3725 {
3726 // Sometimes Taker crossing gets lazy about deleting offers.
3727 // Treat an empty offer as though it is deleted.
3728 auto actorOffers = offersOnAccount(env, actor.acct);
3729 auto const offerCount = std::distance(
3730 actorOffers.begin(),
3732 return (*offer)[sfTakerGets].signum() == 0;
3733 }).begin());
3734 BEAST_EXPECT(offerCount == actor.offers);
3735
3736 env.require(Balance(actor.acct, actor.xrp));
3737 env.require(Balance(actor.acct, actor.btc));
3738 env.require(Balance(actor.acct, actor.usd));
3739 }
3740 // Remove any offers that might be left hanging around. They
3741 // could bollix up later loops.
3742 env(offerCancel(leg0, leg0OfferSeq));
3743 env.close();
3744 env(offerCancel(leg1, leg1OfferSeq));
3745 env.close();
3746 env(offerCancel(self, selfOfferSeq));
3747 env.close();
3748 }
3749 };
3751 }
3752
3753 void
3755 {
3756 testcase("Self Pay Unlimited Funds");
3757 // The Taker offer crossing code recognized when Alice was paying
3758 // Alice the same denomination. In this case, as long as Alice
3759 // has a little bit of that denomination, it treats Alice as though
3760 // she has unlimited funds in that denomination.
3761 //
3762 // Huh? What kind of sense does that make?
3763 //
3764 // One way to think about it is to break a single payment into a
3765 // series of very small payments executed sequentially but very
3766 // quickly. Alice needs to pay herself 1 USD, but she only has
3767 // 0.01 USD. Alice says, "Hey Alice, let me pay you a penny."
3768 // Alice does this, taking the penny out of her pocket and then
3769 // putting it back in her pocket. Then she says, "Hey Alice,
3770 // I found another penny. I can pay you another penny." Repeat
3771 // these steps 100 times and Alice has paid herself 1 USD even though
3772 // she only owns 0.01 USD.
3773 //
3774 // That's all very nice, but the payment code does not support this
3775 // optimization. In part that's because the payment code can
3776 // operate on a whole batch of offers. As a matter of fact, it can
3777 // deal in two consecutive batches of offers. It would take a great
3778 // deal of sorting out to figure out which offers in the two batches
3779 // had the same owner and give them special processing. And,
3780 // honestly, it's a weird little corner case.
3781 //
3782 // So, since Flow offer crossing uses the payments engine, Flow
3783 // offer crossing no longer supports this optimization.
3784 //
3785 // The following test shows the difference in the behaviors between
3786 // Taker offer crossing and Flow offer crossing.
3787
3788 using namespace jtx;
3789 auto const gw = Account("gw");
3790
3791 auto test = [&](auto&& issue1, auto&& issue2) {
3792 Env env{*this, features};
3793 auto const baseFee = env.current()->fees().base.drops();
3794
3795 auto const startXrpBalance = XRP(4'000'000);
3796
3797 env.fund(startXrpBalance, gw);
3798 env.close();
3799
3800 auto const btc = issue1(
3801 {.env = env, .token = "BTC", .issuer = gw, .limit = 40, .transferFee = 25'000});
3802 using tBTC = std::decay_t<decltype(btc)>;
3803 auto const usd = issue2(
3804 {.env = env, .token = "USD", .issuer = gw, .limit = 8'000, .transferFee = 25'000});
3805 using tUSD = std::decay_t<decltype(usd)>;
3806 env.close();
3807
3808 // Test cases
3809 struct Actor
3810 {
3811 Account acct;
3812 int offers{}; // offers on account after crossing
3813 PrettyAmount xrp; // final expected after crossing
3814 PrettyAmount btc; // final expected after crossing
3815 PrettyAmount usd; // final expected after crossing
3816 };
3817 struct TestData
3818 {
3819 // The first three integers give the *index* in actors
3820 // to assign each of the three roles. By using indices it is
3821 // easy for alice to own the offer in the first leg, the second
3822 // leg, or both.
3823 std::size_t self{};
3824 std::size_t leg0{};
3825 std::size_t leg1{};
3826 PrettyAmount btcStart;
3827 std::vector<Actor> actors;
3828 };
3829
3830 // clang-format off
3831 TestData const flowTests[]{
3832 // btcStart ------------------- actor[0] -------------------- ------------------- actor[1] --------------------
3833 {0, 0, 1, btc(5), {{"gay", 1, drops(3950000'000000 - (4 * baseFee)), btc(5), usd (2500)}, {"gar", 1, drops(4050000'000000 - (3 * baseFee)), btc(0), usd(1375)}}}, // no BTC xfer fee
3834 {0, 0, 0, btc(5), {{"hye", 2, drops(4000000'000000 - (5 * baseFee)), btc(5), usd (2000)} }} // no xfer fee
3835 };
3836 // clang-format on
3837
3838 for (auto const& t : flowTests)
3839 {
3840 Account const& self = t.actors[t.self].acct;
3841 Account const& leg0 = t.actors[t.leg0].acct;
3842 Account const& leg1 = t.actors[t.leg1].acct;
3843
3844 for (auto const& actor : t.actors)
3845 {
3846 env.fund(XRP(4'000'000), actor.acct);
3847 env.close();
3848
3849 if constexpr (std::is_same_v<tBTC, MPT>)
3850 {
3851 auto mbtc = MPTTester(env, gw, btc);
3852 mbtc.authorize({.account = actor.acct});
3853 }
3854 else
3855 {
3856 env(trust(actor.acct, btc(40)));
3857 env.close();
3858 }
3859 if constexpr (std::is_same_v<tUSD, MPT>)
3860 {
3861 auto musd = MPTTester(env, gw, usd);
3862 musd.authorize({.account = actor.acct});
3863 }
3864 else
3865 {
3866 env(trust(actor.acct, usd(8'000)));
3867 env.close();
3868 }
3869 }
3870
3871 env(pay(gw, self, t.btcStart));
3872 env(pay(gw, self, usd(2'000)));
3873 if (self.id() != leg1.id())
3874 env(pay(gw, leg1, usd(2'000)));
3875 env.close();
3876
3877 // Get the initial offers in place. Remember their sequences
3878 // so we can delete them later.
3879 env(offer(leg0, btc(10), XRP(100'000), tfPassive));
3880 env.close();
3881 std::uint32_t const leg0OfferSeq = env.seq(leg0) - 1;
3882
3883 env(offer(leg1, XRP(100'000), usd(1'000), tfPassive));
3884 env.close();
3885 std::uint32_t const leg1OfferSeq = env.seq(leg1) - 1;
3886
3887 // This is the offer that matters.
3888 env(offer(self, usd(1'000), btc(10)));
3889 env.close();
3890 std::uint32_t const selfOfferSeq = env.seq(self) - 1;
3891
3892 // Verify results.
3893 for (auto const& actor : t.actors)
3894 {
3895 // Sometimes Taker offer crossing gets lazy about deleting
3896 // offers. Treat an empty offer as though it is deleted.
3897 auto actorOffers = offersOnAccount(env, actor.acct);
3898 auto const offerCount = std::distance(
3899 actorOffers.begin(),
3901 return (*offer)[sfTakerGets].signum() == 0;
3902 }).begin());
3903 BEAST_EXPECT(offerCount == actor.offers);
3904
3905 env.require(Balance(actor.acct, actor.xrp));
3906 env.require(Balance(actor.acct, actor.btc));
3907 env.require(Balance(actor.acct, actor.usd));
3908 }
3909 // Remove any offers that might be left hanging around. They
3910 // could bollix up later loops.
3911 env(offerCancel(leg0, leg0OfferSeq));
3912 env.close();
3913 env(offerCancel(leg1, leg1OfferSeq));
3914 env.close();
3915 env(offerCancel(self, selfOfferSeq));
3916 env.close();
3917 }
3918 };
3920 }
3921
3922 void
3924 {
3925 testcase("lsfRequireAuth");
3926
3927 using namespace jtx;
3928
3929 Env env{*this, features};
3930
3931 auto const gw = Account("gw");
3932 auto const alice = Account("alice");
3933 auto const bob = Account("bob");
3934
3935 env.fund(XRP(400'000), gw, alice, bob);
3936 env.close();
3937
3938 // GW requires authorization for holders of its IOUs
3939 auto gwMUSD =
3940 MPTTester({.env = env, .issuer = gw, .flags = kMptDexFlags | tfMPTRequireAuth});
3941 MPT const gwUSD = gwMUSD;
3942
3943 // Have gw authorize bob and alice
3944 gwMUSD.authorize({.account = alice});
3945 gwMUSD.authorize({.account = gw, .holder = alice});
3946 gwMUSD.authorize({.account = bob});
3947 gwMUSD.authorize({.account = gw, .holder = bob});
3948 // Alice is able to place the offer since the GW has authorized her
3949 env(offer(alice, gwUSD(40), XRP(4'000)));
3950 env.close();
3951
3952 env.require(offers(alice, 1));
3953 env.require(Balance(alice, gwUSD(0)));
3954
3955 env(pay(gw, bob, gwUSD(50)));
3956 env.close();
3957
3958 env.require(Balance(bob, gwUSD(50)));
3959
3960 // Bob's offer should cross Alice's
3961 env(offer(bob, XRP(4'000), gwUSD(40)));
3962 env.close();
3963
3964 env.require(offers(alice, 0));
3965 env.require(Balance(alice, gwUSD(40)));
3966
3967 env.require(offers(bob, 0));
3968 env.require(Balance(bob, gwUSD(10)));
3969 }
3970
3971 void
3973 {
3974 testcase("Missing Auth");
3975 // 1. gw creates MPTokenIssuance, which requires authorization.
3976 // alice creates an offer to acquire USD/gw, an asset for which
3977 // she does not own MPToken. This offer fails since alice
3978 // doesn't own MPToken and authorization is required.
3979 //
3980 // 2. Next, alice creates MPT, but it's not authorized.
3981 // alice attempts to create an offer and again fails.
3982 //
3983 // 3. Finally, gw authorizes alice to own USD/gw.
3984 // At this point alice successfully
3985 // creates and crosses an offer for USD/gw.
3986
3987 using namespace jtx;
3988
3989 Env env{*this, features};
3990
3991 auto const gw = Account("gw");
3992 auto const alice = Account("alice");
3993 auto const bob = Account("bob");
3994
3995 env.fund(XRP(400'000), gw, alice, bob);
3996 env.close();
3997
3998 auto gwMUSD =
3999 MPTTester({.env = env, .issuer = gw, .flags = kMptDexFlags | tfMPTRequireAuth});
4000 MPT const gwUSD = gwMUSD;
4001
4002 // alice can't create an offer because alice doesn't own
4003 // MPToken and MPTokenIssuance requires authorization
4004 env(offer(alice, gwUSD(40), XRP(4'000)), Ter(tecNO_AUTH));
4005 env.close();
4006
4007 env.require(offers(alice, 0));
4008 env.require(Balance(alice, gwUSD(kNone)));
4009
4010 gwMUSD.authorize({.account = bob});
4011 gwMUSD.authorize({.account = gw, .holder = bob});
4012
4013 env(pay(gw, bob, gwUSD(50)));
4014 env.close();
4015 env.require(Balance(bob, gwUSD(50)));
4016
4017 // bob can create an offer since bob owns MPToken
4018 // and it is authorized.
4019 env(offer(bob, XRP(4'000), gwUSD(40)));
4020 env.close();
4021 std::uint32_t const bobOfferSeq = env.seq(bob) - 1;
4022
4023 env.require(offers(alice, 0));
4024
4025 // alice creates MPToken, which is still not authorized. alice
4026 // should still not be able to create an offer for USD/gw.
4027 gwMUSD.authorize({.account = alice});
4028
4029 env(offer(alice, gwUSD(40), XRP(4'000)), Ter(tecNO_AUTH));
4030 env.close();
4031
4032 env.require(offers(alice, 0));
4033 env.require(Balance(alice, gwUSD(0)));
4034
4035 env.require(offers(bob, 1));
4036 env.require(Balance(bob, gwUSD(50)));
4037
4038 // Delete bob's offer so alice can create an offer without crossing.
4039 env(offerCancel(bob, bobOfferSeq));
4040 env.close();
4041 env.require(offers(bob, 0));
4042
4043 // Finally, gw authorizes alice. Now alice's
4044 // offer should succeed.
4045 gwMUSD.authorize({.account = gw, .holder = alice});
4046
4047 env(offer(alice, gwUSD(40), XRP(4'000)));
4048 env.close();
4049
4050 env.require(offers(alice, 1));
4051
4052 // Now bob creates his offer again. alice's offer should cross.
4053 env(offer(bob, XRP(4'000), gwUSD(40)));
4054 env.close();
4055
4056 env.require(offers(alice, 0));
4057 env.require(Balance(alice, gwUSD(40)));
4058
4059 env.require(offers(bob, 0));
4060 env.require(Balance(bob, gwUSD(10)));
4061 }
4062
4063 void
4065 {
4066 testcase("Self Auth");
4067
4068 using namespace jtx;
4069
4070 Env env{*this, features};
4071
4072 auto const gw = Account("gw");
4073 auto const alice = Account("alice");
4074
4075 env.fund(XRP(400'000), gw, alice);
4076 env.close();
4077
4078 auto gwMUSD =
4079 MPTTester({.env = env, .issuer = gw, .flags = kMptDexFlags | tfMPTRequireAuth});
4080 MPT const gwUSD = gwMUSD;
4081
4082 // Test that gw can create an offer to buy gw's currency.
4083 env(offer(gw, gwUSD(40), XRP(4'000)));
4084 env.close();
4085 std::uint32_t const gwOfferSeq = env.seq(gw) - 1;
4086 env.require(offers(gw, 1));
4087
4088 // Cancel gw's offer
4089 env(offerCancel(gw, gwOfferSeq));
4090 env.close();
4091 env.require(offers(gw, 0));
4092
4093 // Before DepositPreauth an account with lsfRequireAuth set could not
4094 // create an offer to buy their own currency. After DepositPreauth
4095 // they can.
4096 env(offer(gw, gwUSD(40), XRP(4'000)));
4097 env.close();
4098
4099 env.require(offers(gw, 1));
4100
4101 // The rest of the test verifies DepositPreauth behavior.
4102
4103 // Create/authorize alice's MPToken
4104 gwMUSD.authorize({.account = alice});
4105 gwMUSD.authorize({.account = gw, .holder = alice});
4106
4107 env(pay(gw, alice, gwUSD(50)));
4108 env.close();
4109
4110 env.require(Balance(alice, gwUSD(50)));
4111
4112 // alice's offer should cross gw's
4113 env(offer(alice, XRP(4'000), gwUSD(40)));
4114 env.close();
4115
4116 env.require(offers(alice, 0));
4117 env.require(Balance(alice, gwUSD(10)));
4118
4119 env.require(offers(gw, 0));
4120 }
4121
4122 void
4124 {
4125 // Show that an offer who's issuer has been deleted cannot be crossed.
4126 using namespace jtx;
4127
4128 testcase("Deleted offer issuer");
4129
4130 auto mpTokenExists =
4131 [](jtx::Env const& env, AccountID const& account, MPTID const& issuanceID) -> bool {
4132 return bool(env.le(keylet::mptoken(issuanceID, account)));
4133 };
4134
4135 Account const alice("alice");
4136 Account const becky("becky");
4137 Account const carol("carol");
4138 Account const gw("gateway");
4139
4140 Env env{*this, features};
4141
4142 env.fund(XRP(10'000), alice, becky, carol, noripple(gw));
4143
4144 auto musd = MPTTester({.env = env, .issuer = gw});
4145 MPT const usd = musd;
4146
4147 musd.authorize({.account = becky});
4148 BEAST_EXPECT(mpTokenExists(env, becky, usd.issuanceID));
4149 env(pay(gw, becky, usd(5)));
4150 env.close();
4151
4152 auto mbux = MPTTester({.env = env, .issuer = alice});
4153 MPT const bux = mbux;
4154
4155 // Make offers that produce USD and can be crossed two ways:
4156 // direct XRP -> USD
4157 // direct BUX -> USD
4158 env(offer(becky, XRP(2), usd(2)), Txflags(tfPassive));
4159 std::uint32_t const beckyBuxUsdSeq{env.seq(becky)};
4160 env(offer(becky, bux(3), usd(3)), Txflags(tfPassive));
4161 env.close();
4162
4163 // becky keeps the offers, but removes MPT.
4164 env(pay(becky, gw, usd(5)));
4165 musd.authorize({.account = becky, .flags = tfMPTUnauthorize});
4166
4167 BEAST_EXPECT(!mpTokenExists(env, becky, usd.issuanceID));
4168 BEAST_EXPECT(isOffer(env, becky, XRP(2), usd(2)));
4169 BEAST_EXPECT(isOffer(env, becky, bux(3), usd(3)));
4170
4171 // Have to delete MPTokenIssuance in order to delete
4172 // the issuer account.
4173 musd.destroy({});
4174
4175 // Delete gw's account.
4176 {
4177 // The ledger sequence needs to far enough ahead of the account
4178 // sequence before the account can be deleted.
4179 int const delta = [&env, &gw, openLedgerSeq = env.current()->seq()]() -> int {
4180 std::uint32_t const gwSeq{env.seq(gw)};
4181 if (gwSeq + 255 > openLedgerSeq)
4182 return gwSeq - openLedgerSeq + 255;
4183 return 0;
4184 }();
4185
4186 for (int i = 0; i < delta; ++i)
4187 env.close();
4188
4189 // Account deletion has a high fee. Account for that.
4190 env(acctdelete(gw, alice), Fee(drops(env.current()->fees().increment)));
4191 env.close();
4192
4193 // Verify that gw's account root is gone from the ledger.
4194 BEAST_EXPECT(!env.closed()->exists(keylet::account(gw.id())));
4195 }
4196
4197 // alice crosses becky's first offer. The offer create fails because
4198 // the USD issuer is not in the ledger.
4199 env(offer(alice, usd(2), XRP(2)), Ter(tecNO_ISSUER));
4200 env.close();
4201 env.require(offers(alice, 0));
4202 BEAST_EXPECT(isOffer(env, becky, XRP(2), usd(2)));
4203 BEAST_EXPECT(isOffer(env, becky, bux(3), usd(3)));
4204
4205 // alice crosses becky's second offer. Again, the offer create fails
4206 // because the USD issuer is not in the ledger.
4207 env(offer(alice, usd(3), bux(3)), Ter(tecNO_ISSUER));
4208 env.require(offers(alice, 0));
4209 BEAST_EXPECT(isOffer(env, becky, XRP(2), usd(2)));
4210 BEAST_EXPECT(isOffer(env, becky, bux(3), usd(3)));
4211
4212 // Cancel becky's BUX -> USD offer so we can try auto-bridging.
4213 env(offerCancel(becky, beckyBuxUsdSeq));
4214 env.close();
4215 BEAST_EXPECT(!isOffer(env, becky, bux(3), usd(3)));
4216
4217 // alice creates an offer that can be auto-bridged with becky's
4218 // remaining offer.
4219 mbux.authorize({.account = carol});
4220 env(pay(alice, carol, bux(2)));
4221
4222 env(offer(alice, bux(2), XRP(2)));
4223 env.close();
4224
4225 // carol attempts the auto-bridge. Again, the offer create fails
4226 // because the USD issuer is not in the ledger.
4227 env(offer(carol, usd(2), bux(2)), Ter(tecNO_ISSUER));
4228 env.close();
4229 BEAST_EXPECT(isOffer(env, alice, bux(2), XRP(2)));
4230 BEAST_EXPECT(isOffer(env, becky, XRP(2), usd(2)));
4231 }
4232
4233 // Helper function that returns offers on an account sorted by sequence.
4236 {
4239 return (*rhs)[sfSequence] < (*lhs)[sfSequence];
4240 });
4241 return offers;
4242 }
4243
4244 void
4246 {
4247 testcase("Ticket Offers");
4248
4249 using namespace jtx;
4250
4251 // Two goals for this test.
4252 //
4253 // o Verify that offers can be created using tickets.
4254 //
4255 // o Show that offers in the _same_ order book remain in
4256 // chronological order regardless of sequence/ticket numbers.
4257 Env env{*this, features};
4258 auto const gw = Account{"gateway"};
4259 auto const alice = Account{"alice"};
4260 auto const bob = Account{"bob"};
4261
4262 env.fund(XRP(10'000), gw, alice, bob);
4263 env.close();
4264
4265 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
4266
4267 env(pay(gw, alice, usd(200)));
4268 env.close();
4269
4270 // Create four offers from the same account with identical quality
4271 // so they go in the same order book. Each offer goes in a different
4272 // ledger so the chronology is clear.
4273 std::uint32_t const offerId0{env.seq(alice)};
4274 env(offer(alice, XRP(50), usd(50)));
4275 env.close();
4276
4277 // Create two tickets.
4278 std::uint32_t const ticketSeq{env.seq(alice) + 1};
4279 env(ticket::create(alice, 2));
4280 env.close();
4281
4282 // Create another sequence-based offer.
4283 std::uint32_t const offerId1{env.seq(alice)};
4284 BEAST_EXPECT(offerId1 == offerId0 + 4);
4285 env(offer(alice, XRP(50), usd(50)));
4286 env.close();
4287
4288 // Create two ticket based offers in reverse order.
4289 std::uint32_t const offerId2{ticketSeq + 1};
4290 env(offer(alice, XRP(50), usd(50)), ticket::Use(offerId2));
4291 env.close();
4292
4293 // Create the last offer.
4294 std::uint32_t const offerId3{ticketSeq};
4295 env(offer(alice, XRP(50), usd(50)), ticket::Use(offerId3));
4296 env.close();
4297
4298 // Verify that all of alice's offers are present.
4299 {
4300 auto offers = sortedOffersOnAccount(env, alice);
4301 BEAST_EXPECT(offers.size() == 4);
4302 BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId0);
4303 BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId3);
4304 BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerId2);
4305 BEAST_EXPECT(offers[3]->getFieldU32(sfSequence) == offerId1);
4306 env.require(Balance(alice, usd(200)));
4307 env.require(Owners(alice, 5));
4308 }
4309
4310 // Cross alice's first offer.
4311 env(offer(bob, usd(50), XRP(50)));
4312 env.close();
4313
4314 // Verify that the first offer alice created was consumed.
4315 {
4316 auto offers = sortedOffersOnAccount(env, alice);
4317 BEAST_EXPECT(offers.size() == 3);
4318 BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId3);
4319 BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId2);
4320 BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerId1);
4321 }
4322
4323 // Cross alice's second offer.
4324 env(offer(bob, usd(50), XRP(50)));
4325 env.close();
4326
4327 // Verify that the second offer alice created was consumed.
4328 {
4329 auto offers = sortedOffersOnAccount(env, alice);
4330 BEAST_EXPECT(offers.size() == 2);
4331 BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId3);
4332 BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId2);
4333 }
4334
4335 // Cross alice's third offer.
4336 env(offer(bob, usd(50), XRP(50)));
4337 env.close();
4338
4339 // Verify that the third offer alice created was consumed.
4340 {
4341 auto offers = sortedOffersOnAccount(env, alice);
4342 BEAST_EXPECT(offers.size() == 1);
4343 BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId3);
4344 }
4345
4346 // Cross alice's last offer.
4347 env(offer(bob, usd(50), XRP(50)));
4348 env.close();
4349
4350 // Verify that the third offer alice created was consumed.
4351 {
4352 auto offers = sortedOffersOnAccount(env, alice);
4353 BEAST_EXPECT(offers.empty());
4354 }
4355 env.require(Balance(alice, usd(0)));
4356 env.require(Owners(alice, 1));
4357 env.require(Balance(bob, usd(200)));
4358 env.require(Owners(bob, 1));
4359 }
4360
4361 void
4363 {
4364 testcase("Ticket Cancel Offers");
4365
4366 using namespace jtx;
4367
4368 // Verify that offers created with or without tickets can be canceled
4369 // by transactions with or without tickets.
4370 Env env{*this, features};
4371 auto const gw = Account{"gateway"};
4372 auto const alice = Account{"alice"};
4373
4374 env.fund(XRP(10'000), gw, alice);
4375 env.close();
4376
4377 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}});
4378
4379 env.require(Owners(alice, 1), tickets(alice, 0));
4380
4381 env(pay(gw, alice, usd(200)));
4382 env.close();
4383
4384 // Create the first of four offers using a sequence.
4385 std::uint32_t const offerSeqId0{env.seq(alice)};
4386 env(offer(alice, XRP(50), usd(50)));
4387 env.close();
4388 env.require(Owners(alice, 2), tickets(alice, 0));
4389
4390 // Create four tickets.
4391 std::uint32_t const ticketSeq{env.seq(alice) + 1};
4392 env(ticket::create(alice, 4));
4393 env.close();
4394 env.require(Owners(alice, 6), tickets(alice, 4));
4395
4396 // Create the second (also sequence-based) offer.
4397 std::uint32_t const offerSeqId1{env.seq(alice)};
4398 BEAST_EXPECT(offerSeqId1 == offerSeqId0 + 6);
4399 env(offer(alice, XRP(50), usd(50)));
4400 env.close();
4401
4402 // Create the third (ticket-based) offer.
4403 std::uint32_t const offerTixId0{ticketSeq + 1};
4404 env(offer(alice, XRP(50), usd(50)), ticket::Use(offerTixId0));
4405 env.close();
4406
4407 // Create the last offer.
4408 std::uint32_t const offerTixId1{ticketSeq};
4409 env(offer(alice, XRP(50), usd(50)), ticket::Use(offerTixId1));
4410 env.close();
4411
4412 // Verify that all of alice's offers are present.
4413 {
4414 auto offers = sortedOffersOnAccount(env, alice);
4415 BEAST_EXPECT(offers.size() == 4);
4416 BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerSeqId0);
4417 BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerTixId1);
4418 BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerTixId0);
4419 BEAST_EXPECT(offers[3]->getFieldU32(sfSequence) == offerSeqId1);
4420 env.require(Balance(alice, usd(200)));
4421 env.require(Owners(alice, 7));
4422 }
4423
4424 // Use a ticket to cancel an offer created with a sequence.
4425 env(offerCancel(alice, offerSeqId0), ticket::Use(ticketSeq + 2));
4426 env.close();
4427
4428 // Verify that offerSeqId_0 was canceled.
4429 {
4430 auto offers = sortedOffersOnAccount(env, alice);
4431 BEAST_EXPECT(offers.size() == 3);
4432 BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerTixId1);
4433 BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerTixId0);
4434 BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerSeqId1);
4435 }
4436
4437 // Use a ticket to cancel an offer created with a ticket.
4438 env(offerCancel(alice, offerTixId0), ticket::Use(ticketSeq + 3));
4439 env.close();
4440
4441 // Verify that offerTixId_0 was canceled.
4442 {
4443 auto offers = sortedOffersOnAccount(env, alice);
4444 BEAST_EXPECT(offers.size() == 2);
4445 BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerTixId1);
4446 BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerSeqId1);
4447 }
4448
4449 // All of alice's tickets should now be used up.
4450 env.require(Owners(alice, 3), tickets(alice, 0));
4451
4452 // Use a sequence to cancel an offer created with a ticket.
4453 env(offerCancel(alice, offerTixId1));
4454 env.close();
4455
4456 // Verify that offerTixId_1 was canceled.
4457 {
4458 auto offers = sortedOffersOnAccount(env, alice);
4459 BEAST_EXPECT(offers.size() == 1);
4460 BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerSeqId1);
4461 }
4462
4463 // Use a sequence to cancel an offer created with a sequence.
4464 env(offerCancel(alice, offerSeqId1));
4465 env.close();
4466
4467 // Verify that offerSeqId_1 was canceled.
4468 // All of alice's tickets should now be used up.
4469 env.require(Owners(alice, 1), tickets(alice, 0), offers(alice, 0));
4470 }
4471
4472 void
4474 {
4475 testcase("fixFillOrKill");
4476 using namespace jtx;
4477 Account const issuer("issuer");
4478 Account const maker("maker");
4479 Account const taker("taker");
4480
4481 auto test = [&](auto&& issue1, auto&& issue2) {
4482 Env env(*this, features);
4483
4484 env.fund(XRP(1'000), issuer);
4485 env.fund(XRP(1'000), maker, taker);
4486 env.close();
4487
4488 auto const usd =
4489 issue1({.env = env, .token = "USD", .issuer = issuer, .holders = {maker, taker}});
4490 auto const eur =
4491 issue2({.env = env, .token = "EUR", .issuer = issuer, .holders = {maker, taker}});
4492
4493 env(pay(issuer, maker, usd(1'000)));
4494 env(pay(issuer, taker, usd(1'000)));
4495 env(pay(issuer, maker, eur(1'000)));
4496 env.close();
4497
4498 auto makerUSDBalance = env.balance(maker, usd).value();
4499 auto takerUSDBalance = env.balance(taker, usd).value();
4500 auto makerEURBalance = env.balance(maker, eur).value();
4501 auto takerEURBalance = env.balance(taker, eur).value();
4502 auto makerXRPBalance = env.balance(maker, XRP).value();
4503 auto takerXRPBalance = env.balance(taker, XRP).value();
4504
4505 // tfFillOrKill, TakerPays must be filled
4506 {
4507 TER const err = features[fixFillOrKill] ? TER(tesSUCCESS) : tecKILLED;
4508
4509 env(offer(maker, XRP(100), usd(100)));
4510 env.close();
4511
4512 env(offer(taker, usd(100), XRP(101)), Txflags(tfFillOrKill), Ter(err));
4513 env.close();
4514
4515 makerXRPBalance -= txFee(env, 1);
4516 takerXRPBalance -= txFee(env, 1);
4517 if (err == tesSUCCESS)
4518 {
4519 makerUSDBalance -= usd(100);
4520 takerUSDBalance += usd(100);
4521 makerXRPBalance += XRP(100).value();
4522 takerXRPBalance -= XRP(100).value();
4523 }
4524 BEAST_EXPECT(expectOffers(env, taker, 0));
4525
4526 env(offer(maker, usd(100), XRP(100)));
4527 env.close();
4528
4529 env(offer(taker, XRP(100), usd(101)), Txflags(tfFillOrKill), Ter(err));
4530 env.close();
4531
4532 makerXRPBalance -= txFee(env, 1);
4533 takerXRPBalance -= txFee(env, 1);
4534 if (err == tesSUCCESS)
4535 {
4536 makerUSDBalance += usd(100);
4537 takerUSDBalance -= usd(100);
4538 makerXRPBalance -= XRP(100).value();
4539 takerXRPBalance += XRP(100).value();
4540 }
4541 BEAST_EXPECT(expectOffers(env, taker, 0));
4542
4543 env(offer(maker, usd(100), eur(100)));
4544 env.close();
4545
4546 env(offer(taker, eur(100), usd(101)), Txflags(tfFillOrKill), Ter(err));
4547 env.close();
4548
4549 makerXRPBalance -= txFee(env, 1);
4550 takerXRPBalance -= txFee(env, 1);
4551 if (err == tesSUCCESS)
4552 {
4553 makerUSDBalance += usd(100);
4554 takerUSDBalance -= usd(100);
4555 makerEURBalance -= eur(100);
4556 takerEURBalance += eur(100);
4557 }
4558 BEAST_EXPECT(expectOffers(env, taker, 0));
4559 }
4560
4561 // tfFillOrKill + tfSell, TakerGets must be filled
4562 {
4563 env(offer(maker, XRP(101), usd(101)));
4564 env.close();
4565
4566 env(offer(taker, usd(100), XRP(101)), Txflags(tfFillOrKill | tfSell));
4567 env.close();
4568
4569 makerUSDBalance -= usd(101);
4570 takerUSDBalance += usd(101);
4571 makerXRPBalance += XRP(101).value() - txFee(env, 1);
4572 takerXRPBalance -= XRP(101).value() + txFee(env, 1);
4573 BEAST_EXPECT(expectOffers(env, taker, 0));
4574
4575 env(offer(maker, usd(101), XRP(101)));
4576 env.close();
4577
4578 env(offer(taker, XRP(100), usd(101)), Txflags(tfFillOrKill | tfSell));
4579 env.close();
4580
4581 makerUSDBalance += usd(101);
4582 takerUSDBalance -= usd(101);
4583 makerXRPBalance -= XRP(101).value() + txFee(env, 1);
4584 takerXRPBalance += XRP(101).value() - txFee(env, 1);
4585 BEAST_EXPECT(expectOffers(env, taker, 0));
4586
4587 env(offer(maker, usd(101), eur(101)));
4588 env.close();
4589
4590 env(offer(taker, eur(100), usd(101)), Txflags(tfFillOrKill | tfSell));
4591 env.close();
4592
4593 makerUSDBalance += usd(101);
4594 takerUSDBalance -= usd(101);
4595 makerEURBalance -= eur(101);
4596 takerEURBalance += eur(101);
4597 makerXRPBalance -= txFee(env, 1);
4598 takerXRPBalance -= txFee(env, 1);
4599 BEAST_EXPECT(expectOffers(env, taker, 0));
4600 }
4601
4602 // Fail regardless of fixFillOrKill amendment
4603 for (auto const flags : {tfFillOrKill, tfFillOrKill + tfSell})
4604 {
4605 env(offer(maker, XRP(100), usd(100)));
4606 env.close();
4607
4608 env(offer(taker, usd(100), XRP(99)), Txflags(flags), Ter(tecKILLED));
4609 env.close();
4610
4611 makerXRPBalance -= txFee(env, 1);
4612 takerXRPBalance -= txFee(env, 1);
4613 BEAST_EXPECT(expectOffers(env, taker, 0));
4614
4615 env(offer(maker, usd(100), XRP(100)));
4616 env.close();
4617
4618 env(offer(taker, XRP(100), usd(99)), Txflags(flags), Ter(tecKILLED));
4619 env.close();
4620
4621 makerXRPBalance -= txFee(env, 1);
4622 takerXRPBalance -= txFee(env, 1);
4623 BEAST_EXPECT(expectOffers(env, taker, 0));
4624
4625 env(offer(maker, usd(100), eur(100)));
4626 env.close();
4627
4628 env(offer(taker, eur(100), usd(99)), Txflags(flags), Ter(tecKILLED));
4629 env.close();
4630
4631 makerXRPBalance -= txFee(env, 1);
4632 takerXRPBalance -= txFee(env, 1);
4633 BEAST_EXPECT(expectOffers(env, taker, 0));
4634 }
4635
4636 BEAST_EXPECT(
4637 env.balance(maker, usd) == makerUSDBalance &&
4638 env.balance(taker, usd) == takerUSDBalance &&
4639 env.balance(maker, eur) == makerEURBalance &&
4640 env.balance(taker, eur) == takerEURBalance &&
4641 env.balance(maker, XRP) == makerXRPBalance &&
4642 env.balance(taker, XRP) == takerXRPBalance);
4643 };
4645 }
4646
4647 void
4649 {
4650 testcase("Tick Size");
4651
4652 using namespace jtx;
4653
4654 auto const gw = Account{"gateway"};
4655 auto const alice = Account{"alice"};
4656
4657 auto getIOU = [&](Env& env) -> PrettyAsset {
4658 static int kI = 0;
4659 std::string const name = "IO" + std::to_string(kI++);
4660 auto const iou = gw[name];
4661 env(trust(alice, iou(1'000)));
4662 env(pay(gw, alice, iou(100)));
4663 env.close();
4664 return iou;
4665 };
4666 auto getMPT = [&](Env& env) -> PrettyAsset {
4667 MPT const mpt =
4668 MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 1'000'000'000});
4669 return mpt;
4670 };
4671 auto getXRP = [&](Env& env) -> PrettyAsset { return XRP; };
4672
4673 using ToAsset = std::function<PrettyAsset(Env&)>;
4674 struct TestInfo
4675 {
4676 ToAsset toAsset1;
4677 ToAsset toAsset2;
4678 int val1;
4679 int val2;
4680 };
4681 // XRP/MPT, MPT/XRP, MPT/MPT offers are not adjusted for TickSize
4682 // IOU/IOU, XRP/IOU, IOU/XRP offers have TickSize logic unchanged
4683 // IOU/MPT, MPT/IOU have TickSize logic applied to adjust IOU only
4685 {.toAsset1 = getIOU, .toAsset2 = getIOU, .val1 = 10, .val2 = 30},
4686 {.toAsset1 = getIOU, .toAsset2 = getXRP, .val1 = 10, .val2 = 30'000'000},
4687 {.toAsset1 = getXRP, .toAsset2 = getIOU, .val1 = 10'000'000, .val2 = 30},
4688 {.toAsset1 = getMPT, .toAsset2 = getXRP, .val1 = 10'000'000, .val2 = 30'000'000},
4689 {.toAsset1 = getXRP, .toAsset2 = getMPT, .val1 = 10'000'000, .val2 = 30'000'000},
4690 {.toAsset1 = getIOU, .toAsset2 = getMPT, .val1 = 10, .val2 = 30'000'000},
4691 {.toAsset1 = getMPT, .toAsset2 = getIOU, .val1 = 10'000'000, .val2 = 30},
4692 {.toAsset1 = getMPT, .toAsset2 = getMPT, .val1 = 10'000'000, .val2 = 30'000'000}};
4693 for (TestInfo const& t : tests)
4694 {
4695 Env env{*this, features};
4696 env.fund(XRP(10'000), gw, alice);
4697 env.close();
4698
4699 auto const xts = t.toAsset1(env);
4700 auto const xxx = t.toAsset2(env);
4701
4702 auto tokenType = [](PrettyAsset const& asset) -> std::string {
4703 return asset.raw().visit(
4704 [&](Issue const& issue) { return issue.native() ? "XRPIssue" : "Issue"; },
4705 [&](MPTIssue const&) { return "MPTIssue"; });
4706 };
4707
4708 testcase << "offer: " << tokenType(xts) << "/" << tokenType(xxx);
4709
4710 {
4711 // Gateway sets its tick size to 5
4712 auto txn = noop(gw);
4713 txn[sfTickSize.fieldName] = 5;
4714 env(txn);
4715 BEAST_EXPECT((*env.le(gw))[sfTickSize] == 5);
4716 }
4717
4718 env(offer(alice, xts(t.val1), xxx(t.val2)));
4719 env(offer(alice, xts(t.val2), xxx(t.val1)));
4720 env(offer(alice, xts(t.val1), xxx(t.val2)), Json(jss::Flags, tfSell));
4721 env(offer(alice, xts(t.val2), xxx(t.val1)), Json(jss::Flags, tfSell));
4722
4724 forEachItem(*env.current(), alice, [&](SLE::const_ref sle) {
4725 if (sle->getType() == ltOFFER)
4726 {
4727 offers.emplace(
4728 (*sle)[sfSequence],
4729 std::make_pair((*sle)[sfTakerPays], (*sle)[sfTakerGets]));
4730 }
4731 });
4732
4733 // first offer
4734 auto it = offers.begin();
4735 BEAST_EXPECT(it != offers.end());
4736 if (xxx.native() && !xts.holds<MPTIssue>())
4737 {
4738 BEAST_EXPECT(
4739 it->second.first == xts(t.val1) && it->second.second == XRPAmount(29'999'400));
4740 }
4741 else if (!xxx.integral())
4742 {
4743 BEAST_EXPECT(
4744 it->second.first == xts(t.val1) && it->second.second < xxx(t.val2) &&
4745 it->second.second > STAmount(xxx, 29'9994, -4));
4746 }
4747 else
4748 {
4749 BEAST_EXPECT(it->second.first == xts(t.val1) && it->second.second == xxx(t.val2));
4750 }
4751
4752 // second offer
4753 ++it;
4754 BEAST_EXPECT(it != offers.end());
4755 BEAST_EXPECT(it->second.first == xts(t.val2) && it->second.second == xxx(t.val1));
4756
4757 // third offer
4758 ++it;
4759 BEAST_EXPECT(it != offers.end());
4760 if (xts.native() && !xxx.holds<MPTIssue>())
4761 {
4762 BEAST_EXPECT(
4763 it->second.first == XRPAmount(10'000'200) && it->second.second == xxx(t.val2));
4764 }
4765 else if (!xts.integral())
4766 {
4767 BEAST_EXPECT(
4768 it->second.first == STAmount(xts, 10'0002, -4) &&
4769 it->second.second == xxx(t.val2));
4770 }
4771 else
4772 {
4773 BEAST_EXPECT(it->second.first == xts(t.val1) && it->second.second == xxx(t.val2));
4774 }
4775
4776 // fourth offer
4777 // exact TakerPays is XTS(1/.033333)
4778 ++it;
4779 BEAST_EXPECT(it != offers.end());
4780 BEAST_EXPECT(it->second.first == xts(t.val2) && it->second.second == xxx(t.val1));
4781
4782 BEAST_EXPECT(++it == offers.end());
4783 }
4784 }
4785
4786 void
4788 {
4789 // When an offer on the book is partially crossed, the payment engine
4790 // auto-creates a new ledger object (MPToken or IOU trustline) for the
4791 // offer owner to hold the incoming asset. This happens inside
4792 // BookStep::forEachOffer (MPT: checkCreateMPT) and BookStep::consumeOffer
4793 // (IOU: directSendNoFeeIOU -> trustCreate) without a reserve sufficiency
4794 // check. The offer owner can therefore end up with more objects than
4795 // their XRP balance can reserve for, consistent with IOU behavior.
4796
4797 testcase("Auto-Create Object Without Reserve Check During Partial Crossing");
4798
4799 using namespace jtx;
4800
4801 auto const gw = Account{"gateway"};
4802 auto const alice = Account{"alice"};
4803 auto const carol = Account{"carol"};
4804 auto const bob = Account{"bob"};
4805
4806 auto test = [&](auto&& getToken, auto&& execTx) {
4807 // MPT/IOU: carol's existing offer buys MPT/IOU by selling XRP.
4808 // carol has no MPToken/Trustline for this issuance. When alice partially crosses
4809 // carol's offer, an MPToken/Trustline is auto-created for carol without checking
4810 // that she can afford the extra reserve slot.
4811 Env env{*this, features};
4812
4813 auto const f = env.current()->fees().base;
4814 auto const r = reserve(env, 0);
4815 auto const inc = reserve(env, 1) - r;
4816
4817 env.fund(XRP(10'000), gw, alice, bob);
4818
4819 // getToken:
4820 // - Create MPT with CanTransfer + CanTrade; authorize alice as holder.
4821 // - Create IOU trustline
4822 auto const token = getToken(env);
4823
4824 // carol: reserve(0) + 1 increment + fee covers placing one offer.
4825 // After the offer tx she has exactly reserve(1) + XRP(30).
4826 // XRP(30) < inc (50 XRP), so receiving a second object will put her
4827 // below reserve(2).
4828 if (BEAST_EXPECT(inc > XRP(30)))
4829 env.fund(r + inc + f + XRP(30), carol);
4830
4831 // carol's offer goes on the book (no counterpart yet).
4832 // TakerPays=Token(30): carol will receive Token when crossed.
4833 // TakerGets=XRP(30): carol will give XRP when crossed.
4834 env(offer(carol, token(30), XRP(30)));
4835 env.require(Owners(carol, 1));
4836
4837 // Execute offer create or cross-currency payment
4838 // alice partially crosses carol's offer.
4839 // alice sends Token(15) to carol and receives XRP(15).
4840 // Token:
4841 // - MPT: checkCreateMPT auto-creates an MPToken for carol (no reserve check).
4842 // - IOU: directSendNoFeeIOU auto-creates an Trustline for carol (no reserve check).
4843 execTx(env, token);
4844
4845 // Carol now owns 2 objects (remaining offer + new MPToken) even
4846 // though her XRP balance is only reserve(1) + XRP(15), which is
4847 // below reserve(2) = reserve(1) + inc.
4848 auto const carolBalance = r + inc + XRP(15);
4849 env.require(Owners(carol, 2), Balance(carol, token(15)), Balance(carol, carolBalance));
4850 BEAST_EXPECT(carolBalance < r + 2 * inc); // below reserve(2)
4851 };
4852 std::function<PrettyAsset(Env&)> const getIOU = [&](Env& env) -> PrettyAsset {
4853 env.trust(gw["USD"](1'000), alice);
4854 env(pay(gw, alice, gw["USD"](100)));
4855 return gw["USD"];
4856 };
4857 std::function<PrettyAsset(Env&)> const getMPT = [&](Env& env) -> PrettyAsset {
4858 MPT const mpT1 = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 100});
4859 return mpT1;
4860 };
4861 for (auto&& getToken : {getIOU, getMPT})
4862 {
4863 test(getToken, [&](Env& env, PrettyAsset const& token) {
4864 // alice partially crosses carol's offer.
4865 // alice sends Token(15) to carol and receives XRP(15).
4866 // Token is MPT: checkCreateMPT auto-creates an MPToken for carol (no reserve
4867 // check). Token is IOU: directSendNoFeeIOU auto-creates a trustline for carol (no
4868 // reserve check).
4869 env(offer(alice, XRP(15), token(15)));
4870 });
4871 test(getToken, [&](Env& env, PrettyAsset const& token) {
4872 // Similar to above but with cross-currency payment.
4873 env(pay(alice, bob, XRP(15)),
4874 Sendmax(token(15)),
4875 Path(~XRP),
4876 Txflags(tfNoRippleDirect | tfPartialPayment));
4877 });
4878 }
4879 }
4880
4881 void
4883 {
4884 testCanceledOffer(features);
4885 testRmFundedOffer(features);
4886 testTinyPayment(features);
4887 testXRPTinyPayment(features);
4888 testInsufficientReserve(features);
4889 testFillModes(features);
4890 testMalformed(features);
4891 testExpiration(features);
4892 testUnfundedCross(features);
4893 testSelfCross(false, features);
4894 testSelfCross(true, features);
4895 testNegativeBalance(features);
4896 testOfferCrossWithXRP(true, features);
4897 testOfferCrossWithXRP(false, features);
4899 testOfferAcceptThenCancel(features);
4903 testCrossCurrencyStartXRP(features);
4904 testCrossCurrencyEndXRP(features);
4905 testCrossCurrencyBridged(features);
4906 testBridgedSecondLegDry(features);
4907 testOfferFeesConsumeFunds(features);
4908 testOfferCreateThenCross(features);
4909 testSellFlagBasic(features);
4910 testSellFlagExceedLimit(features);
4911 testGatewayCrossCurrency(features);
4912 testPartialCross(features);
4913 testXRPDirectCross(features);
4914 testDirectCross(features);
4915 testBridgedCross(features);
4916 testSellOffer(features);
4917 testSellWithFillOrKill(features);
4918 testTransferRateOffer(features);
4919 testSelfCrossOffer(features);
4920 testSelfIssueOffer(features);
4921 testDirectToDirectPath(features);
4923 testOfferInScaling(features);
4925 testSelfPayXferFeeOffer(features);
4926 testSelfPayUnlimitedFunds(features);
4927 testRequireAuth(features);
4928 testMissingAuth(features);
4929 testSelfAuth(features);
4930 testDeletedOfferIssuer(features);
4931 testTicketOffer(features);
4932 testTicketCancelOffer(features);
4935 testFillOrKill(features);
4936 testTickSize(features);
4937 testAutoCreateReserve(features);
4938 }
4939
4940 void
4941 run() override
4942 {
4943 using namespace jtx;
4944 static FeatureBitset const kAll{testableAmendments()};
4945 testAll(kAll);
4946 }
4947};
4948
4950
4951} // namespace xrpl::test
A testsuite class.
Definition suite.h:50
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
Represents a JSON value.
Definition json_value.h:130
virtual Config & config()=0
A currency issued by an account.
Definition Issue.h:13
bool native() const
Definition Issue.cpp:54
std::string getText() const override
Definition STAmount.cpp:646
std::shared_ptr< STLedgerEntry const > const & const_ref
std::shared_ptr< STLedgerEntry const > const_pointer
void testCurrencyConversionInParts(FeatureBitset features)
void run() override
Runs the suite.
void testCurrencyConversionEntire(FeatureBitset features)
void testXRPDirectCross(FeatureBitset features)
void testSellFlagExceedLimit(FeatureBitset features)
void testSelfPayUnlimitedFunds(FeatureBitset features)
void testCanceledOffer(FeatureBitset features)
void testMalformed(FeatureBitset features)
static std::vector< SLE::const_pointer > offersOnAccount(jtx::Env &env, jtx::Account account)
void testOfferAcceptThenCancel(FeatureBitset features)
static std::uint32_t lastClose(jtx::Env &env)
void testOfferInScalingWithXferRate(FeatureBitset features)
void testRequireAuth(FeatureBitset features)
void testUnfundedCross(FeatureBitset features)
void testGatewayCrossCurrency(FeatureBitset features)
void testOfferCreateThenCross(FeatureBitset features)
void testInsufficientReserve(FeatureBitset features)
void testOfferInScaling(FeatureBitset features)
void testRmSmallIncreasedQOffersXRP(FeatureBitset features)
void testSelfAuth(FeatureBitset features)
void testTransferRateOffer(FeatureBitset features)
void testCurrencyConversionIntoDebt(FeatureBitset features)
void testSelfCrossOffer2(FeatureBitset features)
void testBridgedSecondLegDry(FeatureBitset features)
void testTicketOffer(FeatureBitset features)
static std::vector< SLE::const_pointer > sortedOffersOnAccount(jtx::Env &env, jtx::Account const &acct)
void testRmSmallIncreasedQOffersMPT(FeatureBitset features)
void testTinyPayment(FeatureBitset features)
void testSelfCross(bool usePartner, FeatureBitset features)
void testCrossCurrencyBridged(FeatureBitset features)
void testNegativeBalance(FeatureBitset features)
void testSelfCrossOffer1(FeatureBitset features)
void testSellFlagBasic(FeatureBitset features)
void testSelfCrossOffer(FeatureBitset features)
void testDirectCross(FeatureBitset features)
void testSelfCrossLowQualityOffer(FeatureBitset features)
void testRmFundedOffer(FeatureBitset features)
void testMissingAuth(FeatureBitset features)
void testPartialCross(FeatureBitset features)
void testDeletedOfferIssuer(FeatureBitset features)
void testOfferCrossWithXRP(bool reverseOrder, FeatureBitset features)
void testSellWithFillOrKill(FeatureBitset features)
void testSellOffer(FeatureBitset features)
void testTicketCancelOffer(FeatureBitset features)
void testDirectToDirectPath(FeatureBitset features)
void testFillOrKill(FeatureBitset features)
void testCrossCurrencyStartXRP(FeatureBitset features)
void testExpiration(FeatureBitset features)
void testFillModes(FeatureBitset features)
void testCrossCurrencyEndXRP(FeatureBitset features)
void testOfferCrossWithLimitOverride(FeatureBitset features)
void testXRPTinyPayment(FeatureBitset features)
void testTickSize(FeatureBitset features)
void testAutoCreateReserve(FeatureBitset features)
void testOfferFeesConsumeFunds(FeatureBitset features)
void testAll(FeatureBitset features)
void testBridgedCross(FeatureBitset features)
static XRPAmount reserve(jtx::Env &env, std::uint32_t count)
void testSelfIssueOffer(FeatureBitset features)
void testSelfPayXferFeeOffer(FeatureBitset features)
json::Value json() const
Definition PathSet.h:170
Immutable cryptographic account descriptor.
Definition jtx/Account.h:17
AccountID id() const
Returns the Account ID.
Definition jtx/Account.h:85
A transaction testing environment.
Definition Env.h:143
Application & app()
Definition Env.h:280
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:284
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:127
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:296
PrettyAmount limit(Account const &account, Issue const &issue) const
Returns the IOU limit on an account.
Definition Env.cpp:254
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:275
Account const & master
Definition Env.h:147
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:605
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:353
Set the fee on a JTx.
Definition fee.h:15
Inject raw JSON.
Definition jtx_json.h:11
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:385
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:368
Converts to MPT Issue or STAmount.
Match the number of items in the account's owner directory.
Definition owners.h:52
Add a path.
Definition paths.h:39
Check a set of conditions.
Definition require.h:45
Sets the SendMax on a JTx.
Definition sendmax.h:13
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:13
Set the flags on a JTx.
Definition txflags.h:9
Set a ticket sequence on a JTx.
Definition ticket.h:26
T distance(T... args)
T is_same_v
@ Array
array value (ordered list)
Definition json_value.h:25
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:533
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
json::Value create(Account const &account, std::uint32_t count)
Create one of more tickets.
Definition ticket.cpp:16
static NoneT const kNone
Definition tags.h:9
auto const kMptDexFlags
Definition mpt.h:25
json::Value ledgerEntryMPT(jtx::Env &env, jtx::Account const &acct, MPTID const &mptID)
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
bool expectOffers(Env &env, AccountID const &account, std::uint16_t size, std::vector< Amounts > const &toMatch)
json::Value offerCancel(Account const &account, std::uint32_t offerSeq)
Cancel an offer.
Definition offer.cpp:31
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:9
json::Value getBookOffers(jtx::Env &env, Asset const &takerPays, Asset const &takerGets)
XRPAmount txFee(Env const &env, std::uint16_t n)
OwnerCount< ltMPTOKEN > mptokens
Match the number of MPToken in the account's owner directory.
Definition owners.h:73
FeatureBitset testableAmendments()
Definition Env.h:76
void testHelper3TokensMix(TTester &&tester)
json::Value acctdelete(Account const &account, Account const &dest)
Delete account.
std::array< Account, 1+sizeof...(Args)> noripple(Account const &account, Args const &... args)
Designate accounts as no-ripple in Env::fund.
Definition Env.h:70
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)
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
OwnerCount< ltOFFER > offers
Match the number of offers in the account's owner directory.
Definition owners.h:70
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
OwnerCount< ltTICKET > tickets
Match the number of tickets on the account.
Definition ticket.h:42
json::Value ledgerEntryOffer(jtx::Env &env, jtx::Account const &acct, std::uint32_t offerSeq)
static XRPAmount reserve(jtx::Env &env, std::uint32_t count)
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:48
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:48
std::unique_ptr< WSClient > makeWSClient(Config const &cfg, bool v2, unsigned rpcVersion, std::unordered_map< std::string, std::string > const &headers)
Returns a client operating through WebSockets/S.
Definition WSClient.cpp:329
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:97
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:779
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:93
Seed generateSeed(std::string const &passPhrase)
Generate a seed deterministically.
Definition Seed.cpp:58
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
Definition UintTypes.h:44
json::Value getJson(LedgerFill const &fill)
Return a new json::Value representing the ledger with given options.
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
@ temBAD_CURRENCY
Definition TER.h:76
@ temBAD_AMOUNT
Definition TER.h:75
@ temREDUNDANT
Definition TER.h:98
TERSubset< CanCvtToTER > TER
Definition TER.h:634
void forEachItem(ReadView const &view, Keylet const &root, std::function< void(SLE::const_ref)> const &f)
Iterate all items in the given directory.
MPTID badMPT()
Definition MPTIssue.h:110
@ tecINSUF_RESERVE_OFFER
Definition TER.h:287
@ tecPATH_PARTIAL
Definition TER.h:280
@ tecNO_AUTH
Definition TER.h:298
@ tecUNFUNDED_OFFER
Definition TER.h:282
@ tecEXPIRED
Definition TER.h:312
@ tecKILLED
Definition TER.h:314
@ tecNO_ISSUER
Definition TER.h:297
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:238
@ tesSUCCESS
Definition TER.h:240
T remove_if(T... args)
T sort(T... args)
Represents an XRP, IOU, or MPT quantity This customizes the string conversion and supports XRP conver...
STAmount const & value() const
T to_string(T... args)