xrpld
Loading...
Searching...
No Matches
FlowMPT_test.cpp
1#include <test/jtx/Account.h>
2#include <test/jtx/Env.h>
3#include <test/jtx/PathSet.h>
4#include <test/jtx/TestHelpers.h>
5#include <test/jtx/amount.h>
6#include <test/jtx/balance.h>
7#include <test/jtx/mpt.h>
8#include <test/jtx/offer.h>
9#include <test/jtx/owners.h>
10#include <test/jtx/paths.h>
11#include <test/jtx/pay.h>
12#include <test/jtx/sendmax.h>
13#include <test/jtx/ter.h>
14#include <test/jtx/trust.h>
15#include <test/jtx/txflags.h>
16
17#include <xrpl/basics/base_uint.h>
18#include <xrpl/beast/unit_test/suite.h>
19#include <xrpl/core/ServiceRegistry.h>
20#include <xrpl/ledger/ApplyView.h>
21#include <xrpl/ledger/OpenView.h>
22#include <xrpl/ledger/PaymentSandbox.h>
23#include <xrpl/ledger/Sandbox.h>
24#include <xrpl/ledger/helpers/DirectoryHelpers.h>
25#include <xrpl/ledger/helpers/OfferHelpers.h>
26#include <xrpl/protocol/AccountID.h>
27#include <xrpl/protocol/Asset.h>
28#include <xrpl/protocol/Feature.h>
29#include <xrpl/protocol/Indexes.h>
30#include <xrpl/protocol/Keylet.h>
31#include <xrpl/protocol/LedgerFormats.h>
32#include <xrpl/protocol/Protocol.h>
33#include <xrpl/protocol/SField.h>
34#include <xrpl/protocol/STAmount.h>
35#include <xrpl/protocol/STPathSet.h>
36#include <xrpl/protocol/SeqProxy.h>
37#include <xrpl/protocol/TER.h>
38#include <xrpl/protocol/TxFlags.h>
39#include <xrpl/protocol/XRPAmount.h>
40#include <xrpl/tx/paths/Flow.h>
41#include <xrpl/tx/paths/detail/Steps.h>
42
43#include <cstdint>
44#include <optional>
45#include <string>
46#include <type_traits>
47#include <vector>
48
49namespace xrpl::test {
50
52{
54
55 void
57 {
58 testcase("Direct Step");
59
60 using namespace jtx;
61 auto const alice = Account("alice");
62 auto const bob = Account("bob");
63 auto const carol = Account("carol");
64 auto const gw = Account("gw");
65 {
66 // Pay USD, trivial path
67 Env env(*this, features);
68
69 env.fund(XRP(10000), alice, bob, gw);
70 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
71 env(pay(gw, alice, usd(100)));
72 env(pay(alice, bob, usd(10)), Paths(usd));
73 env.require(Balance(bob, usd(10)));
74 }
75 {
76 // Partial payments
77 Env env(*this, features);
78
79 env.fund(XRP(10000), alice, bob, gw);
80 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
81 env(pay(gw, alice, usd(100)));
82 env(pay(alice, bob, usd(110)), Paths(usd), Ter(tecPATH_PARTIAL));
83 env.require(Balance(bob, usd(0)));
84 env(pay(alice, bob, usd(110)), Paths(usd), Txflags(tfPartialPayment));
85 env.require(Balance(bob, usd(100)));
86 }
87
88 {
89 // Limit quality
90 auto test = [&](auto&& issue1, auto&& issue2) {
91 Env env(*this, features);
92
93 env.fund(XRP(10'000), gw, alice, bob, carol);
94 env.close();
95
96 auto const usd =
97 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, carol}});
98 auto const eur =
99 issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {bob}});
100
101 env(pay(gw, alice, usd(100)));
102 env(pay(gw, bob, eur(100)));
103
104 env(offer(alice, eur(4), usd(4)));
105 env.close();
106
107 env(pay(bob, carol, usd(5)),
108 Sendmax(eur(4)),
109 Txflags(tfLimitQuality | tfPartialPayment),
111 env.require(Balance(carol, usd(0)));
112
113 env(pay(bob, carol, usd(5)), Sendmax(eur(4)), Txflags(tfPartialPayment));
114 env.require(Balance(carol, usd(4)));
115 };
117 }
118 }
119
120 void
122 {
123 testcase("Book Step");
124
125 using namespace jtx;
126
127 auto const gw = Account("gateway");
128 Account const alice("alice");
129 Account const bob("bob");
130 Account const carol("carol");
131
132 {
133 // simple [MPT|IOU]/[IOU|MPT] offer
134 auto test = [&](auto&& issue1, auto&& issue2) {
135 Env env(*this, features);
136
137 env.fund(XRP(10'000), alice, bob, carol, gw);
138 env.close();
139
140 auto const usd = issue1(
141 {.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob, carol}});
142 auto const btc = issue2(
143 {.env = env, .token = "BTC", .issuer = gw, .holders = {alice, bob, carol}});
144
145 env(pay(gw, alice, btc(50)));
146 env(pay(gw, bob, usd(50)));
147
148 env(offer(bob, btc(50), usd(50)));
149
150 env(pay(alice, carol, usd(50)), Path(~usd), Sendmax(btc(50)));
151
152 env.require(Balance(alice, btc(0)));
153 env.require(Balance(bob, btc(50)));
154 env.require(Balance(bob, usd(0)));
155 env.require(Balance(carol, usd(50)));
156 BEAST_EXPECT(!isOffer(env, bob, btc(50), usd(50)));
157 };
159 }
160 {
161 // simple [MPT|IOU]/XRP XRP/[IOU|MPT] offer
162 auto test = [&](auto&& issue1, auto&& issue2) {
163 Env env(*this, features);
164
165 env.fund(XRP(10'000), alice, bob, carol, gw);
166 env.close();
167
168 auto const usd = issue1(
169 {.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob, carol}});
170 auto const btc = issue2(
171 {.env = env, .token = "BTC", .issuer = gw, .holders = {alice, bob, carol}});
172
173 env(pay(gw, alice, btc(50)));
174 env(pay(gw, bob, usd(50)));
175
176 env(offer(bob, btc(50), XRP(50)));
177 env(offer(bob, XRP(50), usd(50)));
178
179 env(pay(alice, carol, usd(50)), Path(~XRP, ~usd), Sendmax(btc(50)));
180
181 env.require(Balance(alice, btc(0)));
182 env.require(Balance(bob, btc(50)));
183 env.require(Balance(bob, usd(0)));
184 env.require(Balance(carol, usd(50)));
185 BEAST_EXPECT(!isOffer(env, bob, XRP(50), usd(50)));
186 BEAST_EXPECT(!isOffer(env, bob, btc(50), XRP(50)));
187 };
189 }
190 {
191 // simple XRP -> USD through offer and sendmax
192 Env env(*this, features);
193
194 env.fund(XRP(10'000), alice, bob, carol, gw);
195 env.close();
196
197 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
198 MPT const btc = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
199
200 env(pay(gw, bob, usd(50)));
201
202 env(offer(bob, XRP(50), usd(50)));
203
204 env(pay(alice, carol, usd(50)), Path(~usd), Sendmax(XRP(50)));
205
206 // fee: MPTokenAuthorize * 2(EUR, USD) + pay
207 env.require(Balance(alice, XRP(10'000 - 50) - txFee(env, 3)));
208 // fee: MPTokenAuthorize * 2(EUR, USD) + offer
209 env.require(Balance(bob, XRP(10'000 + 50) - txFee(env, 3)));
210 env.require(Balance(bob, usd(0)));
211 env.require(Balance(carol, usd(50)));
212 BEAST_EXPECT(!isOffer(env, bob, XRP(50), usd(50)));
213 }
214 {
215 // simple USD -> XRP through offer and sendmax
216 Env env(*this, features);
217
218 env.fund(XRP(10'000), alice, bob, carol, gw);
219 env.close();
220
221 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
222 MPT const btc = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
223
224 env(pay(gw, alice, usd(50)));
225
226 env(offer(bob, usd(50), XRP(50)));
227
228 env(pay(alice, carol, XRP(50)), Path(~XRP), Sendmax(usd(50)));
229
230 env.require(Balance(alice, usd(0)));
231 env.require(Balance(bob, XRP(10'000 - 50) - txFee(env, 3)));
232 env.require(Balance(bob, usd(50)));
233 env.require(Balance(carol, XRP(10'000 + 50) - txFee(env, 2)));
234 BEAST_EXPECT(!isOffer(env, bob, usd(50), XRP(50)));
235 }
236 {
237 // test unfunded offers are removed when payment succeeds
238 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
239 Env env(*this, features);
240
241 env.fund(XRP(10'000), alice, bob, carol, gw);
242 env.close();
243
244 auto const usd = issue1(
245 {.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob, carol}});
246 auto const btc = issue2(
247 {.env = env, .token = "BTC", .issuer = gw, .holders = {alice, bob, carol}});
248 auto const eur = issue3(
249 {.env = env, .token = "EUR", .issuer = gw, .holders = {alice, bob, carol}});
250
251 env(pay(gw, alice, btc(60)));
252 env(pay(gw, bob, usd(50)));
253 env(pay(gw, bob, eur(50)));
254
255 env(offer(bob, btc(50), usd(50)));
256 env(offer(bob, btc(40), eur(50)));
257 env(offer(bob, eur(50), usd(50)));
258
259 // unfund offer
260 env(pay(bob, gw, eur(50)));
261 env.require(Balance(bob, eur(0)));
262 BEAST_EXPECT(isOffer(env, bob, btc(50), usd(50)));
263 BEAST_EXPECT(isOffer(env, bob, btc(40), eur(50)));
264 BEAST_EXPECT(isOffer(env, bob, eur(50), usd(50)));
265
266 env(pay(alice, carol, usd(50)), Path(~usd), Path(~eur, ~usd), Sendmax(btc(60)));
267
268 env.require(Balance(alice, btc(10)));
269 env.require(Balance(bob, btc(50)));
270 env.require(Balance(bob, usd(0)));
271 env.require(Balance(bob, eur(0)));
272 env.require(Balance(carol, usd(50)));
273 // used in the payment
274 BEAST_EXPECT(!isOffer(env, bob, btc(50), usd(50)));
275 // found unfunded
276 BEAST_EXPECT(!isOffer(env, bob, btc(40), eur(50)));
277 // unfunded, but should not yet be found unfunded
278 BEAST_EXPECT(isOffer(env, bob, eur(50), usd(50)));
279 };
281 }
282 {
283 // test unfunded offers are returned when the payment fails.
284 // bob makes two offers: a funded 5000 USD for 50 BTC and an
285 // unfunded 5000 EUR for 60 BTC. alice pays carol 6100 USD with 61
286 // BTC. alice only has 60 BTC, so the payment will fail. The payment
287 // uses two paths: one through bob's funded offer and one through
288 // his unfunded offer. When the payment fails `flow` should return
289 // the unfunded offer. This test is intentionally similar to the one
290 // that removes unfunded offers when the payment succeeds.
291 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
292 Env env(*this, features);
293
294 env.fund(XRP(10'000), alice, bob, carol, gw);
295 env.close();
296
297 auto const usd = issue1(
298 {.env = env,
299 .token = "USD",
300 .issuer = gw,
301 .holders = {alice, bob, carol},
302 .limit = 100'000});
303 auto const btc = issue2(
304 {.env = env,
305 .token = "BTC",
306 .issuer = gw,
307 .holders = {alice, bob, carol},
308 .limit = 100'000});
309 auto const eur = issue3(
310 {.env = env,
311 .token = "EUR",
312 .issuer = gw,
313 .holders = {alice, bob, carol},
314 .limit = 100'000});
315
316 env(pay(gw, alice, btc(60)));
317 env(pay(gw, bob, usd(6'000)));
318 env(pay(gw, bob, eur(5'000)));
319 env(pay(gw, carol, eur(100)));
320
321 env(offer(bob, btc(50), usd(5'000)));
322 env(offer(bob, btc(60), eur(5'000)));
323 env(offer(carol, btc(1'000), eur(100)));
324 env(offer(bob, eur(5'000), usd(5'000)));
325
326 // unfund offer
327 env(pay(bob, gw, eur(5'000)));
328 BEAST_EXPECT(isOffer(env, bob, btc(50), usd(5'000)));
329 BEAST_EXPECT(isOffer(env, bob, btc(60), eur(5'000)));
330 BEAST_EXPECT(isOffer(env, carol, btc(1'000), eur(100)));
331
332 auto flowJournal = env.app().getLogs().journal("Flow");
333 auto const flowResult = [&] {
334 STAmount const deliver(usd(5'100));
335 STAmount smax(btc(61));
336 PaymentSandbox sb(env.current().get(), TapNone);
337 STPathSet paths;
338 auto ipe = [](Asset const& asset) {
339 return STPathElement(
341 xrpAccount(),
342 asset,
343 asset.getIssuer());
344 };
345 {
346 // BTC -> USD
347 STPath const p1({ipe(usd)});
348 paths.pushBack(p1);
349 // BTC -> EUR -> USD
350 STPath const p2({ipe(eur), ipe(usd)});
351 paths.pushBack(p2);
352 }
353
354 return flow(
355 sb,
356 deliver,
357 alice,
358 carol,
359 paths,
360 false,
361 false,
362 true,
364 std::nullopt,
365 smax,
366 std::nullopt,
367 flowJournal);
368 }();
369
370 BEAST_EXPECT(flowResult.removableOffers.size() == 1);
371 env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
372 if (flowResult.removableOffers.empty())
373 return false;
374 Sandbox sb(&view, TapNone);
375 for (auto const& o : flowResult.removableOffers)
376 {
377 if (auto ok = sb.peek(keylet::offer(o)))
378 offerDelete(sb, ok, flowJournal);
379 }
380 sb.apply(view);
381 return true;
382 });
383
384 // used in payment, but since payment failed should
385 // be untouched
386 BEAST_EXPECT(isOffer(env, bob, btc(50), usd(5'000)));
387 BEAST_EXPECT(isOffer(env, carol, btc(1'000), eur(100)));
388 // found unfunded
389 BEAST_EXPECT(!isOffer(env, bob, btc(60), eur(5'000)));
390 };
392 }
393 {
394 // Do not produce more in the forward pass than the
395 // reverse pass. This test uses a path whose reverse
396 // pass will compute a 0.5 USD input required for a 1
397 // EUR output. It sets a sendmax of 0.4 USD, so the
398 // payment engine will need to do a forward pass.
399 // Without limits, the 0.4 USD would produce 1000 EUR in
400 // the forward pass. This test checks that the payment
401 // produces 1 EUR, as expected.
402 auto test = [&](auto&& issue1, auto&& issue2) {
403 Env env(*this, features);
404 env.fund(XRP(10'000), alice, bob, carol, gw);
405
406 auto const usd = issue1(
407 {.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob, carol}});
408 auto const eur = issue1(
409 {.env = env, .token = "EUR", .issuer = gw, .holders = {alice, bob, carol}});
410
411 env(pay(gw, alice, usd(1'000)));
412 env(pay(gw, bob, eur(1'000)));
413
414 Keylet const bobUsdOffer = keylet::offer(bob, SeqProxy::rawSequence(env.seq(bob)));
415 env(offer(bob, usd(10), drops(2)), Txflags(tfPassive));
416 env(offer(bob, drops(1), eur(1'000)), Txflags(tfPassive));
417
418 bool const reducedOffersV2 = features[fixReducedOffersV2];
419
420 // With reducedOffersV2, it is not allowed to accept
421 // less than USD(0.5) of bob's USD offer. If we
422 // provide 1 drop for less than USD(0.5), then the
423 // remaining fractional offer would block the order
424 // book.
425 TER const expectedTER = reducedOffersV2 ? TER(tecPATH_DRY) : TER(tesSUCCESS);
426 env(pay(alice, carol, eur(1)),
427 Path(~XRP, ~eur),
428 Sendmax(usd(4)),
429 Txflags(tfNoRippleDirect | tfPartialPayment),
430 Ter(expectedTER));
431
432 if (!reducedOffersV2)
433 {
434 env.require(Balance(carol, eur(1)));
435 env.require(Balance(bob, usd(4)));
436 env.require(Balance(bob, eur(999)));
437
438 // Show that bob's USD offer is now a blocker.
439 SLE::const_pointer const usdOffer = env.le(bobUsdOffer);
440 if (BEAST_EXPECT(usdOffer))
441 {
442 std::uint64_t const bookRate = [&usdOffer]() {
443 // Extract the least significant 64
444 // bits from the book page. That's
445 // where the quality is stored.
446 std::string bookDirStr = to_string(usdOffer->at(sfBookDirectory));
447 bookDirStr.erase(0, 48);
448 return std::stoull(bookDirStr, nullptr, 16);
449 }();
450 std::uint64_t const actualRate =
451 getRate(usdOffer->at(sfTakerGets), usdOffer->at(sfTakerPays));
452
453 // We expect the actual rate of the offer to
454 // be worse (larger) than the rate of the
455 // book page holding the offer. This is a
456 // defect which is corrected by
457 // fixReducedOffersV2.
458 BEAST_EXPECT(actualRate > bookRate);
459 }
460 }
461 };
463 }
464 }
465
466 void
468 {
469 testcase("Transfer Rate");
470
471 using namespace jtx;
472
473 auto const gw = Account("gateway");
474 Account const alice("alice");
475 Account const bob("bob");
476 Account const carol("carol");
477
478 {
479 // Simple payment through a gateway with a
480 // transfer rate
481 Env env(*this, features);
482
483 env.fund(XRP(10000), alice, bob, carol, gw);
484
485 MPT const usd = MPTTester(
486 {.env = env,
487 .issuer = gw,
488 .holders = {alice, bob, carol},
489 .transferFee = 25'000,
490 .maxAmt = 1'000});
491
492 env(pay(gw, alice, usd(50)));
493 env.require(Balance(alice, usd(50)));
494 env(pay(alice, bob, usd(40)), Sendmax(usd(50)));
495 env.require(Balance(bob, usd(40)), Balance(alice, usd(0)));
496 }
497 {
498 // transfer rate is not charged when issuer is src or
499 // dst
500 Env env(*this, features);
501
502 env.fund(XRP(10'000), alice, bob, carol, gw);
503
504 MPT const usd = MPTTester(
505 {.env = env,
506 .issuer = gw,
507 .holders = {alice, bob, carol},
508 .transferFee = 25'000,
509 .maxAmt = 1'000});
510
511 env(pay(gw, alice, usd(50)));
512 env.require(Balance(alice, usd(50)));
513 env(pay(alice, gw, usd(40)), Sendmax(usd(40)));
514 env.require(Balance(alice, usd(10)));
515 }
516 {
517 // transfer fee on an offer
518 Env env(*this, features);
519
520 env.fund(XRP(10'000), alice, bob, carol, gw);
521
522 MPT const usd = MPTTester(
523 {.env = env,
524 .issuer = gw,
525 .holders = {alice, bob, carol},
526 .transferFee = 25'000,
527 .maxAmt = 10'000});
528
529 // scale by 1
530 env(pay(gw, bob, usd(650)));
531
532 env(offer(bob, XRP(50), usd(500)));
533
534 env(pay(alice, carol, usd(500)),
535 Path(~usd),
536 Sendmax(XRP(50)),
537 Txflags(tfPartialPayment));
538
539 // bob pays 25% on 500USD -> 100USD; 400USD goes to carol
540 env.require(
541 Balance(alice, XRP(10'000 - 50) - txFee(env, 2)),
542 Balance(bob, usd(150)),
543 Balance(carol, usd(400)));
544 }
545 {
546 // Transfer fee two consecutive offers
547 auto test = [&](auto&& issue1, auto&& issue2) {
548 Env env(*this, features);
549
550 env.fund(XRP(10'000), alice, bob, carol, gw);
551 env.close();
552
553 auto const usd = issue1(
554 {.env = env,
555 .token = "USD",
556 .issuer = gw,
557 .holders = {alice, bob, carol},
558 .limit = 1'000,
559 .transferFee = 25'000});
560 auto const eur = issue2(
561 {.env = env,
562 .token = "EUR",
563 .issuer = gw,
564 .holders = {alice, bob, carol},
565 .limit = 1'000,
566 .transferFee = 25'000});
567
568 env(pay(gw, bob, usd(50)));
569 env(pay(gw, bob, eur(50)));
570
571 env(offer(bob, XRP(50), usd(50)));
572 env(offer(bob, usd(50), eur(50)));
573
574 env(pay(alice, carol, eur(40)),
575 Path(~usd, ~eur),
576 Sendmax(XRP(40)),
577 Txflags(tfPartialPayment));
578 // +1 for fset in helperIssueIOU
579 using TEur = std::decay_t<decltype(eur)>;
580 auto const fee = txFee(env, 3);
581 // bob pays 25% on 40USD (40 since sendmax is 40XRP)
582 // 8USD goes to gw and 32USD goes back to bob ->
583 // bob's USD balance is 42USD. USD/EUR offer is 32USD/32EUR.
584 // bob pays 25% on 32EUR -> 7EUR if MPT, 6.4EUR if IOU,
585 // therefore carl gets 25EUR if MPT, 25.6EUR if IOU.
586 auto const carolEUR = [&]() {
587 if constexpr (std::is_same_v<TEur, IOU>)
588 {
589 return eur(25.6);
590 }
591 else
592 {
593 return eur(25);
594 }
595 }();
596 env.require(
597 Balance(alice, XRP(10'000 - 40) - fee),
598 Balance(bob, usd(42)),
599 Balance(bob, eur(18)),
600 Balance(carol, carolEUR));
601 };
603 }
604 {
605 // Offer where the owner is also the issuer, sender pays
606 // fee
607 Env env(*this, features);
608
609 env.fund(XRP(10'000), alice, bob, gw);
610
611 MPT const usd = MPTTester(
612 {.env = env,
613 .issuer = gw,
614 .holders = {alice, bob},
615 .transferFee = 25'000,
616 .maxAmt = 1'000});
617
618 env(offer(gw, XRP(100), usd(100)));
619 env(pay(alice, bob, usd(100)), Sendmax(XRP(100)), Txflags(tfPartialPayment));
620 env.require(Balance(alice, XRP(10'000 - 100) - txFee(env, 2)), Balance(bob, usd(80)));
621 }
622 {
623 // Offer where the owner is also the issuer, sender pays
624 // fee
625 Env env(*this, features);
626
627 env.fund(XRP(10'000), alice, bob, gw);
628
629 MPT const usd = MPTTester(
630 {.env = env,
631 .issuer = gw,
632 .holders = {alice, bob},
633 .transferFee = 25'000,
634 .maxAmt = 1'000});
635
636 env(offer(gw, XRP(125), usd(125)));
637 env(pay(alice, bob, usd(100)), Sendmax(XRP(200)));
638 env.require(Balance(alice, XRP(10'000 - 125) - txFee(env, 2)), Balance(bob, usd(100)));
639 }
640 }
641
642 void
644 {
645 testcase("MPT Endpoint transfer rate overflow");
646
647 using namespace jtx;
648
649 Account const iouGW("iou_gateway");
650 Account const mptGW("mpt_gateway");
651 Account const alice("alice");
652 Account const bob("bob");
653
654 {
655 // Control: the same issuer-owned offer path works when the
656 // transfer-fee-adjusted input amount remains representable.
657 Env env(*this, features);
658
659 std::int64_t constexpr deliverAmount = 1'000'000'000'000'000'000LL;
660 std::int64_t constexpr offerAmount = deliverAmount + (deliverAmount / 2);
661
662 env.fund(XRP(10'000), iouGW, mptGW, alice, bob);
663 env.close();
664
665 auto const usd = iouGW["USD"];
666 env.trust(usd(offerAmount), alice);
667 env.trust(usd(offerAmount), mptGW);
668 env(pay(iouGW, alice, usd(offerAmount)));
669
670 MPTTester const mpt(
671 {.env = env, .issuer = mptGW, .holders = {bob}, .transferFee = kMaxTransferFee});
672
673 env(offer(mptGW, usd(offerAmount), mpt(offerAmount)));
674
675 env(pay(alice, bob, mpt(deliverAmount)),
676 Path(~mpt),
677 Sendmax(usd(offerAmount)),
678 Txflags(tfNoRippleDirect | tfPartialPayment));
679
680 env.require(Balance(alice, usd(0)), Balance(bob, mpt(deliverAmount)));
681 BEAST_EXPECT(!isOffer(env, mptGW, usd(offerAmount), mpt(offerAmount)));
682 }
683 {
684 // Regression: an extreme transfer-fee-adjusted MPT amount used to
685 // throw from MPTAmount::mulRatio during the endpoint reverse pass.
686 // The reverse pass now caps srcToDst at the largest amount whose
687 // transfer-fee-adjusted input is representable, so the offer limits
688 // the strand and a partial payment goes through.
689 Env env(*this, features);
690
691 std::int64_t constexpr overflowAmount = 7'000'000'000'000'000'000LL;
692 // The offer caps the input at overflowAmount, which the maximum
693 // transfer rate of 1.5 scales down to 7e18 * 2 / 3, rounded down
694 std::int64_t constexpr deliveredAmount = 4'666'666'666'666'666'666LL;
695
696 env.fund(XRP(10'000), iouGW, mptGW, alice, bob);
697 env.close();
698
699 auto const usd = iouGW["USD"];
700 env.trust(usd(overflowAmount), alice);
701 env.trust(usd(overflowAmount), mptGW);
702 env(pay(iouGW, alice, usd(overflowAmount)));
703
704 MPTTester const mpt(
705 {.env = env, .issuer = mptGW, .holders = {bob}, .transferFee = kMaxTransferFee});
706
707 env(offer(mptGW, usd(overflowAmount), mpt(overflowAmount)));
708
709 env(pay(alice, bob, mpt(overflowAmount)),
710 Path(~mpt),
711 Sendmax(usd(overflowAmount)),
712 Txflags(tfNoRippleDirect | tfPartialPayment));
713
714 env.require(Balance(alice, usd(0)), Balance(bob, mpt(deliveredAmount)));
715 BEAST_EXPECT(!isOffer(env, mptGW, usd(overflowAmount), mpt(overflowAmount)));
716 }
717 }
718
719 void
721 {
722 // A payment between the holders of an MPT with a transfer fee ripples
723 // through the issuer, and the issuing step has to charge the transfer
724 // rate on the amount it receives. maxPaymentFlow() returns the issuance
725 // maximum for that step, so srcToDst * transferRate is not necessarily
726 // representable as an MPT amount. The reverse pass must cap the flow at
727 // the largest representable input instead of declaring the strand dry,
728 // otherwise a deliverable partial payment fails with tecPATH_DRY.
729 //
730 // Same defect as the case above, reached without an offer: holder ->
731 // issuer -> holder, one case per branch of the pre-fix revImp.
732 testcase("MPT Endpoint rippling input overflow");
733
734 using namespace jtx;
735
736 Account const gw("gateway");
737 Account const alice("alice");
738 Account const bob("bob");
739
740 // The maximum transfer fee gives a transfer rate of 1.5, so an input of
741 // kMaxMpTokenAmount covers at most kMaxMpTokenAmount * 2 / 3 of output.
742 std::int64_t constexpr maxRepresentable = 6'148'914'691'236'517'204LL;
743 std::int64_t constexpr aliceBalance = 1'000;
744 // The forward pass rounds the delivered amount down: 1000 / 1.5
745 std::int64_t constexpr bobBalance = 666;
746
747 auto const test =
748 [&](std::uint64_t maxAmt, std::int64_t deliver, std::string const& label) {
749 Env env(*this, features);
750 env.fund(XRP(10'000), gw, alice, bob);
751 env.close();
752
753 auto mpt = MPTTester(
754 {.env = env,
755 .issuer = gw,
756 .holders = {alice, bob},
757 .transferFee = kMaxTransferFee,
758 .maxAmt = maxAmt});
759
760 env(pay(gw, alice, mpt(aliceBalance)));
761 env.close();
762
763 // alice asks to deliver more than the transfer rate can scale,
764 // so the issuing step caps the flow and her balance limits it
765 // further
766 env(pay(alice, bob, mpt(deliver)),
768 Txflags(tfPartialPayment));
769 BEAST_EXPECTS(env.ter() == tesSUCCESS, label);
770 BEAST_EXPECTS(env.balance(alice, mpt) == mpt(0), label);
771 BEAST_EXPECTS(env.balance(bob, mpt) == mpt(bobBalance), label);
772 BEAST_EXPECTS(mpt.checkMPTokenOutstandingAmount(bobBalance), label);
773 };
774
775 // The requested amount is below MaximumAmount, so the reverse pass
776 // takes the non-limiting branch and overflows on the requested amount
777 test(kMaxMpTokenAmount, maxRepresentable + 1, "non-limiting");
778
779 // MaximumAmount is below the requested amount but still large enough
780 // that scaling it by the transfer rate is not representable, so the
781 // reverse pass takes the limiting branch and overflows on the maximum
782 test(maxRepresentable + 1, kMaxMpTokenAmount, "limiting");
783 }
784
785 void
787 {
788 testcase("falseDryChanges");
789
790 using namespace jtx;
791
792 auto const gw = Account("gateway");
793 Account const alice("alice");
794 Account const bob("bob");
795 Account const carol("carol");
796
797 auto test = [&](auto&& issue1, auto&& issue2) {
798 Env env(*this, features);
799
800 env.fund(XRP(10'000), alice, carol, gw);
801 env.fund(reserve(env, 5), bob);
802 env.close();
803
804 auto const usd =
805 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, carol, bob}});
806 auto const eur =
807 issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {alice, carol, bob}});
808
809 env(pay(gw, alice, eur(50)));
810 env(pay(gw, bob, usd(50)));
811
812 // Bob has _just_ slightly less than 50 xrp available
813 // If his owner count changes, he will have more liquidity.
814 // This is one error case to test (when Flow is used).
815 // Computing the incoming xrp to the XRP/USD offer will
816 // require two recursive calls to the EUR/XRP offer. The
817 // second call will return tecPATH_DRY, but the entire path
818 // should not be marked as dry. This is the second error
819 // case to test (when flowV1 is used).
820 env(offer(bob, eur(50), XRP(50)));
821 env(offer(bob, XRP(50), usd(50)));
822
823 env(pay(alice, carol, usd(1'000'000)),
824 Path(~XRP, ~usd),
825 Sendmax(eur(500)),
826 Txflags(tfNoRippleDirect | tfPartialPayment));
827
828 auto const carolUSD = env.balance(carol, usd).value();
829 BEAST_EXPECT(carolUSD > usd(0) && carolUSD < usd(50));
830 };
832 }
833
834 void
836 {
837 // Single path with two offers and limit quality. The
838 // quality limit is such that the first offer should be
839 // taken but the second should not. The total amount
840 // delivered should be the sum of the two offers and sendMax
841 // should be more than the first offer.
842 testcase("limitQuality");
843 using namespace jtx;
844
845 auto const gw = Account("gateway");
846 Account const alice("alice");
847 Account const bob("bob");
848 Account const carol("carol");
849
850 {
851 Env env(*this);
852
853 env.fund(XRP(10'000), alice, bob, carol, gw);
854
855 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}});
856
857 env(pay(gw, bob, usd(100)));
858 env(offer(bob, XRP(50), usd(50)));
859 env(offer(bob, XRP(100), usd(50)));
860
861 env(pay(alice, carol, usd(100)),
862 Path(~usd),
863 Sendmax(XRP(100)),
864 Txflags(tfNoRippleDirect | tfPartialPayment | tfLimitQuality));
865
866 env.require(Balance(carol, usd(50)));
867 }
868 }
869
870 // Helper function that returns the reserve on an account based on
871 // the passed in number of owners.
872 static XRPAmount
874 {
875 return env.current()->fees().accountReserve(count, 1);
876 }
877
878 // Helper function that returns the Offers on an account.
881 {
883 forEachItem(*env.current(), account, [&result](SLE::ConstRef sle) {
884 if (sle->getType() == ltOFFER)
885 result.push_back(sle);
886 });
887 return result;
888 }
889
890 void
892 {
893 using namespace jtx;
894 Account const alice("alice");
895 Account const bob("bob");
896 Account const carol("carol");
897 Account const gw("gw");
898
899 {
900 testcase("Reserve-edge offer owner cannot create another object");
901
902 Env env(*this, features);
903
904 auto const baseFee = env.current()->fees().base;
905 auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
906 auto const xrpOffer = ownerIncrement - drops(1);
907 auto const bobStart = reserve(env, 2) - drops(1) + baseFee;
908
909 env.fund(XRP(10'000), alice, gw);
910 env.fund(bobStart, bob);
911 env.close();
912
913 MPTTester const usd({.env = env, .issuer = gw, .maxAmt = 10});
914
915 env(offer(bob, usd(1), xrpOffer));
916 env.close();
917
918 env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1));
919
920 // This mirrors the full-crossing setup below. Bob has enough XRP
921 // for the resting offer, but not enough to pay a fee and add
922 // another owner-count object while the offer remains on ledger.
923 env(check::create(bob, alice, drops(1)), Ter(tecINSUFFICIENT_RESERVE));
924 env.close();
925
926 env.require(Owners(bob, 1));
927 BEAST_EXPECT(offersOnAccount(env, bob).size() == 1);
928 }
929
930 {
931 testcase("Reserve-edge offer owner creates MPToken during consume");
932
933 Env env(*this, features);
934
935 auto const baseFee = env.current()->fees().base;
936 auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
937 auto const xrpOffer = ownerIncrement - drops(1);
938 auto const bobStart = reserve(env, 2) - drops(1) + baseFee;
939
940 env.fund(XRP(10'000), alice, carol, gw);
941 env.fund(bobStart, bob);
942 env.close();
943
944 MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
945
946 env(pay(gw, alice, usd(1)));
947 env(offer(bob, usd(1), xrpOffer));
948 env.close();
949
950 env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1));
951 BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
952 auto const carolXRP = env.balance(carol);
953
954 // Bob has enough XRP for the resting offer but is close to
955 // reserve. The payment should not create Bob's USD MPToken until
956 // the offer is actually consumed, otherwise the temporary owner
957 // count increase can make the offer look underfunded during path
958 // execution.
959 env(pay(alice, carol, xrpOffer),
960 Path(~XRP),
961 Sendmax(usd(1)),
962 Txflags(tfNoRippleDirect));
963 env.close();
964
965 env.require(Balance(carol, carolXRP + xrpOffer));
966 env.require(Balance(bob, usd(1)));
967 env.require(Balance(bob, reserve(env, 1)), Owners(bob, 1));
968 BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
969 BEAST_EXPECT(offersOnAccount(env, bob).empty());
970 }
971
972 {
973 testcase("Partial offer owner creates MPToken during consume");
974
975 Env env(*this, features);
976
977 auto const baseFee = env.current()->fees().base;
978 auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
979 auto const bobStart = reserve(env, 3) + baseFee;
980
981 env.fund(XRP(10'000), alice, carol, gw);
982 env.fund(bobStart, bob);
983 env.close();
984
985 MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
986
987 env(pay(gw, alice, usd(1)));
988 env(offer(bob, usd(2), drops(2 * ownerIncrement)));
989 env.close();
990
991 env.require(Balance(bob, reserve(env, 3)), Owners(bob, 1));
992 BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
993 auto const carolXRP = env.balance(carol);
994
995 // Partial consumption leaves Bob's offer on the ledger, so he ends
996 // up owning both the remaining offer and a newly created MPToken.
997 // The MPToken is created regardless of reserve; this setup simply
998 // funds Bob enough that he still meets reserve(2) afterward (the
999 // under-reserved case is covered in OfferMPT_test's no-reserve-check
1000 // testcase).
1001 env(pay(alice, carol, drops(ownerIncrement)),
1002 Path(~XRP),
1003 Sendmax(usd(1)),
1004 Txflags(tfNoRippleDirect));
1005 env.close();
1006
1007 env.require(Balance(carol, carolXRP + drops(ownerIncrement)));
1008 env.require(Balance(bob, usd(1)));
1009 env.require(Balance(bob, reserve(env, 2)), Owners(bob, 2));
1010 BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
1011 BEAST_EXPECT(offersOnAccount(env, bob).size() == 1);
1012 BEAST_EXPECT(isOffer(env, bob, usd(1), drops(ownerIncrement)));
1013 }
1014
1015 {
1016 testcase("Issuer-owned offer does not create issuer MPToken");
1017
1018 Env env(*this, features);
1019
1020 env.fund(XRP(10'000), alice, carol, gw);
1021 env.close();
1022
1023 MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
1024
1025 env(pay(gw, alice, usd(1)));
1026 env(offer(gw, usd(1), drops(1'000)));
1027 env.close();
1028
1029 BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id())));
1030 auto const carolXRP = env.balance(carol);
1031
1032 // The issuer can own an offer that receives its own MPT without an
1033 // MPToken. Consuming that offer should keep the issuer side
1034 // tokenless.
1035 env(pay(alice, carol, drops(1'000)),
1036 Path(~XRP),
1037 Sendmax(usd(1)),
1038 Txflags(tfNoRippleDirect));
1039 env.close();
1040
1041 env.require(Balance(alice, usd(0)));
1042 env.require(Balance(carol, carolXRP + drops(1'000)));
1043 BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id())));
1044 BEAST_EXPECT(offersOnAccount(env, gw).empty());
1045 }
1046 }
1047
1048 void
1050 {
1051 testcase("Self-payment 1");
1052
1053 // In this test case the new flow code mis-computes the
1054 // amount of money to move. Fortunately the new code's
1055 // re-execute check catches the problem and throws out the
1056 // transaction.
1057 //
1058 // The old payment code handles the payment correctly.
1059 using namespace jtx;
1060
1061 auto test = [&](auto&& issue1, auto&& issue2) {
1062 auto const gw1 = Account("gw1");
1063 auto const gw2 = Account("gw2");
1064 auto const alice = Account("alice");
1065
1066 Env env(*this, features);
1067
1068 env.fund(XRP(1'000'000), gw1, gw2);
1069 env.close();
1070
1071 // The fee that's charged for transactions.
1072 auto const f = env.current()->fees().base;
1073
1074 env.fund(reserve(env, 3) + f * 4, alice);
1075 env.close();
1076
1077 auto const usd = issue1(
1078 {.env = env, .token = "USD", .issuer = gw1, .holders = {alice}, .limit = 20'000});
1079 auto const eur = issue2(
1080 {.env = env, .token = "EUR", .issuer = gw2, .holders = {alice}, .limit = 20'000});
1081
1082 env(pay(gw1, alice, usd(10)));
1083 env(pay(gw2, alice, eur(10'000)));
1084 env.close();
1085
1086 env(offer(alice, usd(5'000), eur(6'000)));
1087 env.close();
1088
1089 env.require(Owners(alice, 3));
1090 env.require(Balance(alice, usd(10)));
1091 env.require(Balance(alice, eur(10'000)));
1092
1093 auto aliceOffers = offersOnAccount(env, alice);
1094 BEAST_EXPECT(aliceOffers.size() == 1);
1095 for (auto const& offerPtr : aliceOffers)
1096 {
1097 auto const offer = *offerPtr;
1098 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
1099 BEAST_EXPECT(offer[sfTakerGets] == eur(6'000));
1100 BEAST_EXPECT(offer[sfTakerPays] == usd(5'000));
1101 }
1102
1103 env(pay(alice, alice, eur(6'000)), Sendmax(usd(5'000)), Txflags(tfPartialPayment));
1104 env.close();
1105
1106 env.require(Owners(alice, 3));
1107 env.require(Balance(alice, usd(10)));
1108 env.require(Balance(alice, eur(10'000)));
1109 aliceOffers = offersOnAccount(env, alice);
1110 BEAST_EXPECT(aliceOffers.size() == 1);
1111 for (auto const& offerPtr : aliceOffers)
1112 {
1113 auto const offer = *offerPtr;
1114 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
1115 if constexpr (std::is_same_v<std::decay_t<decltype(eur)>, IOU>)
1116 {
1117 BEAST_EXPECT(offer[sfTakerGets] == eur(5'988));
1118 }
1119 else
1120 {
1121 BEAST_EXPECT(offer[sfTakerGets] == eur(5'989));
1122 }
1123 BEAST_EXPECT(offer[sfTakerPays] == usd(4'990));
1124 }
1125 };
1127 }
1128
1129 template <typename TGets, typename TPays>
1137
1138 void
1140 {
1141 testcase("Self-payment 2");
1142
1143 using namespace jtx;
1144
1145 // This test shows a difference between IOU and MPT
1146 // self-payment result depending on IOU trustline limit.
1147
1148 auto const gw1 = Account("gw1");
1149 auto const gw2 = Account("gw2");
1150 auto const alice = Account("alice");
1151
1152 auto initMPT = [&](Env& env) {
1153 MPT const usd =
1154 MPTTester({.env = env, .issuer = gw1, .holders = {alice}, .maxAmt = 506});
1155 MPT const eur =
1156 MPTTester({.env = env, .issuer = gw2, .holders = {alice}, .maxAmt = 606});
1157 // Payment's engine last step overflows
1158 // OutstandingAmount since it doesn't know if the
1159 // BookStep redeems or not. The BookStep then has 600EUR
1160 // available. Consequently, the entire offer is crossed.
1161 // Note remaining takerGets is 541 rather than 540 due to integral
1162 // rounding. XRP has a similar result.
1163 return TokenData<MPT, MPT>{
1164 .gets = eur, .pays = usd, .remTakerGets = eur(541), .remTakerPays = usd(450)};
1165 };
1166
1167 auto initXRP = [&](Env& env) {
1168 MPT const usd =
1169 MPTTester({.env = env, .issuer = gw1, .holders = {alice}, .maxAmt = 1'000});
1170 // Payment's engine last step overflows
1171 // OutstandingAmount since it doesn't know if the
1172 // BookStep redeems or not. The BookStep then has 600EUR
1173 // available. Consequently, the entire offer is crossed.
1174 // Note remaining takerGets is 540.000001 rather than 540 due to
1175 // integral rounding.
1176 return TokenData<XrpT, MPT>{
1177 .gets = XRP,
1178 .pays = usd,
1179 .remTakerGets = XRP(540.000001),
1180 .remTakerPays = usd(450)};
1181 };
1182
1183 auto initIOU = [&](Env& env) {
1184 auto const usd = gw1["USD"];
1185 auto const eur = gw2["EUR"];
1186 env(trust(alice, usd(506)));
1187 env(trust(alice, eur(606)));
1188 env.close();
1189 // Payment's engine last step is limited by alice's
1190 // trustline - 606. Therefore, only 6EUR is delivered
1191 // and the offer is partially crossed.
1192 return TokenData<IOU, IOU>{
1193 .gets = eur, .pays = usd, .remTakerGets = eur(594), .remTakerPays = usd(495)};
1194 };
1195
1196 auto initIOU1 = [&](Env& env) {
1197 auto const usd = gw1["USD"];
1198 auto const eur = gw2["EUR"];
1199 env(trust(alice, usd(1'000)));
1200 env(trust(alice, eur(1'000)));
1201 env.close();
1202 // Payment's engine last step is not limited by alice's
1203 // trustline. Therefore, the entire offer is crossed.
1204 // This the same result as with MPT.
1205 return TokenData<IOU, IOU>{
1206 .gets = eur, .pays = usd, .remTakerGets = eur(540), .remTakerPays = usd(450)};
1207 };
1208
1209 auto test = [&](auto&& initToken) {
1210 Env env(*this, features);
1211
1212 env.fund(XRP(2'000), gw1, gw2, alice);
1213 env.close();
1214
1215 auto const f = env.current()->fees().base;
1216
1217 auto const tok = initToken(env);
1218
1219 auto const& toK1 = tok.pays;
1220 auto const& toK2 = tok.gets;
1221 bool const isTakerGetsXRP = isXRP(Asset{toK2});
1222 std::uint32_t const ownerCnt = isTakerGetsXRP ? 2 : 3;
1223
1224 env(pay(gw1, alice, toK1(500)));
1225 if (!isTakerGetsXRP)
1226 env(pay(gw2, alice, toK2(600)));
1227 env.close();
1228
1229 env(offer(alice, toK1(500), toK2(600)));
1230 env.close();
1231
1232 env.require(Owners(alice, ownerCnt));
1233 env.require(Balance(alice, toK1(500)));
1234 if (isTakerGetsXRP)
1235 {
1236 env.require(Balance(alice, toK2(2'000) - 2 * f));
1237 }
1238 else
1239 {
1240 env.require(Balance(alice, toK2(600)));
1241 }
1242
1243 auto aliceOffers = offersOnAccount(env, alice);
1244 BEAST_EXPECT(aliceOffers.size() == 1);
1245 for (auto const& offerPtr : aliceOffers)
1246 {
1247 auto const offer = *offerPtr;
1248 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
1249 BEAST_EXPECT(offer[sfTakerGets] == toK2(600));
1250 BEAST_EXPECT(offer[sfTakerPays] == toK1(500));
1251 }
1252
1253 env(pay(alice, alice, toK2(60)), Sendmax(toK1(50)), Txflags(tfPartialPayment));
1254 env.close();
1255
1256 env.require(Owners(alice, ownerCnt));
1257 env.require(Balance(alice, toK1(500)));
1258 if (isTakerGetsXRP)
1259 {
1260 env.require(Balance(alice, toK2(2'000) - 3 * f));
1261 }
1262 else
1263 {
1264 env.require(Balance(alice, toK2(600)));
1265 }
1266 aliceOffers = offersOnAccount(env, alice);
1267 BEAST_EXPECT(aliceOffers.size() == 1);
1268 for (auto const& offerPtr : aliceOffers)
1269 {
1270 auto const offer = *offerPtr;
1271 BEAST_EXPECT(offer[sfLedgerEntryType] == ltOFFER);
1272 BEAST_EXPECT(offer[sfTakerGets] == tok.remTakerGets);
1273 BEAST_EXPECT(offer[sfTakerPays] == tok.remTakerPays);
1274 }
1275 };
1276
1277 test(initXRP);
1278 test(initMPT);
1279 test(initIOU);
1280 test(initIOU1);
1281 }
1282
1283 void
1284 testSelfFundedXRPEndpoint(bool consumeOffer, FeatureBitset features)
1285 {
1286 // Test that the deferred credit table is not bypassed for
1287 // XRPEndpointSteps. If the account in the first step is
1288 // sending XRP and that account also owns an offer that
1289 // receives XRP, it should not be possible for that step to
1290 // use the XRP received in the offer as part of the payment.
1291 testcase("Self funded XRPEndpoint");
1292
1293 using namespace jtx;
1294
1295 Env env(*this, features);
1296
1297 auto const alice = Account("alice");
1298 auto const gw = Account("gw");
1299
1300 env.fund(XRP(10'000), alice, gw);
1301
1302 MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 20});
1303
1304 env(pay(gw, alice, usd(10)));
1305 env(offer(alice, XRP(50'000), usd(10)));
1306
1307 // Consuming the offer changes the owner count, which could
1308 // also cause liquidity to decrease in the forward pass
1309 auto const toSend = consumeOffer ? usd(10) : usd(9);
1310 env(pay(alice, alice, toSend),
1311 Path(~usd),
1312 Sendmax(XRP(20'000)),
1313 Txflags(tfPartialPayment | tfNoRippleDirect));
1314 }
1315
1316 void
1318 {
1319 testcase("Unfunded Offer");
1320
1321 using namespace jtx;
1322 {
1323 // Test reverse
1324 Env env(*this, features);
1325
1326 auto const alice = Account("alice");
1327 auto const bob = Account("bob");
1328 auto const gw = Account("gw");
1329
1330 env.fund(XRP(100'000), alice, bob, gw);
1331
1332 MPT const usd =
1333 MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 20E+17});
1334
1335 // scale by 17
1336 STAmount const tinyAmt1{usd, 9'000'000'000'000'000ll, 0, false, STAmount::Unchecked{}};
1337 STAmount const tinyAmt3{usd, 9'000'000'000'000'003ll, 0, false, STAmount::Unchecked{}};
1338
1339 env(offer(gw, drops(9'000'000'000), tinyAmt3));
1340
1341 env(pay(alice, bob, tinyAmt1),
1342 Path(~usd),
1343 Sendmax(drops(9'000'000'000)),
1344 Txflags(tfNoRippleDirect));
1345
1346 BEAST_EXPECT(!isOffer(env, gw, XRP(0), usd(0)));
1347 }
1348 {
1349 // Test forward
1350 Env env(*this, features);
1351
1352 auto const alice = Account("alice");
1353 auto const bob = Account("bob");
1354 auto const gw = Account("gw");
1355
1356 env.fund(XRP(100'000), alice, bob, gw);
1357
1358 MPT const usd =
1359 MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 20E+17});
1360
1361 // scale by 17
1362 STAmount const tinyAmt1{usd, 9'000'000'000'000'000ll, 0, false, STAmount::Unchecked{}};
1363 STAmount const tinyAmt3{usd, 9'000'000'000'000'003ll, 0, false, STAmount::Unchecked{}};
1364
1365 env(pay(gw, alice, tinyAmt1));
1366
1367 env(offer(gw, tinyAmt3, drops(9'000'000'000)));
1368 env(pay(alice, bob, drops(9'000'000'000)),
1369 Path(~XRP),
1370 Sendmax(usd(static_cast<std::uint64_t>(1E+17))),
1371 Txflags(tfNoRippleDirect));
1372
1373 BEAST_EXPECT(!isOffer(env, gw, usd(0), XRP(0)));
1374 }
1375 }
1376
1377 void
1379 {
1380 testcase("ReexecuteDirectStep");
1381
1382 using namespace jtx;
1383 Env env(*this, features);
1384
1385 auto const alice = Account("alice");
1386 auto const bob = Account("bob");
1387 auto const gw = Account("gw");
1388
1389 env.fund(XRP(10'000), alice, bob, gw);
1390
1391 // scale by 16
1392 MPT const usd =
1393 MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = 100E+16});
1394
1395 env(
1396 pay(gw,
1397 alice,
1398 // 12.55....
1399 STAmount{usd, std::uint64_t(1255555555555555ull), 2, false}));
1400
1401 env(offer(
1402 gw,
1403 // 5.0...
1404 STAmount{usd, std::uint64_t(5000000000000000ull), 1, false},
1405 XRP(1000)));
1406
1407 env(offer(
1408 gw,
1409 // .555...
1410 STAmount{usd, std::uint64_t(5555555555555555ull), 0, false},
1411 XRP(10)));
1412
1413 env(offer(
1414 gw,
1415 // 4.44....
1416 STAmount{usd, std::uint64_t(4444444444444444ull), 1, false},
1417 XRP(.1)));
1418
1419 env(offer(
1420 alice,
1421 // 17
1422 STAmount{usd, std::uint64_t(1700000000000000ull), 0, false},
1423 XRP(.001)));
1424
1425 env(pay(alice, bob, XRP(10'000)),
1426 Path(~XRP),
1427 Sendmax(usd(static_cast<std::uint64_t>(100E+16))),
1428 Txflags(tfPartialPayment | tfNoRippleDirect));
1429 }
1430
1431 void
1433 {
1434 // The new payment code used to assert if an offer was made
1435 // for more XRP than the offering account held. This unit
1436 // test reproduces that failing case.
1437 testcase("Self crossing low quality offer");
1438
1439 using namespace jtx;
1440
1441 Env env(*this, features);
1442
1443 auto const ann = Account("ann");
1444 auto const gw = Account("gateway");
1445
1446 auto const fee = env.current()->fees().base;
1447 env.fund(reserve(env, 2) + drops(9999640) + fee, ann);
1448 env.fund(reserve(env, 2) + fee * 4, gw);
1449
1450 // scale by 5
1451 MPT const ctb = MPTTester(
1452 {.env = env,
1453 .issuer = gw,
1454 .holders = {ann},
1455 .transferFee = 2'000, // 2%
1456 .maxAmt = 1'000'000});
1457
1458 env(pay(gw, ann, ctb(285'600)));
1459 env.close();
1460
1461 env(offer(ann, drops(365'611'702'030), ctb(571'300)));
1462 env.close();
1463
1464 // This payment caused assert.
1465 env(pay(ann, ann, ctb(68'700)), Sendmax(drops(20'000'000'000)), Txflags(tfPartialPayment));
1466 }
1467
1468 void
1470 {
1471 testcase("Empty Strand");
1472 using namespace jtx;
1473
1474 auto const alice = Account("alice");
1475
1476 Env env(*this, features);
1477
1478 env.fund(XRP(10000), alice);
1479
1480 MPT const usd;
1481
1482 env(pay(alice, alice, usd(100)), Path(~usd), Ter(temBAD_PATH));
1483 }
1484
1485 void
1487 {
1488 testcase("Circular XRP");
1489
1490 using namespace jtx;
1491 auto const alice = Account("alice");
1492 auto const bob = Account("bob");
1493 auto const gw = Account("gw");
1494
1495 {
1496 // Payment path starting with XRP
1497 auto test = [&](auto&& issue1, auto&& issue2) {
1498 Env env(*this);
1499 env.fund(XRP(10'000), alice, bob, gw);
1500
1501 auto const usd =
1502 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob}});
1503 auto const eur =
1504 issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {alice, bob}});
1505 env(pay(gw, alice, usd(100)));
1506 env(pay(gw, alice, eur(100)));
1507 env.close();
1508
1509 env(offer(alice, XRP(100), usd(100)), Txflags(tfPassive));
1510 env(offer(alice, usd(100), XRP(100)), Txflags(tfPassive));
1511 env(offer(alice, XRP(100), eur(100)), Txflags(tfPassive));
1512 env.close();
1513
1514 TER const expectedTer = TER{temBAD_PATH_LOOP};
1515 env(pay(alice, bob, eur(1)),
1516 Path(~usd, ~XRP, ~eur),
1517 Sendmax(XRP(1)),
1518 Txflags(tfNoRippleDirect),
1519 Ter(expectedTer));
1520 };
1522 }
1523 {
1524 // Payment path ending with XRP
1525 auto test = [&](auto&& issue1, auto&& issue2) {
1526 Env env(*this);
1527 env.fund(XRP(10'000), alice, bob, gw);
1528 auto const usd =
1529 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob}});
1530 auto const eur =
1531 issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {alice, bob}});
1532 env(pay(gw, alice, usd(100)));
1533 env(pay(gw, alice, eur(100)));
1534 env.close();
1535
1536 env(offer(alice, XRP(100), usd(100)), Txflags(tfPassive));
1537 env(offer(alice, eur(100), XRP(100)), Txflags(tfPassive));
1538 env.close();
1539 // EUR -> //XRP -> //USD ->XRP
1540 env(pay(alice, bob, XRP(1)),
1541 Path(~XRP, ~usd, ~XRP),
1542 Sendmax(eur(1)),
1543 Txflags(tfNoRippleDirect),
1545 };
1547 }
1548 {
1549 // Payment where loop is formed in the middle of the
1550 // path, not on an endpoint
1551 auto test = [&](auto&& issue1, auto&& issue2, auto&& issue3) {
1552 Env env(*this);
1553 env.fund(XRP(10'000), alice, bob, gw);
1554 env.close();
1555 auto const usd =
1556 issue1({.env = env, .token = "USD", .issuer = gw, .holders = {alice, bob}});
1557 auto const eur =
1558 issue2({.env = env, .token = "EUR", .issuer = gw, .holders = {alice, bob}});
1559 auto const jpy =
1560 issue3({.env = env, .token = "JPY", .issuer = gw, .holders = {alice, bob}});
1561 env(pay(gw, alice, usd(100)));
1562 env(pay(gw, alice, eur(100)));
1563 env(pay(gw, alice, jpy(100)));
1564 env.close();
1565
1566 env(offer(alice, usd(100), XRP(100)), Txflags(tfPassive));
1567 env(offer(alice, XRP(100), eur(100)), Txflags(tfPassive));
1568 env(offer(alice, eur(100), XRP(100)), Txflags(tfPassive));
1569 env(offer(alice, XRP(100), jpy(100)), Txflags(tfPassive));
1570 env.close();
1571
1572 env(pay(alice, bob, jpy(1)),
1573 Path(~XRP, ~eur, ~XRP, ~jpy),
1574 Sendmax(usd(1)),
1575 Txflags(tfNoRippleDirect),
1577 };
1579 }
1580 }
1581
1582 void
1584 {
1585 testcase("Max Flow/Self Payment Edge Cases");
1586 using namespace jtx;
1587 Account const gw("gw");
1588 Account const alice("alice");
1589 Account const carol("carol");
1590 Account const bob("bob");
1591
1592 // Direct payment between holders.
1593 {
1594 Env env(*this);
1595
1596 env.fund(XRP(1'000), gw, alice, carol);
1597
1598 MPT const usd =
1599 MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}, .maxAmt = 100});
1600
1601 env(pay(gw, alice, usd(100)));
1602
1603 env(pay(alice, carol, usd(100)));
1604
1605 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1606 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1607 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1608 }
1609
1610 // Direct payment between holders. Partial payment limited
1611 // by holder funds.
1612 {
1613 Env env(*this);
1614
1615 env.fund(XRP(1'000), gw, alice, carol);
1616
1617 MPT const usd =
1618 MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}, .maxAmt = 100});
1619
1620 env(pay(gw, alice, usd(80)));
1621
1622 env(pay(alice, carol, usd(100)), Txflags(tfPartialPayment));
1623
1624 BEAST_EXPECT(env.balance(gw, usd) == usd(-80));
1625 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1626 BEAST_EXPECT(env.balance(carol, usd) == usd(80));
1627 }
1628
1629 // Direct payment between holders. Partial payment limited
1630 // by holder funds. OutstandingAmount is already at max
1631 // before the payment.
1632 {
1633 Env env(*this);
1634
1635 env.fund(XRP(1'000), gw, alice, carol, bob);
1636
1637 MPT const usd = MPTTester(
1638 {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .maxAmt = 100});
1639
1640 env(pay(gw, bob, usd(20)));
1641 env(pay(gw, alice, usd(80)));
1642
1643 env(pay(alice, carol, usd(100)), Txflags(tfPartialPayment));
1644
1645 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1646 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1647 BEAST_EXPECT(env.balance(carol, usd) == usd(80));
1648 }
1649
1650 // Cross-currency payment holder to holder. Holder owns an
1651 // offer. OutstandingAmount is already at max before the
1652 // payment.
1653 {
1654 Env env(*this);
1655
1656 env.fund(XRP(1'000), gw, alice, carol, bob);
1657
1658 MPT const usd =
1659 MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}, .maxAmt = 100});
1660
1661 env(pay(gw, alice, usd(100)));
1662
1663 env(offer(alice, XRP(100), usd(100)));
1664
1665 env(pay(bob, carol, usd(100)), Sendmax(XRP(100)), Path(~usd));
1666
1667 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1668 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1669 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1670 }
1671
1672 // Cross-currency payment holder to holder. Issuer owns an
1673 // offer. OutstandingAmount is already at max before the
1674 // payment. Since an issuer owns the offer, it issues more
1675 // tokens to another holder, and the payment fails.
1676 {
1677 Env env(*this);
1678
1679 env.fund(XRP(1'000), gw, alice, carol);
1680
1681 MPT const usd =
1682 MPTTester({.env = env, .issuer = gw, .holders = {carol}, .maxAmt = 100});
1683
1684 env(pay(gw, carol, usd(100)));
1685
1686 env(offer(gw, XRP(100), usd(100)));
1687
1688 env(pay(alice, carol, usd(100)),
1689 Sendmax(XRP(100)),
1690 Path(~usd),
1691 Txflags(tfPartialPayment),
1692 Ter(tecPATH_DRY));
1693
1694 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1695 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1696 }
1697
1698 // Cross-currency payment holder to holder. Issuer owns an
1699 // offer. OutstandingAmount is at 80USD before the payment.
1700 // Consequently, the issuer can issue 20USD more.
1701 {
1702 Env env(*this);
1703
1704 env.fund(XRP(1'000), gw, alice, carol);
1705
1706 MPT const usd =
1707 MPTTester({.env = env, .issuer = gw, .holders = {carol}, .maxAmt = 100});
1708
1709 env(pay(gw, carol, usd(80)));
1710
1711 env(offer(gw, XRP(100), usd(100)));
1712
1713 env(pay(alice, carol, usd(100)),
1714 Sendmax(XRP(100)),
1715 Path(~usd),
1716 Txflags(tfPartialPayment));
1717
1718 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1719 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1720 }
1721
1722 // Cross-currency payment holder to holder. Holder owns an
1723 // offer. The offer buys more MPT's. The payment fails since
1724 // OutstandingAmount is already at max.
1725 {
1726 Env env(*this);
1727
1728 env.fund(XRP(1'000), gw, alice);
1729
1730 MPT const usd =
1731 MPTTester({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 100});
1732
1733 env(pay(gw, alice, usd(100)));
1734
1735 env(offer(alice, usd(100), XRP(100)));
1736
1737 env(pay(gw, alice, XRP(100)), Sendmax(usd(100)), Path(~XRP), Ter(tecPATH_PARTIAL));
1738
1739 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1740 BEAST_EXPECT(env.balance(alice, usd) == usd(100));
1741 }
1742
1743 // Cross-currency payment issuer to holder. Holder owns an
1744 // offer. The offer buys EUR, OutstandingAmount goes to max,
1745 // no overflow. The offer redeems USD to the issuer. While
1746 // OutstandingAmount is already at max, the payment succeeds
1747 // since USD is redeemed.
1748 {
1749 auto test = [&](auto&& issue1, auto&& issue2) {
1750 Env env(*this);
1751
1752 env.fund(XRP(1'000), gw, alice, carol);
1753 env.close();
1754
1755 auto const usd = issue1(
1756 {.env = env,
1757 .token = "USD",
1758 .issuer = gw,
1759 .holders = {alice, carol},
1760 .limit = 100});
1761 using TUsd = std::decay_t<decltype(usd)>;
1762 auto const eur = issue2(
1763 {.env = env,
1764 .token = "EUR",
1765 .issuer = gw,
1766 .holders = {alice, carol},
1767 .limit = 100});
1768
1769 env(pay(gw, alice, usd(100)));
1770
1771 env(offer(alice, eur(100), usd(100)));
1772
1773 env(pay(gw, carol, usd(100)), Sendmax(eur(100)), Path(~usd));
1774
1775 if constexpr (std::is_same_v<TUsd, MPT>)
1776 BEAST_EXPECT(env.balance(gw, usd) == usd(-100));
1777 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1778 BEAST_EXPECT(env.balance(alice, eur) == eur(100));
1779 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1780 };
1782 }
1783
1784 // Cross-currency payment holder to holder. Offer is owned
1785 // by destination account. OutstandingAmount is not at max.
1786 {
1787 Env env(*this);
1788
1789 env.fund(XRP(1'000), gw, alice, carol);
1790
1791 MPT const usd =
1792 MPTTester({.env = env, .issuer = gw, .holders = {carol}, .maxAmt = 120});
1793
1794 env(pay(gw, carol, usd(100)));
1795
1796 env(offer(carol, XRP(100), usd(100)));
1797
1798 env(pay(alice, carol, usd(100)),
1799 Path(~usd),
1800 Sendmax(XRP(100)),
1801 Txflags(tfPartialPayment));
1802
1803 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1804 }
1805
1806 // Cross-currency payment holder to holder. Offer is owned
1807 // by destination account. OutstandingAmount is already at
1808 // max.
1809 {
1810 Env env(*this);
1811
1812 env.fund(XRP(1'000), gw, alice, carol);
1813
1814 MPT const usd =
1815 MPTTester({.env = env, .issuer = gw, .holders = {carol}, .maxAmt = 100});
1816
1817 env(pay(gw, carol, usd(100)));
1818
1819 env(offer(carol, XRP(100), usd(100)));
1820
1821 env(pay(alice, carol, usd(100)),
1822 Path(~usd),
1823 Sendmax(XRP(100)),
1824 Txflags(tfPartialPayment));
1825
1826 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
1827 }
1828
1829 // Cross-currency payment holder to holder. Multiple offers
1830 // with different owners - some holders, some issuer.
1831 {
1832 auto test = [&](auto&& issue1, auto&& issue2) {
1833 Env env(*this);
1834
1835 env.fund(XRP(1'000), gw, alice, carol, bob);
1836 env.close();
1837
1838 auto const usd = issue1(
1839 {.env = env,
1840 .token = "USD",
1841 .issuer = gw,
1842 .holders = {alice, carol, bob},
1843 .limit = 1'000});
1844 using TUsd = std::decay_t<decltype(usd)>;
1845 auto const eur = issue2(
1846 {.env = env,
1847 .token = "EUR",
1848 .issuer = gw,
1849 .holders = {alice, carol, bob},
1850 .limit = 1'000});
1851 using TEur = std::decay_t<decltype(eur)>;
1852
1853 env(pay(gw, alice, usd(600)));
1854 env(pay(gw, carol, eur(700)));
1855
1856 env(offer(alice, eur(100), usd(105)));
1857 env(offer(gw, eur(100), usd(104)));
1858 env(offer(gw, eur(100), usd(103)));
1859 env(offer(gw, eur(100), usd(102)));
1860 env(offer(gw, eur(100), usd(101)));
1861 env(offer(gw, eur(100), usd(100)));
1862
1863 env(pay(carol, bob, usd(2'000)),
1864 Sendmax(eur(2'000)),
1865 Path(~usd),
1866 Txflags(tfPartialPayment));
1867
1868 if constexpr (std::is_same_v<TUsd, MPT>)
1869 {
1870 BEAST_EXPECT(env.balance(gw, usd) == usd(-1'000));
1871 BEAST_EXPECT(env.balance(alice, usd) == usd(495));
1872 BEAST_EXPECT(env.balance(bob, usd) == usd(505));
1873 }
1874 else
1875 {
1876 BEAST_EXPECT(env.balance(gw, usd) == usd(0));
1877 BEAST_EXPECT(env.balance(alice, usd) == usd(495));
1878 // all offers are consumed since the limit is different
1879 // for the holders
1880 BEAST_EXPECT(env.balance(bob, usd) == usd(615));
1881 }
1882 if constexpr (std::is_same_v<TEur, MPT>)
1883 {
1884 if constexpr (std::is_same_v<TUsd, MPT>)
1885 {
1886 BEAST_EXPECT(env.balance(carol, eur) == eur(210));
1887 }
1888 else
1889 {
1890 // carol sells 600USD since all offers are consumed
1891 BEAST_EXPECT(env.balance(carol, eur) == eur(100));
1892 }
1893 }
1894 else
1895 {
1896 BEAST_EXPECT(
1897 env.balance(carol, eur) == STAmount(eur, UINT64_C(209'9009900990099), -13));
1898 }
1899 // 100/101 is partially crossed (90/91) and 100/100 is
1900 // unfunded when MPT. All offers are consumed if IOU.
1901 env.require(Offers(gw, 0));
1902 // alice's offer is consumed.
1903 env.require(Offers(alice, 0));
1904 };
1906 }
1907
1908 // Cross-currency payment holder to holder. Multiple offers
1909 // with different owners - some holders, some issuer. Source
1910 // and destination account is the same.
1911 {
1912 Env env(*this);
1913
1914 env.fund(XRP(1'000), gw, alice, carol);
1915
1916 MPT const usd =
1917 MPTTester({.env = env, .issuer = gw, .holders = {alice, carol}, .maxAmt = 2'000});
1918
1919 env(pay(gw, carol, usd(1'000)));
1920 env(pay(gw, alice, usd(600)));
1921
1922 env(offer(gw, XRP(5), usd(11)));
1923 env(offer(gw, XRP(6), usd(13)));
1924 env(offer(carol, XRP(7), usd(15)));
1925 env(offer(carol, XRP(17), usd(35)));
1926 env(offer(carol, XRP(23), usd(47)));
1927 env(offer(alice, XRP(10), usd(19)));
1928 env(offer(alice, XRP(15), usd(28)));
1929 env(offer(alice, XRP(25), usd(46)));
1930
1931 env(pay(carol, carol, usd(200)), Sendmax(XRP(100)), Txflags(tfPartialPayment));
1932
1933 BEAST_EXPECT(env.balance(gw, usd) == usd(-1'624));
1934 BEAST_EXPECT(env.balance(carol, usd) == usd(1'102));
1935 env.require(Offers(carol, 0));
1936 env.require(Offers(gw, 0));
1937 // 100 XRP's = 5+6+7+17+23+10+15+17(25-8)
1938 BEAST_EXPECT(isOffer(env, alice, XRP(8), usd(15)));
1939 }
1940
1941 // Cross-currency payment holder to holder. Multiple offers
1942 // with different owners - some holders, some issuer.
1943 {
1944 Env env(*this);
1945 env.fund(XRP(1'000), gw, alice, carol, bob);
1946
1947 MPT const usd =
1948 MPTTester({.env = env, .issuer = gw, .holders = {alice, carol, bob}, .maxAmt = 30});
1949
1950 env(pay(gw, alice, usd(12))); // 12, 15, 20
1951 env(pay(gw, bob, usd(5))); // 5, 5, 10
1952
1953 env(offer(alice, XRP(10), usd(12)));
1954 env(offer(gw, XRP(10), usd(11)));
1955 env(offer(bob, XRP(10), usd(10)));
1956
1957 env(pay(carol, bob, usd(30)), Sendmax(XRP(30)), Txflags(tfPartialPayment), Path(~usd));
1958 BEAST_EXPECT(env.balance(gw, usd) == usd(-28));
1959 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
1960 // 12+11+5
1961 BEAST_EXPECT(env.balance(bob, usd) == usd(28));
1962 }
1963
1964 // Cross-currency payment two steps. Second book step
1965 // issues, first book step redeems.
1966 {
1967 Account const dan{"dan"};
1968 Account const john{"john"};
1969 Account const ed{"ed"};
1970 Account const sam{"sam"};
1971 Account const bill{"bill"};
1972
1973 struct TestData
1974 {
1975 int maxAmt;
1976 int sendMax;
1977 int dstTrustLimit;
1978 int dstExpectEUR;
1979 int outstandingUSD;
1980 int expEdBuyUSD;
1981 int expDanBuyUSD;
1982 int expBobSellUSD;
1983 int expGwXRP; // whole XRP excluding the fees
1984 std::uint8_t expOffersGw;
1985 bool lastGwBuyUSD;
1986 [[nodiscard]] std::uint8_t
1987 expOffersBob() const
1988 {
1989 return expBobSellUSD == 0 ? 1 : 0;
1990 }
1991 [[nodiscard]] std::uint8_t
1992 expOffersEd() const
1993 {
1994 // partially crossed if < 100
1995 return expEdBuyUSD < 100 ? 1 : 0;
1996 }
1997 [[nodiscard]] std::uint8_t
1998 expOffersDan() const
1999 {
2000 return expDanBuyUSD == 0 ? 1 : 0;
2001 }
2002 };
2003
2004 auto test = [&](TestData const& d) {
2005 Env env(*this);
2006 env.fund(XRP(1'000), gw, alice, carol, bob, dan, john, ed, sam, bill);
2007 env.close();
2008
2009 MPT const usd = MPTTester(
2010 {.env = env, .issuer = gw, .holders = {alice, carol, bob}, .maxAmt = d.maxAmt});
2011 auto const eur = gw["EUR"];
2012
2013 env(pay(gw, alice, usd(100)));
2014 env(pay(gw, carol, usd(100)));
2015 env(pay(gw, bob, usd(100)));
2016
2017 BEAST_EXPECT(env.balance(gw, usd) == usd(-300));
2018
2019 env(trust(john, eur(100)));
2020 env(trust(dan, eur(100)));
2021 env(trust(ed, eur(100)));
2022 env(trust(bill, eur(d.dstTrustLimit)));
2023
2024 env(pay(gw, john, eur(100)));
2025 env(pay(gw, dan, eur(100)));
2026 env(pay(gw, ed, eur(100)));
2027 env.close();
2028
2029 // Sell USD
2030 env(offer(alice, XRP(100), usd(100)));
2031 env.close(); // close after each create to ensure
2032 // the order
2033 env(offer(carol, XRP(100), usd(100)));
2034 env.close();
2035 if (!d.lastGwBuyUSD)
2036 {
2037 env(offer(gw, XRP(100), usd(100)));
2038 env.close();
2039 }
2040 env(offer(bob, XRP(100), usd(100)));
2041 env.close();
2042 if (d.lastGwBuyUSD)
2043 {
2044 env(offer(gw, XRP(100), usd(100)));
2045 env.close();
2046 }
2047 BEAST_EXPECT(expectOffers(env, alice, 1));
2048 BEAST_EXPECT(expectOffers(env, carol, 1));
2049 BEAST_EXPECT(expectOffers(env, gw, 1));
2050 BEAST_EXPECT(expectOffers(env, bob, 1));
2051
2052 // Buy USD
2053 env(offer(john, usd(100), eur(100)));
2054 env.close();
2055 env(offer(gw, usd(100), eur(100)));
2056 env.close();
2057 env(offer(dan, usd(100), eur(100)));
2058 env.close();
2059 env(offer(ed, usd(100), eur(100)));
2060 env.close();
2061 BEAST_EXPECT(expectOffers(env, john, 1));
2062 BEAST_EXPECT(expectOffers(env, gw, 2));
2063 BEAST_EXPECT(expectOffers(env, dan, 1));
2064 BEAST_EXPECT(expectOffers(env, ed, 1));
2065
2066 env(pay(sam, bill, eur(400)),
2067 Sendmax(XRP(d.sendMax)),
2068 Path(~usd, ~eur),
2069 Txflags(tfPartialPayment | tfNoRippleDirect));
2070 env.close();
2071
2072 auto const baseFee = env.current()->fees().base.drops();
2073 BEAST_EXPECT(env.balance(bill, eur) == eur(d.dstExpectEUR));
2074 BEAST_EXPECT(env.balance(john, usd) == usd(100));
2075 BEAST_EXPECT(env.balance(dan, usd) == usd(d.expDanBuyUSD));
2076 BEAST_EXPECT(env.balance(ed, usd) == usd(d.expEdBuyUSD));
2077 BEAST_EXPECT(env.balance(gw, usd) == usd(-d.outstandingUSD));
2078 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
2079 BEAST_EXPECT(env.balance(carol, usd) == usd(0));
2080 BEAST_EXPECT(env.balance(bob, usd) == usd(100 - d.expBobSellUSD));
2081 BEAST_EXPECT(env.balance(gw) == XRPAmount{d.expGwXRP * kDropsPerXrp - baseFee * 9});
2082 BEAST_EXPECT(expectOffers(env, john, 0));
2083 BEAST_EXPECT(expectOffers(env, gw, d.expOffersGw));
2084 BEAST_EXPECT(expectOffers(env, dan, d.expOffersDan()));
2085 BEAST_EXPECT(expectOffers(env, ed, d.expOffersEd()));
2086 BEAST_EXPECT(expectOffers(env, alice, 0));
2087 BEAST_EXPECT(expectOffers(env, carol, 0));
2088 BEAST_EXPECT(expectOffers(env, bob, d.expOffersBob()));
2089 };
2090
2091 // clang-format off
2093 // Sell USD: alice, carol, bob, gw are consumed.
2094 // Buy USD: john, gw, dan, ed are consumed.
2095 // gw's sell USD is consumed because there is sufficient available balance (100USD).
2096 // but OutstandingAmount is 300USD because gw's sell offer is balanced out by
2097 // gw's buy offer.
2098 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
2099 { .maxAmt=400, .sendMax=400, .dstTrustLimit=400, .dstExpectEUR=400, .outstandingUSD=300, .expEdBuyUSD=100, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1100, .expOffersGw=0, .lastGwBuyUSD=false},
2100 // Sell USD: alice, carol, bob, gw are consumed.
2101 // Buy USD: john, gw, dan, ed (partially) are consumed.
2102 // gw's sell USD is partially consumed because there is available balance (50USD).
2103 // OutstandingAmount is 250USD because gw's sell offer is partially balanced by
2104 // gw's buy offer. ed's offer is on the books because it's partially crossed.
2105 // gw's offer is removed from the order book because it's partially consumed and
2106 // the remaining offer is unfunded.
2107 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
2108 { .maxAmt=350, .sendMax=400, .dstTrustLimit=400, .dstExpectEUR=350, .outstandingUSD=250, .expEdBuyUSD=50, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=0, .lastGwBuyUSD=false},
2109 // Sell USD: alice, carol, bob are consumed; gw's is unfunded
2110 // since OutstandingAmount is initially at MaximumAmount.
2111 // Buy USD: john, gw, dan are consumed; ed's remains on the order
2112 // book since 300USD is the sell limit.
2113 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
2114 { .maxAmt=300, .sendMax=400, .dstTrustLimit=400, .dstExpectEUR=300, .outstandingUSD=200, .expEdBuyUSD=0, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=false},
2115 // Same as above. bill's trustline limit sets the output to 300USD.
2116 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
2117 { .maxAmt=300, .sendMax=400, .dstTrustLimit=300, .dstExpectEUR=300, .outstandingUSD=200, .expEdBuyUSD=0, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=false},
2118 // Sell USD: alice, carol, bob are consumed; gw's removed from
2119 // the order book since it's unfunded.
2120 // Buy USD: john, gw, dan are consumed; ed's remains on the order
2121 // book since 300USD is the limit.
2122 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
2123 { .maxAmt=300, .sendMax=400, .dstTrustLimit=300, .dstExpectEUR=300, .outstandingUSD=200, .expEdBuyUSD=0, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=true},
2124 // Sell USD: alice, carol are consumed; gw's removed from
2125 // the order book in rev pass since it's unfunded; bob's
2126 // remains on the order book.
2127 // Buy USD: john, gw; ed's, dan's remains on the order
2128 // book since 300USD is the limit.
2129 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
2130 { .maxAmt=300, .sendMax=200, .dstTrustLimit=300, .dstExpectEUR=200, .outstandingUSD=200, .expEdBuyUSD=0, .expDanBuyUSD=0, .expBobSellUSD=0, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=false},
2131 // Same as three tests above since limited by buy 300USD (gw offer is unfunded)
2132 //*maxAmt sendMax limitEUR expectEUR outstandingUSD edBuy danBuy bobSell gwXRP offersGw lastGw
2133 { .maxAmt=300, .sendMax=380, .dstTrustLimit=400, .dstExpectEUR=300, .outstandingUSD=200, .expEdBuyUSD=0, .expDanBuyUSD=100, .expBobSellUSD=100, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=false},
2134 };
2135 // clang-format on
2136 for (auto const& t : tests)
2137 test(t);
2138 }
2139
2140 // Cross-currency payment. BookStep issues, the first step
2141 // redeems.
2142 {
2143 Account const ed{"ed"};
2144
2145 struct TestData
2146 {
2147 int maxAmt;
2148 int sendMax;
2149 int gwOffer; // quality == 1
2150 int dstExpectXRP;
2151 int outstandingUSD;
2152 int expBobBuyUSD;
2153 int expGwXRP; // whole XRP excluding the fees
2154 std::uint8_t expOffersGw;
2155 bool lastGwBuyUSD;
2156 [[nodiscard]] std::uint8_t
2157 expOffersBob() const
2158 {
2159 // partially crossed if < 100
2160 return expBobBuyUSD < 100 ? 1 : 0;
2161 }
2162 };
2163
2164 auto test = [&](TestData const& d) {
2165 Env env(*this);
2166 env.fund(XRP(1'000), gw, alice, carol, bob, ed);
2167 env.close();
2168
2169 MPT const usd =
2170 MPTTester({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = d.maxAmt});
2171
2172 env(pay(gw, alice, usd(300)));
2173 env.close();
2174
2175 env(offer(carol, usd(100), XRP(100)));
2176 env.close();
2177 if (!d.lastGwBuyUSD)
2178 {
2179 env(offer(gw, usd(d.gwOffer), XRP(d.gwOffer)));
2180 env.close();
2181 }
2182 env(offer(bob, usd(100), XRP(100)));
2183 env.close();
2184 if (d.lastGwBuyUSD)
2185 {
2186 env(offer(gw, usd(d.gwOffer), XRP(d.gwOffer)));
2187 env.close();
2188 }
2189
2190 BEAST_EXPECT(expectOffers(env, carol, 1));
2191 BEAST_EXPECT(expectOffers(env, bob, 1));
2192 BEAST_EXPECT(expectOffers(env, gw, 1));
2193 BEAST_EXPECT(env.balance(gw, usd) == usd(-300));
2194
2195 env(pay(alice, ed, XRP(300)),
2196 Sendmax(usd(d.sendMax)),
2197 Path(~XRP),
2198 Txflags(tfPartialPayment | tfNoRippleDirect));
2199 env.close();
2200
2201 auto const baseFee = env.current()->fees().base.drops();
2202 BEAST_EXPECT(env.balance(alice, usd) == usd(300 - d.sendMax));
2203 BEAST_EXPECT(env.balance(carol, usd) == usd(100));
2204 BEAST_EXPECT(env.balance(bob, usd) == usd(d.expBobBuyUSD));
2205 BEAST_EXPECT(env.balance(ed) == XRP(d.dstExpectXRP));
2206 BEAST_EXPECT(env.balance(gw, usd) == usd(-d.outstandingUSD));
2207 BEAST_EXPECT(env.balance(gw) == XRPAmount{d.expGwXRP * kDropsPerXrp - baseFee * 3});
2208 BEAST_EXPECT(expectOffers(env, carol, 0));
2209 BEAST_EXPECT(expectOffers(env, bob, d.expOffersBob()));
2210 BEAST_EXPECT(expectOffers(env, gw, d.expOffersGw));
2211 };
2212
2213 // clang-format off
2215 // Buy USD: carol, gw, bob are consumed.
2216 // Gw gets 300USD from alice; carol and bob buy 200USD,
2217 // therefore OutstandingAmount is 200.
2218 //*maxAmt sendMax gwOffer dstXRP outstandingUSD bobBuy gwXRP offersGw lastGw
2219 { .maxAmt=300, .sendMax=300, .gwOffer=100, .dstExpectXRP=1300, .outstandingUSD=200, .expBobBuyUSD=100, .expGwXRP=900, .expOffersGw=0, .lastGwBuyUSD=false},
2220 // Same as above. Gw offer location in the order book doesn't matter
2221 //*maxAmt sendMax gwOffer dstXRP outstandingUSD bobBuy gwXRP offersGw lastGw
2222 { .maxAmt=300, .sendMax=300, .gwOffer=100, .dstExpectXRP=1300, .outstandingUSD=200, .expBobBuyUSD=100, .expGwXRP=900, .expOffersGw=0, .lastGwBuyUSD=true},
2223 // Buy USD: carol, gw are consumed. bob's offer remains on the order book.
2224 // Gw gets 300USD from alice; carol buys 100USD,
2225 // therefore OutstandingAmount is 100.
2226 //*maxAmt sendMax gwOffer dstXRP outstandingUSD bobBuy gwXRP offersGw lastGw
2227 { .maxAmt=300, .sendMax=300, .gwOffer=200, .dstExpectXRP=1300, .outstandingUSD=100, .expBobBuyUSD=0, .expGwXRP=800, .expOffersGw=0, .lastGwBuyUSD=false},
2228 // Buy USD: carol, bob are consumed; gw's is partially consumed (100/100) since it's last.
2229 // Gw gets 300USD from alice; carol and bob buy 200USD,
2230 // therefore OutstandingAmount is 200.
2231 //*maxAmt sendMax gwOffer dstXRP outstandingUSD bobBuy gwXRP offersGw lastGw
2232 { .maxAmt=300, .sendMax=300, .gwOffer=200, .dstExpectXRP=1300, .outstandingUSD=200, .expBobBuyUSD=100, .expGwXRP=900, .expOffersGw=1, .lastGwBuyUSD=true},
2233 // Buy USD: carol, bob are consumed; gw's is partially consumed (50/50) since it's last
2234 // and sendMax limits the output.
2235 // Gw gets 250USD from alice; carol and bob buy 200USD, alice has 50USD left,
2236 // therefore OutstandingAmount is 200.
2237 //*maxAmt sendMax gwOffer dstXRP outstandingUSD bobBuy gwXRP offersGw lastGw
2238 { .maxAmt=300, .sendMax=250, .gwOffer=200, .dstExpectXRP=1250, .outstandingUSD=250, .expBobBuyUSD=100, .expGwXRP=950, .expOffersGw=1, .lastGwBuyUSD=true},
2239 };
2240 // clang-format on
2241 for (auto const& t : tests)
2242 test(t);
2243 }
2244
2245 // Cross-currency payment. BookStep redeems, the last step
2246 // issues.
2247 {
2248 Account const ed{"ed"};
2249
2250 struct TestData
2251 {
2252 int maxAmt;
2253 int sendMax;
2254 int initDst;
2255 int gwOffer; // quality == 1
2256 int dstExpectUSD;
2257 int outstandingUSD;
2258 int expAliceXRP; // whole XRP excluding the fees
2259 int expBobSellUSD;
2260 int expGwXRP;
2261 std::uint8_t expOffersGw;
2262 bool lastGwBuyUSD;
2263 [[nodiscard]] std::uint8_t
2264 expOffersBob() const
2265 {
2266 return expBobSellUSD > 0 && expBobSellUSD < 100 ? 1 : 0;
2267 }
2268 };
2269
2270 auto test = [&](TestData const& d) {
2271 Env env(*this);
2272 env.fund(XRP(1'000), gw, alice, carol, bob, ed);
2273 env.close();
2274
2275 MPT const usd = MPTTester(
2276 {.env = env, .issuer = gw, .holders = {carol, bob, ed}, .maxAmt = d.maxAmt});
2277
2278 if (d.initDst != 0)
2279 env(pay(gw, ed, usd(d.initDst)));
2280 env(pay(gw, carol, usd(100)));
2281 env(pay(gw, bob, usd(100)));
2282 env.close();
2283
2284 env(offer(carol, XRP(100), usd(100)));
2285 env.close();
2286 if (!d.lastGwBuyUSD)
2287 {
2288 env(offer(gw, XRP(d.gwOffer), usd(d.gwOffer)));
2289 env.close();
2290 }
2291 env(offer(bob, XRP(100), usd(100)));
2292 env.close();
2293 if (d.lastGwBuyUSD)
2294 {
2295 env(offer(gw, XRP(d.gwOffer), usd(d.gwOffer)));
2296 env.close();
2297 }
2298
2299 BEAST_EXPECT(expectOffers(env, carol, 1));
2300 BEAST_EXPECT(expectOffers(env, bob, 1));
2301 BEAST_EXPECT(expectOffers(env, gw, 1));
2302 BEAST_EXPECT(env.balance(gw, usd) == usd(-200 - d.initDst));
2303
2304 env(pay(alice, ed, usd(300)),
2305 Sendmax(XRP(d.sendMax)),
2306 Path(~usd),
2307 Txflags(tfPartialPayment | tfNoRippleDirect));
2308 env.close();
2309
2310 auto const baseFee = env.current()->fees().base.drops();
2311 BEAST_EXPECT(
2312 env.balance(alice) == XRPAmount{d.expAliceXRP * kDropsPerXrp - baseFee});
2313 BEAST_EXPECT(env.balance(carol, usd) == usd(0));
2314 BEAST_EXPECT(env.balance(bob, usd) == usd(100 - d.expBobSellUSD));
2315 BEAST_EXPECT(env.balance(ed, usd) == usd(d.dstExpectUSD));
2316 BEAST_EXPECT(env.balance(gw, usd) == usd(-d.outstandingUSD));
2317 BEAST_EXPECT(
2318 env.balance(gw) ==
2319 XRPAmount{
2320 d.expGwXRP * kDropsPerXrp - baseFee * (4 + (d.initDst != 0 ? 1 : 0))});
2321 BEAST_EXPECT(expectOffers(env, carol, 0));
2322 BEAST_EXPECT(expectOffers(env, bob, d.expOffersBob()));
2323 BEAST_EXPECT(expectOffers(env, gw, d.expOffersGw));
2324 };
2325
2326 // clang-format off
2328 // Sell USD: carol, gw, bob are consumed.
2329 // ed buys 300USD from carol, gw, bob therefore OutstandingAmount is 300.
2330 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2331 { .maxAmt=300, .sendMax=300, .initDst=0, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=700, .expBobSellUSD=100, .expGwXRP=1100, .expOffersGw=0, .lastGwBuyUSD=false},
2332 // Same as above. Gw offer location in the order book doesn't matter
2333 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2334 { .maxAmt=300, .sendMax=300, .initDst=0, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=700, .expBobSellUSD=100, .expGwXRP=1100, .expOffersGw=0, .lastGwBuyUSD=true},
2335 // Sell USD: carol, bob are consumed, gw is partially consumed.
2336 // ed buys 200 from carol and bob and 50 from gw because gw can only issue 50
2337 // (300(max) - 200(carol+bob) - 50(ed)). ed buys 250 from carol, gw, bob and has 50 initially,
2338 // therefore OutstandingAmount is 300.
2339 // gw's offer is removed from the order book because it's partially consumed and the remaining
2340 // offer is unfunded.
2341 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2342 { .maxAmt=300, .sendMax=300, .initDst=50, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=750, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=0, .lastGwBuyUSD=false},
2343 // Same as above. Gw offer location in the order book doesn't matter.
2344 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2345 { .maxAmt=300, .sendMax=300, .initDst=50, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=750, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=0, .lastGwBuyUSD=true},
2346 // Same as above. Gw offer size doesn't matter.
2347 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2348 { .maxAmt=300, .sendMax=300, .initDst=50, .gwOffer=200, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=750, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=0, .lastGwBuyUSD=true},
2349 // Sell USD: carol, gw are consumed, bob is partially consumed.
2350 // ed buys 200 from carol and gw and 50 form bob because of sendMax limit. bob keeps 50,
2351 // therefore OutstandingAmount is 300.
2352 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2353 { .maxAmt=300, .sendMax=250, .initDst=0, .gwOffer=100, .dstExpectUSD=250, .outstandingUSD=300, .expAliceXRP=750, .expBobSellUSD=50, .expGwXRP=1100, .expOffersGw=0, .lastGwBuyUSD=false},
2354 // Sell USD: carol, bob are consumed, gw is partially consumed because of sendMax limit.
2355 // ed buys 200 from carol and bob and 50 from gw. Therefore, OutstandingAmount is 250.
2356 // gw's offer remains on the order book because it's partially consumed and has more funds.
2357 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2358 { .maxAmt=300, .sendMax=250, .initDst=0, .gwOffer=100, .dstExpectUSD=250, .outstandingUSD=250, .expAliceXRP=750, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=1, .lastGwBuyUSD=true},
2359 // Sell USD: carol, bob are consumed, gw is partially consumed because of sendMax limit, also
2360 // there is only 50 available to issue. ed buys 200 from carol and bob and 50 from gw, plus
2361 // he has initially 50, therefore OutstandingAmount is 300.
2362 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2363 { .maxAmt=300, .sendMax=250, .initDst=50, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=750, .expBobSellUSD=100, .expGwXRP=1050, .expOffersGw=0, .lastGwBuyUSD=true},
2364 // Sell USD: carol, bob are consumed, gw is not consumed because there is not available funds
2365 // to issue. ed buys 200 from carol and bob and, plus he has initially 100,
2366 // therefore OutstandingAmount is 300. gw offer is removed because it's unfunded.
2367 //*maxAmt sendMax initDst gwOffer dstUSD outstandingUSD aliceXRP bobSell gwXRP offersGw lastGw
2368 { .maxAmt=300, .sendMax=250, .initDst=100, .gwOffer=100, .dstExpectUSD=300, .outstandingUSD=300, .expAliceXRP=800, .expBobSellUSD=100, .expGwXRP=1000, .expOffersGw=0, .lastGwBuyUSD=true},
2369 };
2370 // clang-format on
2371 for (auto const& t : tests)
2372 test(t);
2373 }
2374
2375 // Cross-currency payment with BookStep as the first step.
2376 // BookStep limits the buy amount.
2377 {
2378 auto test = [&](int sendMax, std::uint16_t dstXRP, std::uint8_t expGwOffers) {
2379 Env env(*this);
2380 env.fund(XRP(1'000), gw, alice, carol);
2381
2382 MPT const usd = MPTTester({.env = env, .issuer = gw, .maxAmt = 300});
2383
2384 env(offer(carol, usd(400), XRP(400)));
2385 env(offer(gw, usd(100), XRP(100)));
2386 BEAST_EXPECT(expectOffers(env, carol, 1));
2387 BEAST_EXPECT(expectOffers(env, gw, 1));
2388
2389 env(pay(gw, alice, XRP(500)),
2390 Sendmax(usd(sendMax)),
2391 Path(~XRP),
2392 Txflags(tfPartialPayment | tfNoRippleDirect));
2393
2394 BEAST_EXPECT(env.balance(alice) == XRP(dstXRP));
2395 BEAST_EXPECT(env.balance(gw, usd) == usd(-300));
2396 BEAST_EXPECT(env.balance(carol, usd) == usd(300));
2397 BEAST_EXPECT(expectOffers(env, carol, 0));
2398 BEAST_EXPECT(expectOffers(env, gw, expGwOffers));
2399 };
2400 // carol's offer is partially consumed - 300USD/300XRP
2401 // because available amount to issue is 300USD. gw's
2402 // offer is fully consumed because it doesn't change
2403 // OutstandingAmount. Both offers are removed from the
2404 // order book - carol's offer is unfunded and gw's offer
2405 // is fully consumed.
2406 test(500, 1'400, 0);
2407 // carol's offer is partially consumed - 300USD/300XRP
2408 // because available amount to issue is 300USD. gw's
2409 // offer is partially consumed because of sendMax limit.
2410 // carol's offer is removed from the order book because
2411 // it's unfunded. gw's offer remains on the order book
2412 // because it's partially consumed and gw has more
2413 // funds.
2414 test(350, 1'350, 1);
2415 }
2416 }
2417
2418 void
2420 {
2421 // Regression: a cross-currency strand whose second book step
2422 // consumes the offer of a holder that is locked on the step's
2423 // in-asset (an MPT). The strand is XRP -> [book1: XRP/USD] ->
2424 // USD -> [book2: USD/EUR] -> EUR, so book2 has book_.in == USD
2425 // (an MPT) and its previous step is another BookStep. That is
2426 // exactly the checkMPTDEX() branch that trusts the preceding
2427 // BookStep and no longer re-checks isFrozen(owner, book_.in).
2428 //
2429 // The bypass the branch might appear to open does not exist:
2430 // for MPT, isDeepFrozen() == isFrozen() (frozen MPTs can neither
2431 // send nor receive), and OfferStream gates every offer through
2432 // isDeepFrozen(owner, assetIn) before it can reach checkMPTDEX().
2433 // So a locked mid-path holder's offer is removed by the liquidity
2434 // source and the strand simply finds no liquidity at book2.
2435 testcase("Locked mid-path holder behind a BookStep");
2436
2437 using namespace jtx;
2438
2439 Account const gw("gw");
2440 Account const alice("alice"); // book1 (XRP/USD) offer owner
2441 Account const mid("mid"); // book2 (USD/EUR) offer owner
2442 Account const sam("sam"); // source
2443 Account const bill("bill"); // destination
2444
2445 auto const test = [&](bool lock) {
2446 Env env(*this, features);
2447 env.fund(XRP(1'000), gw, alice, mid, sam, bill);
2448 env.close();
2449
2450 auto usd = MPTTester(
2451 {.env = env,
2452 .issuer = gw,
2453 .holders = {alice, mid},
2454 .flags = kMptDexFlags | tfMPTCanLock,
2455 .maxAmt = 1'000});
2456 auto const eur = gw["EUR"];
2457
2458 // alice funds book1 (sells USD for XRP); mid funds book2
2459 // (sells EUR for USD, i.e. receives the mid-path USD).
2460 env(pay(gw, alice, usd(100)));
2461 env(trust(mid, eur(100)));
2462 env(pay(gw, mid, eur(100)));
2463 env(trust(bill, eur(100)));
2464 env.close();
2465
2466 env(offer(alice, XRP(100), usd(100))); // XRP/USD, sells USD
2467 env.close();
2468 env(offer(mid, usd(100), eur(100))); // USD/EUR, sells EUR
2469 env.close();
2470 BEAST_EXPECT(expectOffers(env, alice, 1));
2471 BEAST_EXPECT(expectOffers(env, mid, 1));
2472
2473 // Lock mid on USD *after* its offer is already on the book:
2474 // the reviewer's "frozen holder's offer sits behind a
2475 // BookStep" scenario.
2476 if (lock)
2477 {
2478 usd.set({.holder = mid, .flags = tfMPTLock});
2479 env.close();
2480 }
2481
2482 env(pay(sam, bill, eur(100)),
2483 Sendmax(XRP(100)),
2484 Path(~usd, ~eur),
2485 Txflags(tfNoRippleDirect),
2486 // book1 (XRP/USD) still has liquidity, so the strand is
2487 // not fully dry; it just cannot cross book2 once mid's
2488 // offer is removed, hence PARTIAL rather than DRY.
2489 Ter(lock ? TER(tecPATH_PARTIAL) : TER(tesSUCCESS)));
2490 env.close();
2491
2492 if (lock)
2493 {
2494 // No liquidity reached book2: mid neither received USD
2495 // nor delivered EUR, so bill received nothing.
2496 BEAST_EXPECT(env.balance(bill, eur) == eur(0));
2497 BEAST_EXPECT(env.balance(mid, usd) == usd(0));
2498 }
2499 else
2500 {
2501 // The strand crosses both books: mid receives the
2502 // mid-path USD and bill receives EUR.
2503 BEAST_EXPECT(env.balance(bill, eur) == eur(100));
2504 BEAST_EXPECT(env.balance(mid, usd) == usd(100));
2505 BEAST_EXPECT(env.balance(alice, usd) == usd(0));
2506 BEAST_EXPECT(expectOffers(env, alice, 0));
2507 BEAST_EXPECT(expectOffers(env, mid, 0));
2508 }
2509 };
2510
2511 test(false); // baseline: unlocked strand succeeds
2512 test(true); // locked mid-path holder: strand finds no liquidity
2513 }
2514
2515 void
2517 {
2518 using namespace jtx;
2519
2521 testFalseDry(features);
2522 testDirectStep(features);
2523 testBookStep(features);
2524 testOfferOwnerMPTCreation(features);
2525 testTransferRate(features);
2528 testSelfPayment1(features);
2529 testSelfPayment2(features);
2530 testSelfFundedXRPEndpoint(false, features);
2531 testSelfFundedXRPEndpoint(true, features);
2532 testUnfundedOffer(features);
2533 testReExecuteDirectStep(features);
2535 testLockedMidPathHolder(features);
2536 }
2537
2538 void
2539 run() override
2540 {
2541 using namespace jtx;
2542 auto const sa = testableAmendments();
2545 testWithFeats(sa);
2546 testEmptyStrand(sa);
2547 }
2548};
2549
2551
2552} // namespace xrpl::test
A generic endpoint for log messages.
Definition Journal.h:44
A testsuite class.
Definition suite.h:52
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
beast::Journal journal(std::string const &name)
Definition Log.cpp:137
bool modify(ModifyType const &f)
Modify the open ledger.
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
A wrapper which makes credits unavailable to balances.
std::shared_ptr< STLedgerEntry const > const & ConstRef
std::shared_ptr< STLedgerEntry const > const_pointer
bool pushBack(STPath const &e)
pushBack adds a path to the set.
Definition STPathSet.h:669
Discardable, editable view to a ledger.
Definition Sandbox.h:18
void apply(RawView &to)
Definition Sandbox.h:38
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
virtual OpenLedger & getOpenLedger()=0
virtual Logs & getLogs()=0
SLE::pointer peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
TER ter() const
Return the TER for the last JTx.
Definition Env.h:844
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
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
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
Converts to IOU Issue or STAmount.
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:512
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
Set Paths, SendMax on a JTx.
Definition paths.h:23
Sets the SendMax on a JTx.
Definition sendmax.h:16
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
Set the flags on a JTx.
Definition txflags.h:14
T erase(T... args)
T is_same_v
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
json::Value create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
auto const kMptDexFlags
Definition mpt.h:47
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)
void testHelper2TokensMix(TTester &&tester)
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
XRPAmount txFee(Env const &env, std::uint16_t n)
FeatureBitset testableAmendments()
Definition Env.h:92
void testHelper3TokensMix(TTester &&tester)
OwnerCount< ltOFFER > Offers
Match the number of offers in the account's owner directory.
Definition owners.h:137
STPathElement ipe(Asset const &asset)
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 trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
BEAST_DEFINE_TESTSUITE_PRIO(AccountDelete, app, xrpl, 2)
bool isOffer(jtx::Env &env, jtx::Account const &account, STAmount const &takerPays, STAmount const &takerGets)
An offer exists.
Definition PathSet.h:60
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
void forEachItem(ReadView const &view, Keylet const &root, std::function< void(SLE::ConstRef)> const &f)
Iterate all items in the given directory.
TER offerDelete(ApplyView &view, SLE::Ref sle, beast::Journal j)
Delete an offer.
bool isXRP(AccountID const &c)
Definition AccountID.h:84
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
StrandResult< TInAmt, TOutAmt > flow(PaymentSandbox const &baseView, Strand const &strand, std::optional< TInAmt > const &maxIn, TOutAmt const &out, beast::Journal j)
Request out amount from a strand.
Definition StrandFlow.h:103
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
Definition STAmount.cpp:423
@ TapNone
Definition ApplyView.h:28
constexpr std::uint16_t kMaxTransferFee
The maximum token transfer fee allowed.
Definition Protocol.h:97
@ temBAD_PATH
Definition TER.h:84
@ temBAD_PATH_LOOP
Definition TER.h:85
TERSubset< CanCvtToTER > TER
Definition TER.h:654
AccountID const & xrpAccount()
Compute AccountID from public key.
@ tecPATH_PARTIAL
Definition TER.h:290
@ tecPATH_DRY
Definition TER.h:302
@ tecINSUFFICIENT_RESERVE
Definition TER.h:315
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:297
@ tesSUCCESS
Definition TER.h:250
T stoull(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
void run() override
Runs the suite.
void testLockedMidPathHolder(FeatureBitset features)
void testSelfFundedXRPEndpoint(bool consumeOffer, FeatureBitset features)
void testMPTEndpointRipplingInputOverflow(FeatureBitset features)
void testFalseDry(FeatureBitset features)
void testSelfPayment1(FeatureBitset features)
void testWithFeats(FeatureBitset features)
void testTransferRate(FeatureBitset features)
void testBookStep(FeatureBitset features)
void testMPTEndpointTransferRateOverflow(FeatureBitset features)
std::vector< jtx::Account > Accounts
static std::vector< SLE::const_pointer > offersOnAccount(jtx::Env &env, jtx::Account account)
void testOfferOwnerMPTCreation(FeatureBitset features)
void testSelfPayment2(FeatureBitset features)
void testEmptyStrand(FeatureBitset features)
void testDirectStep(FeatureBitset features)
void testMaxAndSelfPaymentEdgeCases(FeatureBitset features)
void testSelfPayLowQualityOffer(FeatureBitset features)
static XRPAmount reserve(jtx::Env &env, std::uint32_t count)
void testReExecuteDirectStep(FeatureBitset features)
void testUnfundedOffer(FeatureBitset features)
Represents an XRP, IOU, or MPT quantity This customizes the string conversion and supports XRP conver...
STAmount const & value() const