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