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