xrpld
Loading...
Searching...
No Matches
PermissionedDEX_test.cpp
1#include <test/jtx/AMM.h>
2#include <test/jtx/AMMTest.h>
3#include <test/jtx/Account.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/balance.h>
8#include <test/jtx/credentials.h>
9#include <test/jtx/domain.h>
10#include <test/jtx/fee.h>
11#include <test/jtx/jtx_json.h>
12#include <test/jtx/ledgerStateFix.h>
13#include <test/jtx/offer.h>
14#include <test/jtx/owners.h> // IWYU pragma: keep
15#include <test/jtx/paths.h>
16#include <test/jtx/pay.h>
17#include <test/jtx/permissioned_dex.h>
18#include <test/jtx/permissioned_domains.h>
19#include <test/jtx/sendmax.h>
20#include <test/jtx/ter.h>
21#include <test/jtx/trust.h>
22#include <test/jtx/txflags.h>
23
24#include <xrpl/basics/Slice.h>
25#include <xrpl/basics/base_uint.h>
26#include <xrpl/beast/unit_test/suite.h>
27#include <xrpl/beast/utility/Journal.h>
28#include <xrpl/ledger/OpenView.h>
29#include <xrpl/protocol/Book.h>
30#include <xrpl/protocol/Feature.h>
31#include <xrpl/protocol/Indexes.h>
32#include <xrpl/protocol/Issue.h>
33#include <xrpl/protocol/Keylet.h>
34#include <xrpl/protocol/LedgerFormats.h>
35#include <xrpl/protocol/SField.h>
36#include <xrpl/protocol/STAmount.h>
37#include <xrpl/protocol/STArray.h>
38#include <xrpl/protocol/STLedgerEntry.h>
39#include <xrpl/protocol/SeqProxy.h>
40#include <xrpl/protocol/TER.h>
41#include <xrpl/protocol/TxFlags.h>
42#include <xrpl/protocol/jss.h>
43
44#include <algorithm>
45#include <chrono>
46#include <cstddef>
47#include <cstdint>
48#include <map>
49#include <memory>
50#include <optional>
51#include <string>
52#include <utility>
53#include <vector>
54
55namespace xrpl::test {
56
57using namespace jtx;
58
60{
61 [[nodiscard]] static bool
62 offerExists(Env const& env, Account const& account, std::uint32_t offerSeq)
63 {
64 return static_cast<bool>(
65 env.le(keylet::offer(account.id(), SeqProxy::rawSequence(offerSeq))));
66 }
67
68 [[nodiscard]] static bool
70 Env const& env,
71 Account const& account,
72 std::uint32_t offerSeq,
73 STAmount const& takerPays,
74 STAmount const& takerGets,
75 uint32_t const flags = 0,
76 bool const domainOffer = false)
77 {
78 auto offerInDir = [&](UInt256 const& directory,
79 uint64_t const pageIndex,
80 std::optional<UInt256> domain = std::nullopt) -> bool {
81 auto const page = env.le(keylet::page(directory, pageIndex));
82 if (!page)
83 return false;
84
85 if (domain != (*page)[~sfDomainID])
86 return false;
87
88 auto const& indexes = page->getFieldV256(sfIndexes);
89 return std::ranges::any_of(indexes, [&](auto const& index) {
90 return index == keylet::offer(account, SeqProxy::rawSequence(offerSeq)).key;
91 });
92 };
93
94 auto const sle = env.le(keylet::offer(account.id(), SeqProxy::rawSequence(offerSeq)));
95 if (!sle)
96 return false;
97 if (sle->getFieldAmount(sfTakerGets) != takerGets)
98 return false;
99 if (sle->getFieldAmount(sfTakerPays) != takerPays)
100 return false;
101 if (sle->getFlags() != flags)
102 return false;
103 if (domainOffer && !sle->isFieldPresent(sfDomainID))
104 return false;
105 if (!domainOffer && sle->isFieldPresent(sfDomainID))
106 return false;
107 if (!offerInDir(
108 sle->getFieldH256(sfBookDirectory),
109 sle->getFieldU64(sfBookNode),
110 (*sle)[~sfDomainID]))
111 return false;
112
113 if (sle->isFlag(lsfHybrid))
114 {
115 if (!sle->isFieldPresent(sfDomainID))
116 return false;
117 if (!sle->isFieldPresent(sfAdditionalBooks))
118 return false;
119 if (sle->getFieldArray(sfAdditionalBooks).size() != 1)
120 return false;
121
122 auto const& additionalBookDirs = sle->getFieldArray(sfAdditionalBooks);
123
124 for (auto const& bookDir : additionalBookDirs)
125 {
126 auto const& dirIndex = bookDir.getFieldH256(sfBookDirectory);
127 auto const& dirNode = bookDir.getFieldU64(sfBookNode);
128
129 // the directory is for the open order book, so the dir
130 // doesn't have domainID
131 if (!offerInDir(dirIndex, dirNode, std::nullopt))
132 return false;
133 }
134 }
135 else
136 {
137 if (sle->isFieldPresent(sfAdditionalBooks))
138 return false;
139 }
140
141 return true;
142 }
143
144 static UInt256
145 getBookDirKey(Book const& book, STAmount const& takerPays, STAmount const& takerGets)
146 {
147 return keylet::quality(keylet::book(book), getRate(takerGets, takerPays)).key;
148 }
149
151 getDefaultOfferDirKey(Env const& env, Account const& account, std::uint32_t offerSeq)
152 {
153 if (auto const sle = env.le(keylet::offer(account.id(), SeqProxy::rawSequence(offerSeq))))
154 return Keylet(ltDIR_NODE, (*sle)[sfBookDirectory]).key;
155
156 return {};
157 }
158
159 [[nodiscard]] static bool
161 {
162 std::optional<std::uint64_t> pageIndex{0};
163 std::uint32_t dirCnt = 0;
164
165 do
166 {
167 auto const page = env.le(
168 keylet::page(directory, *pageIndex)); // NOLINT(bugprone-unchecked-optional-access)
169 if (!page)
170 break;
171
172 pageIndex = (*page)[~sfIndexNext];
173 dirCnt += (*page)[sfIndexes].size();
174
175 } while (pageIndex.value_or(0) != 0u);
176
177 return dirCnt == dirSize;
178 }
179
180 void
182 {
183 bool const fixEnabled = features[fixCleanup3_4_0];
184
185 testcase << "OfferCreate"
186 << (fixEnabled ? " (Cleanup3_4_0 enabled)" : " (Cleanup3_4_0 disabled)");
187
188 // test preflight
189 {
190 Env env(*this, features - featurePermissionedDEX);
191 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
192 PermissionedDEX(env);
193
194 env(offer(bob, XRP(10), USD(10)), Domain(domainID), Ter(temDISABLED));
195 env.close();
196
197 env.enableFeature(featurePermissionedDEX);
198 env.close();
199 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
200 env.close();
201 }
202
203 // test preflight - malformed DomainID being zero
204 // Only test this with fixCleanup3_2_0 enabled. Without the fix,
205 // an assert-enabled build can crash when Ledger::read() receives
206 // a zero-key PermissionedDomain keylet.
207 if (features[fixCleanup3_2_0])
208 {
209 Env env(*this, features);
210 auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] =
211 PermissionedDEX(env);
212
213 env(offer(bob_, XRP(10), USD(10)), Domain(UInt256{}), Ter(temMALFORMED));
214 env.close();
215 }
216
217 // preclaim - someone outside of the domain cannot create domain offer
218 {
219 Env env(*this, features);
220 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
221 PermissionedDEX(env);
222
223 // create devin account who is not part of the domain
224 Account const devin("devin");
225 env.fund(XRP(1000), devin);
226 env.close();
227 env.trust(USD(1000), devin);
228 env.close();
229 env(pay(gw, devin, USD(100)));
230 env.close();
231
232 env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(tecNO_PERMISSION));
233 env.close();
234
235 // domain owner also issues a credential for devin
236 env(credentials::create(devin, domainOwner, credType));
237 env.close();
238
239 // devin still cannot create offer since he didn't accept credential
240 env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(tecNO_PERMISSION));
241 env.close();
242
243 env(credentials::accept(devin, domainOwner, credType));
244 env.close();
245
246 env(offer(devin, XRP(10), USD(10)), Domain(domainID));
247 env.close();
248 }
249
250 // preclaim - someone with expired cred cannot create domain offer
251 {
252 Env env(*this, features);
253 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
254 PermissionedDEX(env);
255
256 // create devin account who is not part of the domain
257 Account const devin("devin");
258 env.fund(XRP(1000), devin);
259 env.close();
260 env.trust(USD(1000), devin);
261 env.close();
262 env(pay(gw, devin, USD(100)));
263 env.close();
264
265 auto jv = credentials::create(devin, domainOwner, credType);
266 uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count();
267 jv[sfExpiration.jsonName] = t + 20;
268 env(jv);
269
270 env(credentials::accept(devin, domainOwner, credType));
271 env.close();
272
273 // devin can still create offer while his cred is not expired
274 env(offer(devin, XRP(10), USD(10)), Domain(domainID));
275 env.close();
276
277 // time advance
279
280 // Devin cannot create offer with expired cred. After fixCleanup3_4_0,
281 // doApply deletes the expired credential SLE and returns tecEXPIRED.
282 TER const expectedExpiredCredTer = fixEnabled ? tecEXPIRED : tecNO_PERMISSION;
283 env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(expectedExpiredCredTer));
284 env.close();
285 }
286
287 // preclaim - cannot create an offer in a non existent domain
288 {
289 Env env(*this, features);
290 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
291 PermissionedDEX(env);
292 UInt256 const badDomain{
293 "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134"
294 "E5"};
295
296 env(offer(bob, XRP(10), USD(10)), Domain(badDomain), Ter(tecNO_PERMISSION));
297 env.close();
298 }
299
300 // apply - offer can be created even if takergets issuer is not in
301 // domain
302 {
303 Env env(*this, features);
304 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
305 PermissionedDEX(env);
306
307 env(credentials::deleteCred(domainOwner, gw, domainOwner, credType));
308 env.close();
309
310 auto const bobOfferSeq{env.seq(bob)};
311 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
312 env.close();
313
314 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
315 }
316
317 // apply - offer can be created even if takerpays issuer is not in
318 // domain
319 {
320 Env env(*this, features);
321 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
322 PermissionedDEX(env);
323
324 env(credentials::deleteCred(domainOwner, gw, domainOwner, credType));
325 env.close();
326
327 auto const bobOfferSeq{env.seq(bob)};
328 env(offer(bob, USD(10), XRP(10)), Domain(domainID));
329 env.close();
330
331 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, USD(10), XRP(10), 0, true));
332 }
333
334 // apply - two domain offers cross with each other
335 {
336 Env env(*this, features);
337 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
338 PermissionedDEX(env);
339
340 auto const bobOfferSeq{env.seq(bob)};
341 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
342 env.close();
343
344 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
345 BEAST_EXPECT(ownerCount(env, bob) == 3);
346
347 // a non domain offer cannot cross with domain offer
348 env(offer(carol, USD(10), XRP(10)));
349 env.close();
350
351 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
352
353 auto const aliceOfferSeq{env.seq(alice)};
354 env(offer(alice, USD(10), XRP(10)), Domain(domainID));
355 env.close();
356
357 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
358 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
359 BEAST_EXPECT(ownerCount(env, alice) == 2);
360 }
361
362 // apply - create lots of domain offers
363 {
364 Env env(*this, features);
365 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
366 PermissionedDEX(env);
367
369 offerSeqs.reserve(100);
370
371 for (size_t i = 0; i <= 100; i++)
372 {
373 auto const bobOfferSeq{env.seq(bob)};
374 offerSeqs.emplace_back(bobOfferSeq);
375
376 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
377 env.close();
378 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
379 }
380
381 for (auto const offerSeq : offerSeqs)
382 {
383 env(offerCancel(bob, offerSeq));
384 env.close();
385 BEAST_EXPECT(!offerExists(env, bob, offerSeq));
386 }
387 }
388 }
389
390 void
392 {
393 testcase("Payment");
394
395 // test preflight - without enabling featurePermissionedDEX amendment
396 {
397 Env env(*this, features - featurePermissionedDEX);
398 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
399 PermissionedDEX(env);
400
401 env(pay(bob, alice, USD(10)),
402 Path(~USD),
403 Sendmax(XRP(10)),
404 Domain(domainID),
406 env.close();
407
408 env.enableFeature(featurePermissionedDEX);
409 env.close();
410
411 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
412 env.close();
413
414 env(pay(bob, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
415 env.close();
416 }
417
418 // test preflight - malformed DomainID being zero
419 // Only test this with fixCleanup3_2_0 enabled. Without the fix,
420 // an assert-enabled build can crash when Ledger::read() receives
421 // a zero-key PermissionedDomain keylet.
422 if (features[fixCleanup3_2_0])
423 {
424 Env env(*this, features);
425 auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] =
426 PermissionedDEX(env);
427
428 env(pay(bob_, alice_, USD(10)),
429 Path(~USD),
430 Sendmax(XRP(10)),
431 Domain(UInt256{}),
433 env.close();
434 }
435
436 // preclaim - cannot send payment with non existent domain
437 {
438 Env env(*this, features);
439 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
440 PermissionedDEX(env);
441 UInt256 const badDomain{
442 "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134"
443 "E5"};
444
445 env(pay(bob, alice, USD(10)),
446 Path(~USD),
447 Sendmax(XRP(10)),
448 Domain(badDomain),
450 env.close();
451 }
452
453 // preclaim - payment with non-domain destination fails
454 {
455 Env env(*this, features);
456 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
457 PermissionedDEX(env);
458
459 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
460 env.close();
461
462 // create devin account who is not part of the domain
463 Account const devin("devin");
464 env.fund(XRP(1000), devin);
465 env.close();
466 env.trust(USD(1000), devin);
467 env.close();
468 env(pay(gw, devin, USD(100)));
469 env.close();
470
471 // devin is not part of domain
472 env(pay(alice, devin, USD(10)),
473 Path(~USD),
474 Sendmax(XRP(10)),
475 Domain(domainID),
477 env.close();
478
479 // domain owner also issues a credential for devin
480 env(credentials::create(devin, domainOwner, credType));
481 env.close();
482
483 // devin has not yet accepted cred
484 env(pay(alice, devin, USD(10)),
485 Path(~USD),
486 Sendmax(XRP(10)),
487 Domain(domainID),
489 env.close();
490
491 env(credentials::accept(devin, domainOwner, credType));
492 env.close();
493
494 // devin can now receive payment after he is in domain
495 env(pay(alice, devin, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
496 env.close();
497 }
498
499 // preclaim - non-domain sender cannot send payment
500 {
501 Env env(*this, features);
502 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
503 PermissionedDEX(env);
504
505 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
506 env.close();
507
508 // create devin account who is not part of the domain
509 Account const devin("devin");
510 env.fund(XRP(1000), devin);
511 env.close();
512 env.trust(USD(1000), devin);
513 env.close();
514 env(pay(gw, devin, USD(100)));
515 env.close();
516
517 // devin tries to send domain payment
518 env(pay(devin, alice, USD(10)),
519 Path(~USD),
520 Sendmax(XRP(10)),
521 Domain(domainID),
523 env.close();
524
525 // domain owner also issues a credential for devin
526 env(credentials::create(devin, domainOwner, credType));
527 env.close();
528
529 // devin has not yet accepted cred
530 env(pay(devin, alice, USD(10)),
531 Path(~USD),
532 Sendmax(XRP(10)),
533 Domain(domainID),
535 env.close();
536
537 env(credentials::accept(devin, domainOwner, credType));
538 env.close();
539
540 // devin can now send payment after he is in domain
541 env(pay(devin, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
542 env.close();
543 }
544
545 // apply - domain owner can always send and receive domain payment
546 {
547 Env env(*this, features);
548 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
549 PermissionedDEX(env);
550
551 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
552 env.close();
553
554 // domain owner can always be destination
555 env(pay(alice, domainOwner, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
556 env.close();
557
558 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
559 env.close();
560
561 // domain owner can send
562 env(pay(domainOwner, alice, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
563 env.close();
564 }
565 }
566
567 void
569 {
570 testcase("Book step");
571
572 // test domain cross currency payment consuming one offer
573 {
574 Env env(*this, features);
575 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
576 PermissionedDEX(env);
577
578 // create a regular offer without domain
579 auto const regularOfferSeq{env.seq(bob)};
580 env(offer(bob, XRP(10), USD(10)));
581 env.close();
582 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
583
584 auto const regularDirKey = getDefaultOfferDirKey(env, bob, regularOfferSeq);
585 BEAST_EXPECT(regularDirKey);
586 BEAST_EXPECT(checkDirectorySize(
587 env, *regularDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access)
588
589 // a domain payment cannot consume regular offers
590 env(pay(alice, carol, USD(10)),
591 Path(~USD),
592 Sendmax(XRP(10)),
593 Domain(domainID),
595 env.close();
596
597 // create a domain offer
598 auto const domainOfferSeq{env.seq(bob)};
599 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
600 env.close();
601
602 BEAST_EXPECT(checkOffer(env, bob, domainOfferSeq, XRP(10), USD(10), 0, true));
603
604 auto const domainDirKey = getDefaultOfferDirKey(env, bob, domainOfferSeq);
605 BEAST_EXPECT(domainDirKey);
606 BEAST_EXPECT(checkDirectorySize(
607 env, *domainDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access)
608
609 // cross-currency permissioned payment consumed
610 // domain offer instead of regular offer
611 env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
612 env.close();
613 BEAST_EXPECT(!offerExists(env, bob, domainOfferSeq));
614 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
615
616 // domain directory is empty
617 BEAST_EXPECT(checkDirectorySize(
618 env, *domainDirKey, 0)); // NOLINT(bugprone-unchecked-optional-access)
619 BEAST_EXPECT(checkDirectorySize(
620 env, *regularDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access)
621 }
622
623 // test domain payment consuming two offers in the path
624 {
625 Env env(*this, features);
626 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
627 PermissionedDEX(env);
628
629 auto const eur = gw["EUR"];
630 env.trust(eur(1000), alice);
631 env.close();
632 env.trust(eur(1000), bob);
633 env.close();
634 env.trust(eur(1000), carol);
635 env.close();
636 env(pay(gw, bob, eur(100)));
637 env.close();
638
639 // create XRP/USD domain offer
640 auto const usdOfferSeq{env.seq(bob)};
641 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
642 env.close();
643
644 BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
645
646 // payment fail because there isn't eur offer
647 env(pay(alice, carol, eur(10)),
648 Path(~USD, ~eur),
649 Sendmax(XRP(10)),
650 Domain(domainID),
652 env.close();
653 BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
654
655 // bob creates a regular USD/EUR offer
656 auto const regularOfferSeq{env.seq(bob)};
657 env(offer(bob, USD(10), eur(10)));
658 env.close();
659 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, USD(10), eur(10)));
660
661 // alice tries to pay again, but still fails because the regular
662 // offer cannot be consumed
663 env(pay(alice, carol, eur(10)),
664 Path(~USD, ~eur),
665 Sendmax(XRP(10)),
666 Domain(domainID),
668 env.close();
669
670 // bob creates a domain USD/EUR offer
671 auto const eurOfferSeq{env.seq(bob)};
672 env(offer(bob, USD(10), eur(10)), Domain(domainID));
673 env.close();
674 BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), 0, true));
675
676 // alice successfully consume two domain offers: xrp/usd and usd/eur
677 env(pay(alice, carol, eur(5)), Sendmax(XRP(5)), Domain(domainID), Path(~USD, ~eur));
678 env.close();
679
680 BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true));
681 BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), 0, true));
682
683 // alice successfully consume two domain offers and deletes them
684 // we compute path this time using `paths`
685 env(pay(alice, carol, eur(5)), Sendmax(XRP(5)), Domain(domainID), Paths(XRP));
686 env.close();
687
688 BEAST_EXPECT(!offerExists(env, bob, usdOfferSeq));
689 BEAST_EXPECT(!offerExists(env, bob, eurOfferSeq));
690
691 // regular offer is not consumed
692 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, USD(10), eur(10)));
693 }
694
695 // domain payment cannot consume offer from another domain
696 {
697 Env env(*this, features);
698 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
699 PermissionedDEX(env);
700
701 // Fund devin and create USD trustline
702 Account const badDomainOwner("badDomainOwner");
703 Account const devin("devin");
704 env.fund(XRP(1000), badDomainOwner, devin);
705 env.close();
706 env.trust(USD(1000), devin);
707 env.close();
708 env(pay(gw, devin, USD(100)));
709 env.close();
710
711 auto const badCredType = "badCred";
713 {.issuer = badDomainOwner, .credType = badCredType}};
714 env(pdomain::setTx(badDomainOwner, credentials));
715
716 auto objects = pdomain::getObjects(badDomainOwner, env);
717 auto const badDomainID = objects.begin()->first;
718
719 env(credentials::create(devin, badDomainOwner, badCredType));
720 env.close();
721 env(credentials::accept(devin, badDomainOwner, badCredType));
722
723 // devin creates a domain offer in another domain
724 env(offer(devin, XRP(10), USD(10)), Domain(badDomainID));
725 env.close();
726
727 // domain payment can't consume an offer from another domain
728 env(pay(alice, carol, USD(10)),
729 Path(~USD),
730 Sendmax(XRP(10)),
731 Domain(domainID),
733 env.close();
734
735 // bob creates an offer under the right domain
736 auto const bobOfferSeq{env.seq(bob)};
737 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
738 env.close();
739 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
740
741 // domain payment now consumes from the right domain
742 env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
743 env.close();
744
745 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
746 }
747
748 // sanity check: devin, who is part of the domain but doesn't have a
749 // trustline with USD issuer, can successfully make a payment using
750 // offer
751 {
752 Env env(*this, features);
753 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
754 PermissionedDEX(env);
755
756 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
757 env.close();
758
759 // fund devin but don't create a USD trustline with gateway
760 Account const devin("devin");
761 env.fund(XRP(1000), devin);
762 env.close();
763
764 // domain owner also issues a credential for devin
765 env(credentials::create(devin, domainOwner, credType));
766 env.close();
767
768 env(credentials::accept(devin, domainOwner, credType));
769 env.close();
770
771 // successful payment because offer is consumed
772 env(pay(devin, alice, USD(10)), Sendmax(XRP(10)), Domain(domainID));
773 env.close();
774 }
775
776 // offer becomes unfunded when offer owner's cred expires
777 {
778 Env env(*this, features);
779 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
780 PermissionedDEX(env);
781
782 // create devin account who is not part of the domain
783 Account const devin("devin");
784 env.fund(XRP(1000), devin);
785 env.close();
786 env.trust(USD(1000), devin);
787 env.close();
788 env(pay(gw, devin, USD(100)));
789 env.close();
790
791 auto jv = credentials::create(devin, domainOwner, credType);
792 uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count();
793 jv[sfExpiration.jsonName] = t + 20;
794 env(jv);
795
796 env(credentials::accept(devin, domainOwner, credType));
797 env.close();
798
799 // devin can still create offer while his cred is not expired
800 auto const offerSeq{env.seq(devin)};
801 env(offer(devin, XRP(10), USD(10)), Domain(domainID));
802 env.close();
803
804 // devin's offer can still be consumed while his cred isn't expired
805 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID));
806 env.close();
807 BEAST_EXPECT(checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true));
808
809 // advance time
811
812 // devin's offer is unfunded now due to expired cred
813 env(pay(alice, carol, USD(5)),
814 Path(~USD),
815 Sendmax(XRP(5)),
816 Domain(domainID),
818 env.close();
819 BEAST_EXPECT(checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true));
820 }
821
822 // offer becomes unfunded when offer owner's cred is removed
823 {
824 Env env(*this, features);
825 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
826 PermissionedDEX(env);
827
828 auto const offerSeq{env.seq(bob)};
829 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
830 env.close();
831
832 // bob's offer can still be consumed while his cred exists
833 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID));
834 env.close();
835 BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true));
836
837 // remove bob's cred
838 env(credentials::deleteCred(domainOwner, bob, domainOwner, credType));
839 env.close();
840
841 // bob's offer is unfunded now due to expired cred
842 env(pay(alice, carol, USD(5)),
843 Path(~USD),
844 Sendmax(XRP(5)),
845 Domain(domainID),
847 env.close();
848 BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true));
849 }
850 }
851
852 void
854 {
855 testcase("Rippling");
856
857 // test a non-domain account can still be part of rippling in a domain
858 // payment. If the domain wishes to control who is allowed to ripple
859 // through, they should set the rippling individually
860 Env env(*this, features);
861 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
862 PermissionedDEX(env);
863
864 auto const eura = alice["EUR"];
865 auto const eurb = bob["EUR"];
866
867 env.trust(eura(100), bob);
868 env.trust(eurb(100), carol);
869 env.close();
870
871 // remove bob from domain
872 env(credentials::deleteCred(domainOwner, bob, domainOwner, credType));
873 env.close();
874
875 // alice can still ripple through bob even though he's not part
876 // of the domain, this is intentional
877 env(pay(alice, carol, eurb(10)), Paths(eura), Domain(domainID));
878 env.close();
879 env.require(Balance(bob, eura(10)), Balance(carol, eurb(10)));
880
881 // carol sets no ripple on bob
882 env(trust(carol, bob["EUR"](0), bob, tfSetNoRipple));
883 env.close();
884
885 // payment no longer works because carol has no ripple on bob
886 env(pay(alice, carol, eurb(5)), Paths(eura), Domain(domainID), Ter(tecPATH_DRY));
887 env.close();
888 env.require(Balance(bob, eura(10)), Balance(carol, eurb(10)));
889 }
890
891 void
893 {
894 testcase("Offer token issuer in domain");
895
896 // whether the issuer is in the domain should NOT affect whether an
897 // offer can be consumed in domain payment
898 Env env(*this, features);
899 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
900 PermissionedDEX(env);
901
902 // create an xrp/usd offer with usd as takergets
903 auto const bobOffer1Seq{env.seq(bob)};
904 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
905 env.close();
906
907 // create an usd/xrp offer with usd as takerpays
908 auto const bobOffer2Seq{env.seq(bob)};
909 env(offer(bob, USD(10), XRP(10)), Domain(domainID), Txflags(tfPassive));
910 env.close();
911
912 BEAST_EXPECT(checkOffer(env, bob, bobOffer1Seq, XRP(10), USD(10), 0, true));
913 BEAST_EXPECT(checkOffer(env, bob, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true));
914
915 // remove gateway from domain
916 env(credentials::deleteCred(domainOwner, gw, domainOwner, credType));
917 env.close();
918
919 // payment succeeds even if issuer is not in domain
920 // xrp/usd offer is consumed
921 env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
922 env.close();
923 BEAST_EXPECT(!offerExists(env, bob, bobOffer1Seq));
924
925 // payment succeeds even if issuer is not in domain
926 // usd/xrp offer is consumed
927 env(pay(alice, carol, XRP(10)), Path(~XRP), Sendmax(USD(10)), Domain(domainID));
928 env.close();
929 BEAST_EXPECT(!offerExists(env, bob, bobOffer2Seq));
930 }
931
932 void
934 {
935 testcase("Remove unfunded offer");
936
937 // checking that an unfunded offer will be implicitly removed by a
938 // successful payment tx
939 Env env(*this, features);
940 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
941 PermissionedDEX(env);
942
943 auto const aliceOfferSeq{env.seq(alice)};
944 env(offer(alice, XRP(100), USD(100)), Domain(domainID));
945 env.close();
946
947 auto const bobOfferSeq{env.seq(bob)};
948 env(offer(bob, XRP(20), USD(20)), Domain(domainID));
949 env.close();
950
951 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(20), USD(20), 0, true));
952 BEAST_EXPECT(checkOffer(env, alice, aliceOfferSeq, XRP(100), USD(100), 0, true));
953
954 auto const domainDirKey = getDefaultOfferDirKey(env, bob, bobOfferSeq);
955 BEAST_EXPECT(domainDirKey);
956 BEAST_EXPECT(checkDirectorySize(
957 env, *domainDirKey, 2)); // NOLINT(bugprone-unchecked-optional-access)
958
959 // remove alice from domain and thus alice's offer becomes unfunded
960 env(credentials::deleteCred(domainOwner, alice, domainOwner, credType));
961 env.close();
962
963 env(pay(gw, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
964 env.close();
965
966 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
967
968 // alice's unfunded offer is removed implicitly
969 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
970 BEAST_EXPECT(checkDirectorySize(
971 env, *domainDirKey, 1)); // NOLINT(bugprone-unchecked-optional-access)
972 }
973
974 void
976 {
977 testcase("AMM not used");
978
979 Env env(*this, features);
980 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
981 PermissionedDEX(env);
982 AMM const amm(env, alice, XRP(10), USD(50));
983
984 // a domain payment isn't able to consume AMM
985 env(pay(bob, carol, USD(5)),
986 Path(~USD),
987 Sendmax(XRP(5)),
988 Domain(domainID),
990 env.close();
991
992 // a non domain payment can use AMM
993 env(pay(bob, carol, USD(5)), Path(~USD), Sendmax(XRP(5)));
994 env.close();
995
996 // USD amount in AMM is changed
997 auto [xrp, usd, lpt] = amm.balances(XRP, USD);
998 BEAST_EXPECT(usd == USD(45));
999 }
1000
1001 void
1003 {
1004 bool const excludesAmmFromDomainQuality = features[fixCleanup3_3_0];
1005
1006 testcase << "AMM quality not leaked into domain BookStep"
1007 << (excludesAmmFromDomainQuality ? " (Cleanup3_3_0 enabled)"
1008 : " (Cleanup3_3_0 disabled)");
1009
1010 Env env(*this, features);
1011 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1012 PermissionedDEX(env);
1013 auto const eur = gw["EUR"];
1014
1015 env.trust(eur(1000), bob, domainOwner);
1016 env.close();
1017 env(pay(gw, bob, eur(100)));
1018 env.close();
1019
1020 env(pay(gw, alice, USD(500)));
1021 env.close();
1022
1023 // The AMM makes the direct XRP->USD book look much better than it
1024 // really is for domain payments. The domain LOB direct path is 1:1,
1025 // while the competing XRP->EUR->USD path is 2:1.
1026 AMM const amm(env, alice, XRP(10), USD(500));
1027
1028 auto const directOfferSeq{env.seq(bob)};
1029 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
1030 env.close();
1031
1032 auto const xrpEurOfferSeq{env.seq(bob)};
1033 env(offer(bob, XRP(10), eur(20)), Domain(domainID));
1034 env.close();
1035
1036 auto const eurUsdOfferSeq{env.seq(domainOwner)};
1037 env(offer(domainOwner, eur(20), USD(20)), Domain(domainID));
1038 env.close();
1039
1040 auto const carolBalBefore = env.balance(carol, USD);
1041
1042 // Both paths compete for the same XRP(10) sendmax. If AMM quality leaks
1043 // into the direct domain book, the engine ranks direct XRP->USD first
1044 // but crossing can only consume the 1:1 LOB offer. With the fix, the
1045 // direct book is ranked by its domain LOB quality, so the 2:1
1046 // XRP->EUR->USD path executes first.
1047 env(pay(alice, carol, USD(100)),
1048 Path(~USD),
1049 Path(~eur, ~USD),
1050 Sendmax(XRP(10)),
1051 Txflags(tfPartialPayment | tfNoRippleDirect),
1052 Domain(domainID));
1053 env.close();
1054
1055 auto const delivered = env.balance(carol, USD) - carolBalBefore;
1056 if (excludesAmmFromDomainQuality)
1057 {
1058 BEAST_EXPECT(delivered == USD(20));
1059
1060 BEAST_EXPECT(checkOffer(env, bob, directOfferSeq, XRP(10), USD(10), 0, true));
1061 BEAST_EXPECT(!offerExists(env, bob, xrpEurOfferSeq));
1062 BEAST_EXPECT(!offerExists(env, domainOwner, eurUsdOfferSeq));
1063 }
1064 else
1065 {
1066 BEAST_EXPECT(delivered == USD(10));
1067
1068 BEAST_EXPECT(!offerExists(env, bob, directOfferSeq));
1069 BEAST_EXPECT(checkOffer(env, bob, xrpEurOfferSeq, XRP(10), eur(20), 0, true));
1070 BEAST_EXPECT(checkOffer(env, domainOwner, eurUsdOfferSeq, eur(20), USD(20), 0, true));
1071 }
1072
1073 auto [xrp, usd, lpt] = amm.balances(XRP, USD);
1074 BEAST_EXPECT(xrp == XRP(10));
1075 BEAST_EXPECT(usd == USD(500));
1076 }
1077
1078 void
1080 {
1081 testcase("Hybrid offer create");
1082
1083 // test preflight - invalid hybrid flag
1084 {
1085 Env env(*this, features - featurePermissionedDEX);
1086 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1087 PermissionedDEX(env);
1088
1089 env(offer(bob, XRP(10), USD(10)),
1090 Domain(domainID),
1091 Txflags(tfHybrid),
1092 Ter(temDISABLED));
1093 env.close();
1094
1095 env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG));
1096 env.close();
1097
1098 env.enableFeature(featurePermissionedDEX);
1099 env.close();
1100
1101 // hybrid offer must have domainID
1102 env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Ter(temINVALID_FLAG));
1103 env.close();
1104
1105 // hybrid offer must have domainID
1106 auto const offerSeq{env.seq(bob)};
1107 env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID));
1108 env.close();
1109 BEAST_EXPECT(checkOffer(env, bob, offerSeq, XRP(10), USD(10), lsfHybrid, true));
1110 }
1111
1112 // apply - domain offer can cross with hybrid
1113 {
1114 Env env(*this, features);
1115 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1116 PermissionedDEX(env);
1117
1118 auto const bobOfferSeq{env.seq(bob)};
1119 env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID));
1120 env.close();
1121
1122 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1123 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1124 BEAST_EXPECT(ownerCount(env, bob) == 3);
1125
1126 auto const aliceOfferSeq{env.seq(alice)};
1127 env(offer(alice, USD(10), XRP(10)), Domain(domainID));
1128 env.close();
1129
1130 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1131 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1132 BEAST_EXPECT(ownerCount(env, alice) == 2);
1133 }
1134
1135 // apply - open offer can cross with hybrid
1136 {
1137 Env env(*this, features);
1138 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1139 PermissionedDEX(env);
1140
1141 auto const bobOfferSeq{env.seq(bob)};
1142 env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID));
1143 env.close();
1144
1145 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1146 BEAST_EXPECT(ownerCount(env, bob) == 3);
1147 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1148
1149 auto const aliceOfferSeq{env.seq(alice)};
1150 env(offer(alice, USD(10), XRP(10)));
1151 env.close();
1152
1153 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1154 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1155 BEAST_EXPECT(ownerCount(env, alice) == 2);
1156 }
1157
1158 // apply - by default, hybrid offer tries to cross with offers in the
1159 // domain book
1160 {
1161 Env env(*this, features);
1162 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1163 PermissionedDEX(env);
1164
1165 auto const bobOfferSeq{env.seq(bob)};
1166 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
1167 env.close();
1168
1169 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
1170 BEAST_EXPECT(ownerCount(env, bob) == 3);
1171
1172 // hybrid offer auto crosses with domain offer
1173 auto const aliceOfferSeq{env.seq(alice)};
1174 env(offer(alice, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid));
1175 env.close();
1176
1177 BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq));
1178 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1179 BEAST_EXPECT(ownerCount(env, alice) == 2);
1180 }
1181
1182 // apply - hybrid offer does not automatically cross with open offers
1183 // because by default, it only tries to cross domain offers
1184 {
1185 Env env(*this, features);
1186 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1187 PermissionedDEX(env);
1188
1189 auto const bobOfferSeq{env.seq(bob)};
1190 env(offer(bob, XRP(10), USD(10)));
1191 env.close();
1192
1193 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false));
1194 BEAST_EXPECT(ownerCount(env, bob) == 3);
1195
1196 // hybrid offer auto crosses with domain offer
1197 auto const aliceOfferSeq{env.seq(alice)};
1198 env(offer(alice, USD(10), XRP(10)), Domain(domainID), Txflags(tfHybrid));
1199 env.close();
1200
1201 BEAST_EXPECT(offerExists(env, alice, aliceOfferSeq));
1202 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1203 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false));
1204 BEAST_EXPECT(checkOffer(env, alice, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true));
1205 BEAST_EXPECT(ownerCount(env, alice) == 3);
1206 }
1207 }
1208
1209 void
1211 {
1212 testcase("Hybrid invalid offer");
1213
1214 // bob has a hybrid offer and then he is removed from the domain.
1215 // Domain payments must not consume the offer; regular open-book
1216 // payments follow the fixCleanup3_3_0 behavior checked below.
1217 Env env(*this, features);
1218 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1219 PermissionedDEX(env);
1220
1221 auto const hybridOfferSeq{env.seq(bob)};
1222 env(offer(bob, XRP(50), USD(50)), Txflags(tfHybrid), Domain(domainID));
1223 env.close();
1224
1225 // remove bob from domain
1226 env(credentials::deleteCred(domainOwner, bob, domainOwner, credType));
1227 env.close();
1228
1229 // bob's hybrid offer is unfunded and can not be consumed in a domain
1230 // payment
1231 env(pay(alice, carol, USD(5)),
1232 Path(~USD),
1233 Sendmax(XRP(5)),
1234 Domain(domainID),
1236 env.close();
1237 BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true));
1238
1239 if (features[fixCleanup3_3_0])
1240 {
1241 // Post-fixCleanup3_3_0: hybrid offer can still be consumed via a regular
1242 // open-book payment even though the domain credential was revoked.
1243 auto const carolBalBefore = env.balance(carol, USD);
1244 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)));
1245 env.close();
1246 BEAST_EXPECT(env.balance(carol, USD) - carolBalBefore == USD(5));
1247 BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(45), USD(45), lsfHybrid, true));
1248
1249 // create a regular offer alongside the hybrid one
1250 auto const regularOfferSeq{env.seq(bob)};
1251 env(offer(bob, XRP(10), USD(10)));
1252 env.close();
1253 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
1254
1255 auto const sleHybridOffer =
1256 env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(hybridOfferSeq)));
1257 if (!BEAST_EXPECT(sleHybridOffer))
1258 return;
1259 auto const openDir =
1260 sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory);
1261 // both offers are in the open book directory
1262 BEAST_EXPECT(checkDirectorySize(env, openDir, 2));
1263
1264 // A regular payment crosses the hybrid offer first (FIFO, older
1265 // offer), then stops; the regular offer is untouched.
1266 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)));
1267 env.close();
1268
1269 BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(40), USD(40), lsfHybrid, true));
1270 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
1271 BEAST_EXPECT(checkDirectorySize(env, openDir, 2));
1272 }
1273 else
1274 {
1275 // Pre-fixCleanup3_3_0: the open-book traversal
1276 // also runs the offerInDomain eviction check, so the hybrid offer
1277 // is treated as unfunded and the regular payment fails.
1278 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Ter(tecPATH_PARTIAL));
1279 env.close();
1280 BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true));
1281
1282 // create a regular offer
1283 auto const regularOfferSeq{env.seq(bob)};
1284 env(offer(bob, XRP(10), USD(10)));
1285 env.close();
1286 BEAST_EXPECT(offerExists(env, bob, regularOfferSeq));
1287 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10)));
1288
1289 auto const sleHybridOffer =
1290 env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(hybridOfferSeq)));
1291 if (!BEAST_EXPECT(sleHybridOffer))
1292 return;
1293 auto const openDir =
1294 sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory);
1295 BEAST_EXPECT(checkDirectorySize(env, openDir, 2));
1296
1297 // This payment crosses the regular offer and permanently evicts the
1298 // hybrid offer from the open book (since the payment succeeds, the
1299 // sandbox, including the hybrid eviction, is committed).
1300 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)));
1301 env.close();
1302
1303 BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq));
1304 BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(5), USD(5)));
1305 BEAST_EXPECT(checkDirectorySize(env, openDir, 1));
1306 }
1307 }
1308
1309 void
1311 {
1312 testcase("Hybrid book step");
1313
1314 // both non domain and domain payments can consume hybrid offer
1315 {
1316 Env env(*this, features);
1317 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1318 PermissionedDEX(env);
1319
1320 auto const hybridOfferSeq{env.seq(bob)};
1321 env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID));
1322 env.close();
1323
1324 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID));
1325 env.close();
1326 BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true));
1327
1328 // hybrid offer can't be consumed since bob is not in domain anymore
1329 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)));
1330 env.close();
1331
1332 BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq));
1333 }
1334
1335 // someone from another domain can't cross hybrid if they specified
1336 // wrong domainID
1337 {
1338 Env env(*this, features);
1339 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1340 PermissionedDEX(env);
1341
1342 // Fund accounts
1343 Account const badDomainOwner("badDomainOwner");
1344 Account const devin("devin");
1345 env.fund(XRP(1000), badDomainOwner, devin);
1346 env.close();
1347
1348 auto const badCredType = "badCred";
1350 {.issuer = badDomainOwner, .credType = badCredType}};
1351 env(pdomain::setTx(badDomainOwner, credentials));
1352
1353 auto objects = pdomain::getObjects(badDomainOwner, env);
1354 auto const badDomainID = objects.begin()->first;
1355
1356 env(credentials::create(devin, badDomainOwner, badCredType));
1357 env.close();
1358 env(credentials::accept(devin, badDomainOwner, badCredType));
1359 env.close();
1360
1361 auto const hybridOfferSeq{env.seq(bob)};
1362 env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID));
1363 env.close();
1364
1365 // other domains can't consume the offer
1366 env(pay(devin, badDomainOwner, USD(5)),
1367 Path(~USD),
1368 Sendmax(XRP(5)),
1369 Domain(badDomainID),
1370 Ter(tecPATH_DRY));
1371 env.close();
1372 BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1373
1374 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)), Domain(domainID));
1375 env.close();
1376 BEAST_EXPECT(checkOffer(env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true));
1377
1378 // hybrid offer can't be consumed since bob is not in domain anymore
1379 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)));
1380 env.close();
1381
1382 BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq));
1383 }
1384
1385 // test domain payment consuming two offers w/ hybrid offer
1386 {
1387 Env env(*this, features);
1388 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1389 PermissionedDEX(env);
1390
1391 auto const eur = gw["EUR"];
1392 env.trust(eur(1000), alice);
1393 env.close();
1394 env.trust(eur(1000), bob);
1395 env.close();
1396 env.trust(eur(1000), carol);
1397 env.close();
1398 env(pay(gw, bob, eur(100)));
1399 env.close();
1400
1401 auto const usdOfferSeq{env.seq(bob)};
1402 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
1403 env.close();
1404
1405 BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
1406
1407 // payment fail because there isn't eur offer
1408 env(pay(alice, carol, eur(5)),
1409 Path(~USD, ~eur),
1410 Sendmax(XRP(5)),
1411 Domain(domainID),
1413 env.close();
1414 BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true));
1415
1416 // bob creates a hybrid eur offer
1417 auto const eurOfferSeq{env.seq(bob)};
1418 env(offer(bob, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid));
1419 env.close();
1420 BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), lsfHybrid, true));
1421
1422 // alice successfully consume two domain offers: xrp/usd and usd/eur
1423 env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)), Domain(domainID));
1424 env.close();
1425
1426 BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true));
1427 BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), lsfHybrid, true));
1428 }
1429
1430 // test regular payment using a regular offer and a hybrid offer
1431 {
1432 Env env(*this, features);
1433 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1434 PermissionedDEX(env);
1435
1436 auto const eur = gw["EUR"];
1437 env.trust(eur(1000), alice);
1438 env.close();
1439 env.trust(eur(1000), bob);
1440 env.close();
1441 env.trust(eur(1000), carol);
1442 env.close();
1443 env(pay(gw, bob, eur(100)));
1444 env.close();
1445
1446 // bob creates a regular usd offer
1447 auto const usdOfferSeq{env.seq(bob)};
1448 env(offer(bob, XRP(10), USD(10)));
1449 env.close();
1450
1451 BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, false));
1452
1453 // bob creates a hybrid eur offer
1454 auto const eurOfferSeq{env.seq(bob)};
1455 env(offer(bob, USD(10), eur(10)), Domain(domainID), Txflags(tfHybrid));
1456 env.close();
1457 BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(10), eur(10), lsfHybrid, true));
1458
1459 // alice successfully consume two offers: xrp/usd and usd/eur
1460 env(pay(alice, carol, eur(5)), Path(~USD, ~eur), Sendmax(XRP(5)));
1461 env.close();
1462
1463 BEAST_EXPECT(checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, false));
1464 BEAST_EXPECT(checkOffer(env, bob, eurOfferSeq, USD(5), eur(5), lsfHybrid, true));
1465 }
1466 }
1467
1468 // Test that a hybrid offer remains crossable in the open book after the
1469 // owner's domain credential expires. A domain payment after expiry should
1470 // fail (domain book evicts the offer in its sandbox), but the open book
1471 // remains usable.
1472 void
1474 {
1475 testcase("Hybrid open book after credential expiry");
1476
1477 Env env(*this, features);
1478 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1479 PermissionedDEX(env);
1480
1481 Account const devin("devin");
1482 env.fund(XRP(100000), devin);
1483 env.close();
1484 env.trust(USD(1000), devin);
1485 env.close();
1486 env(pay(gw, devin, USD(100)));
1487 env.close();
1488
1489 // Give devin a credential that expires far enough in the future to
1490 // survive the setup env.close() calls.
1491 auto jv = credentials::create(devin, domainOwner, credType);
1492 uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count();
1493 jv[sfExpiration.jsonName] = t + 100;
1494 env(jv);
1495 env.close();
1496 env(credentials::accept(devin, domainOwner, credType));
1497 env.close();
1498
1499 // Devin creates a hybrid offer: sell USD(10) for XRP(10).
1500 // The offer is placed in both the domain book and the open book.
1501 auto const hybridOfferSeq{env.seq(devin)};
1502 env(offer(devin, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID));
1503 env.close();
1504
1505 BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1506
1507 // A non-domain open-book payment partially crosses the offer while
1508 // devin's credential is still valid.
1509 auto carolBalance = env.balance(carol, USD);
1510 env(pay(alice, carol, USD(5)), Path(~USD), Sendmax(XRP(5)));
1511 env.close();
1512 BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(5));
1513 BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true));
1514
1515 // Advance time so that devin's credential expires.
1516 env.close(std::chrono::seconds(100));
1517
1518 // Confirm devin can no longer create domain offers.
1519 // After fixCleanup3_4_0, OfferCreate deletes the expired credential and
1520 // returns tecEXPIRED (covered in depth by testExpiredCredentialCleanup).
1521 env(offer(devin, XRP(1), USD(1)), Domain(domainID), Ter(tecEXPIRED));
1522 env.close();
1523
1524 // The hybrid offer must still exist in the open book after expiry.
1525 BEAST_EXPECT(offerExists(env, devin, hybridOfferSeq));
1526
1527 // A non-domain open-book payment must cross (not evict) the
1528 // remaining portion of devin's hybrid offer.
1529 carolBalance = env.balance(carol, USD);
1530 env(pay(alice, carol, USD(2)), Path(~USD), Sendmax(XRP(2)));
1531 env.close();
1532
1533 // Carol received USD; the offer was crossed, not evicted.
1534 BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(2));
1535 // Offer still exists with 3 USD / 3 XRP remaining.
1536 BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(3), USD(3), lsfHybrid, true));
1537
1538 // A domain payment now fails because the domain book evicts devin's
1539 // offer (his credential has expired). The eviction is rolled back with
1540 // the failed sandbox, so the offer is NOT permanently removed.
1541 env(pay(alice, carol, USD(1)),
1542 Path(~USD),
1543 Sendmax(XRP(1)),
1544 Domain(domainID),
1546 env.close();
1547
1548 // Offer still intact in the open book; domain payment did not
1549 // permanently delete it.
1550 BEAST_EXPECT(checkOffer(env, devin, hybridOfferSeq, XRP(3), USD(3), lsfHybrid, true));
1551
1552 // The open book can still fully consume the remaining portion.
1553 carolBalance = env.balance(carol, USD);
1554 env(pay(alice, carol, USD(3)), Path(~USD), Sendmax(XRP(3)));
1555 env.close();
1556 BEAST_EXPECT(env.balance(carol, USD) - carolBalance == USD(3));
1557 BEAST_EXPECT(!offerExists(env, devin, hybridOfferSeq));
1558 }
1559
1560 void
1562 {
1563 Env env(*this, features);
1564 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1565 PermissionedDEX(env);
1566
1568 offerSeqs.reserve(100);
1569
1570 Book const domainBook{Issue(XRP), Issue(USD), domainID};
1571 Book const openBook{Issue(XRP), Issue(USD), std::nullopt};
1572
1573 auto const domainDir = getBookDirKey(domainBook, XRP(10), USD(10));
1574 auto const openDir = getBookDirKey(openBook, XRP(10), USD(10));
1575
1576 size_t dirCnt = 100;
1577
1578 for (size_t i = 1; i <= dirCnt; i++)
1579 {
1580 auto const bobOfferSeq{env.seq(bob)};
1581 offerSeqs.emplace_back(bobOfferSeq);
1582 env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID));
1583 env.close();
1584
1585 auto const sleOffer =
1586 env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobOfferSeq)));
1587 BEAST_EXPECT(sleOffer);
1588 BEAST_EXPECT(sleOffer->getFieldH256(sfBookDirectory) == domainDir);
1589 BEAST_EXPECT(sleOffer->getFieldArray(sfAdditionalBooks).size() == 1);
1590 BEAST_EXPECT(
1591 sleOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory) ==
1592 openDir);
1593
1594 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true));
1595 BEAST_EXPECT(checkDirectorySize(env, domainDir, i));
1596 BEAST_EXPECT(checkDirectorySize(env, openDir, i));
1597 }
1598
1599 for (auto const offerSeq : offerSeqs)
1600 {
1601 env(offerCancel(bob, offerSeq));
1602 env.close();
1603 dirCnt--;
1604 BEAST_EXPECT(!offerExists(env, bob, offerSeq));
1605 BEAST_EXPECT(checkDirectorySize(env, domainDir, dirCnt));
1606 BEAST_EXPECT(checkDirectorySize(env, openDir, dirCnt));
1607 }
1608 }
1609
1610 void
1612 {
1613 testcase("Auto bridge");
1614
1615 Env env(*this, features);
1616 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1617 PermissionedDEX(env);
1618 auto const eur = gw["EUR"];
1619
1620 for (auto const& account : {alice, bob, carol})
1621 {
1622 env(trust(account, eur(10000)));
1623 env.close();
1624 }
1625
1626 env(pay(gw, carol, eur(1)));
1627 env.close();
1628
1629 auto const aliceOfferSeq{env.seq(alice)};
1630 auto const bobOfferSeq{env.seq(bob)};
1631 env(offer(alice, XRP(100), USD(1)), Domain(domainID));
1632 env(offer(bob, eur(1), XRP(100)), Domain(domainID));
1633 env.close();
1634
1635 // carol's offer should cross bob and alice's offers due to auto
1636 // bridging
1637 auto const carolOfferSeq{env.seq(carol)};
1638 env(offer(carol, USD(1), eur(1)), Domain(domainID));
1639 env.close();
1640
1641 BEAST_EXPECT(!offerExists(env, bob, aliceOfferSeq));
1642 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
1643 BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq));
1644 }
1645
1646 void
1648 {
1649 bool const fixEnabled = features[fixCleanup3_4_0];
1650
1651 testcase << "Expired credential cleanup"
1652 << (fixEnabled ? " (Cleanup3_4_0 enabled)" : " (Cleanup3_4_0 disabled)");
1653
1654 TER const expectedExpiredCredTer = fixEnabled ? tecEXPIRED : tecNO_PERMISSION;
1655
1656 auto const fundAccount =
1657 [](Env& env, Account const& account, Account const& gw, IOU const& usd) {
1658 env.fund(XRP(1000), account);
1659 env.close();
1660 env.trust(usd(1000), account);
1661 env.close();
1662 env(pay(gw, account, usd(100)));
1663 env.close();
1664 };
1665
1666 auto const fundDevin = [&](Env& env, Account const& gw, IOU const& usd) {
1667 Account const devin("devin");
1668 fundAccount(env, devin, gw, usd);
1669 return devin;
1670 };
1671
1672 auto const createExpiringCredential = [](Env& env,
1673 Account const& subject,
1674 Account const& issuer,
1675 std::string const& credType) {
1676 auto jv = credentials::create(subject, issuer, credType);
1677 uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count();
1678 jv[sfExpiration.jsonName] = t + 20;
1679 env(jv);
1680 env(credentials::accept(subject, issuer, credType));
1681 env.close();
1682
1683 return keylet::credential(subject.id(), issuer.id(), makeSlice(credType));
1684 };
1685
1686 auto const expectExpiredCredentialState = [&](Env const& env, Keylet const& credKey) {
1687 if (fixEnabled)
1688 {
1689 BEAST_EXPECT(!env.le(credKey));
1690 }
1691 else
1692 {
1693 BEAST_EXPECT(env.le(credKey));
1694 }
1695 };
1696
1697 // A payment referencing a non-existent domain is rejected in preclaim.
1698 {
1699 Env env(*this, features);
1700 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1701 PermissionedDEX(env);
1702
1703 UInt256 const badDomain{
1704 "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134"
1705 "E5"};
1706
1707 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
1708 env.close();
1709
1710 env(pay(alice, bob, USD(10)),
1711 Path(~USD),
1712 Sendmax(XRP(10)),
1713 Domain(badDomain),
1715 env.close();
1716 }
1717
1718 // OfferCreate with an expired credential.
1719 {
1720 Env env(*this, features);
1721 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1722 PermissionedDEX(env);
1723
1724 Account const devin = fundDevin(env, gw, USD);
1725 auto const credKey = createExpiringCredential(env, devin, domainOwner, credType);
1726 BEAST_EXPECT(env.le(credKey)); // credential exists before expiry
1727
1728 env.close(std::chrono::seconds(20));
1729
1730 env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(expectedExpiredCredTer));
1731 env.close();
1732
1733 expectExpiredCredentialState(env, credKey);
1734 }
1735
1736 // Payment where the sender's credential is expired.
1737 {
1738 Env env(*this, features);
1739 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1740 PermissionedDEX(env);
1741
1742 Account const devin = fundDevin(env, gw, USD);
1743 auto const credKey = createExpiringCredential(env, devin, domainOwner, credType);
1744
1745 auto const bobOfferSeq{env.seq(bob)};
1746 auto const bobCredKey =
1747 keylet::credential(bob.id(), domainOwner.id(), makeSlice(credType));
1748 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
1749 env.close();
1750
1751 BEAST_EXPECT(env.le(credKey));
1752 BEAST_EXPECT(env.le(bobCredKey));
1753 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1754
1755 env.close(std::chrono::seconds(20));
1756
1757 env(pay(devin, alice, USD(10)),
1758 Path(~USD),
1759 Sendmax(XRP(10)),
1760 Domain(domainID),
1761 Ter(expectedExpiredCredTer));
1762 env.close();
1763
1764 expectExpiredCredentialState(env, credKey);
1765 BEAST_EXPECT(env.le(bobCredKey));
1766 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1767 }
1768
1769 // Payment where the destination's credential is expired.
1770 {
1771 Env env(*this, features);
1772 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1773 PermissionedDEX(env);
1774
1775 Account const devin = fundDevin(env, gw, USD);
1776 auto const credKey = createExpiringCredential(env, devin, domainOwner, credType);
1777
1778 auto const bobOfferSeq{env.seq(bob)};
1779 auto const bobCredKey =
1780 keylet::credential(bob.id(), domainOwner.id(), makeSlice(credType));
1781 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
1782 env.close();
1783
1784 BEAST_EXPECT(env.le(credKey));
1785 BEAST_EXPECT(env.le(bobCredKey));
1786 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1787
1788 env.close(std::chrono::seconds(20));
1789
1790 env(pay(alice, devin, USD(10)),
1791 Path(~USD),
1792 Sendmax(XRP(10)),
1793 Domain(domainID),
1794 Ter(expectedExpiredCredTer));
1795 env.close();
1796
1797 expectExpiredCredentialState(env, credKey);
1798 BEAST_EXPECT(env.le(bobCredKey));
1799 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1800 }
1801
1802 // Payment where both sender and destination credentials are expired.
1803 {
1804 Env env(*this, features);
1805 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1806 PermissionedDEX(env);
1807
1808 Account const devin = fundDevin(env, gw, USD);
1809 Account const erin("erin");
1810 fundAccount(env, erin, gw, USD);
1811
1812 auto const devinCredKey = createExpiringCredential(env, devin, domainOwner, credType);
1813 auto const erinCredKey = createExpiringCredential(env, erin, domainOwner, credType);
1814
1815 auto const bobOfferSeq{env.seq(bob)};
1816 auto const bobCredKey =
1817 keylet::credential(bob.id(), domainOwner.id(), makeSlice(credType));
1818 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
1819 env.close();
1820
1821 BEAST_EXPECT(env.le(devinCredKey));
1822 BEAST_EXPECT(env.le(erinCredKey));
1823 BEAST_EXPECT(env.le(bobCredKey));
1824 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1825
1826 env.close(std::chrono::seconds(20));
1827
1828 env(pay(devin, erin, USD(10)),
1829 Path(~USD),
1830 Sendmax(XRP(10)),
1831 Domain(domainID),
1832 Ter(expectedExpiredCredTer));
1833 env.close();
1834
1835 expectExpiredCredentialState(env, devinCredKey);
1836 expectExpiredCredentialState(env, erinCredKey);
1837 BEAST_EXPECT(env.le(bobCredKey));
1838 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1839 }
1840 }
1841
1842 void
1844 {
1845 bool const fixEnabled = features[fixCleanup3_1_3];
1846
1847 testcase << "Hybrid offer with empty AdditionalBooks"
1848 << (fixEnabled ? " (fixCleanup3_1_3 enabled)" : " (fixCleanup3_1_3 disabled)");
1849
1850 // offerInDomain has two code paths gated by fixCleanup3_1_3:
1851 //
1852 // pre-fix: only rejects a hybrid offer when sfAdditionalBooks is
1853 // entirely absent — an empty array (size 0) passes through.
1854 // post-fix: also rejects a hybrid offer whose sfAdditionalBooks array
1855 // has size != 1 (i.e. 0 or >1 entries).
1856 //
1857 // We create a valid hybrid offer, then directly manipulate its SLE to
1858 // produce the size==0 case that cannot occur via normal transactions,
1859 // and verify that the two code paths produce the expected outcomes.
1860 //
1861 // Note: the PermissionedDEX invariant checker (ValidPermissionedDEX)
1862 // does not flag this malformation for ttPAYMENT — only for
1863 // ttOFFER_CREATE — so the without-fix payment completes as tesSUCCESS.
1864
1865 Env env(*this, features);
1866 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
1867 PermissionedDEX(env);
1868
1869 // Create a valid hybrid offer (sfAdditionalBooks has exactly 1 entry)
1870 auto const bobOfferSeq{env.seq(bob)};
1871 env(offer(bob, XRP(10), USD(10)), Txflags(tfHybrid), Domain(domainID));
1872 env.close();
1873 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
1874
1875 // Directly manipulate the offer SLE in the open ledger so that
1876 // sfAdditionalBooks is present but empty (size 0). This is the
1877 // malformed state that fixCleanup3_1_3 is designed to catch.
1878 auto const offerKey = keylet::offer(bob.id(), SeqProxy::rawSequence(bobOfferSeq));
1879 env.app().getOpenLedger().modify([&offerKey](OpenView& view, beast::Journal) {
1880 auto const sle = view.read(offerKey);
1881 if (!sle)
1882 return false;
1883 auto replacement = std::make_shared<SLE>(*sle, sle->key());
1884 replacement->setFieldArray(sfAdditionalBooks, STArray{});
1885 view.rawReplace(replacement);
1886 return true;
1887 });
1888
1889 if (fixEnabled)
1890 {
1891 // post-fixCleanup3_1_3: offerInDomain rejects the malformed
1892 // offer (size == 0), so no valid domain offer is found.
1893 env(pay(alice, carol, USD(10)),
1894 Path(~USD),
1895 Sendmax(XRP(10)),
1896 Domain(domainID),
1898 }
1899 else
1900 {
1901 // pre-fixCleanup3_1_3: offerInDomain only checks for a missing
1902 // sfAdditionalBooks field; size == 0 passes through, so the
1903 // malformed offer is crossed and the payment succeeds.
1904 env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
1905 }
1906 }
1907
1908 void
1910 {
1911 bool const fixEnabled = features[fixCleanup3_2_0];
1912 testcase << "Hybrid offer crossing quality"
1913 << (fixEnabled ? " (fixCleanup3_2_0)" : " (pre-fix)");
1914
1915 // Partially-crossed hybrid offer should have consistent quality
1916 // across both book directories.
1917 //
1918 // Steps:
1919 // - Bob places a hybrid offer.
1920 // - Alice places an opposing hybrid offer that partially crosses.
1921 //
1922 // Verify:
1923 // - Domain-book key quality == its sfExchangeRate.
1924 // - Post-fix: open-book key quality == domain-book key quality.
1925 // - Pre-fix: open-book key quality != domain-book key quality
1926 // (key used post-crossing rate, sfExchangeRate used pre-crossing).
1927
1928 Env env(*this, features);
1929 auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] =
1930 PermissionedDEX(env);
1931
1932 // Bob places a hybrid offer: TakerPays = XRP(100), TakerGets = USD(40)
1933 auto const bobOfferSeq{env.seq(bob_)};
1934 env(offer(bob_, XRP(100), USD(40)), Txflags(tfHybrid), Domain(domainID));
1935 env.close();
1936 BEAST_EXPECT(offerExists(env, bob_, bobOfferSeq));
1937
1938 // Alice places a hybrid offer in the opposite direction that
1939 // partially crosses Bob's offer.
1940 // Alice: TakerPays = USD(100), TakerGets = XRP(300) (rate = 3 XRP/USD)
1941 // Bob's offer is at a better rate (2.5 XRP/USD) so crossing occurs.
1942 auto const aliceOfferSeq{env.seq(alice_)};
1943 env(offer(alice_, USD(100), XRP(300)), Txflags(tfHybrid), Domain(domainID));
1944 env.close();
1945
1946 // After crossing, Alice's remaining offer should be placed.
1947 auto const sle = env.le(keylet::offer(alice_.id(), SeqProxy::rawSequence(aliceOfferSeq)));
1948 BEAST_EXPECT(sle);
1949 BEAST_EXPECT(sle->isFieldPresent(sfAdditionalBooks));
1950 BEAST_EXPECT(sle->getFieldArray(sfAdditionalBooks).size() == 1);
1951
1952 auto const domainDirKey = sle->getFieldH256(sfBookDirectory);
1953 auto const openDirKey =
1954 sle->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory);
1955
1956 auto const domainQuality = getQuality(domainDirKey);
1957 auto const openQuality = getQuality(openDirKey);
1958
1959 // Read the directory SLEs and check sfExchangeRate vs key quality.
1960 auto const domainDirSle = env.le(Keylet(ltDIR_NODE, domainDirKey));
1961 auto const openDirSle = env.le(Keylet(ltDIR_NODE, openDirKey));
1962 BEAST_EXPECT(domainDirSle);
1963 BEAST_EXPECT(openDirSle);
1964
1965 auto const domainExRate = domainDirSle->getFieldU64(sfExchangeRate);
1966 auto const openExRate = openDirSle->getFieldU64(sfExchangeRate);
1967 auto const preCrossingQuality = std::uint64_t{5623825668291712342ULL};
1968 auto const postCrossingQuality = std::uint64_t{5623825668291712341ULL};
1969
1970 // Domain directory: sfExchangeRate should always match key quality
1971 // (both use the pre-crossing rate). Correct behavior.
1972 BEAST_EXPECT(domainQuality == preCrossingQuality);
1973 BEAST_EXPECT(domainExRate == preCrossingQuality);
1974 BEAST_EXPECT(domainExRate == domainQuality);
1975
1976 if (fixEnabled)
1977 {
1978 // Correct behavior: both directory keys use the pre-crossing rate.
1979 BEAST_EXPECT(openQuality == preCrossingQuality);
1980 BEAST_EXPECT(domainQuality == openQuality);
1981
1982 // sfExchangeRate matches key quality on both directories.
1983 BEAST_EXPECT(openExRate == preCrossingQuality);
1984 BEAST_EXPECT(openExRate == openQuality);
1985 }
1986 else
1987 {
1988 // Wrong legacy behavior: the open-book directory key uses the
1989 // post-crossing rate instead of the domain-book rate.
1990 BEAST_EXPECT(openQuality == postCrossingQuality);
1991 BEAST_EXPECT(domainQuality != openQuality);
1992
1993 // The open-book sfExchangeRate still uses the pre-crossing rate,
1994 // so it no longer matches the actual quality encoded in the
1995 // open-book directory key.
1996 BEAST_EXPECT(openExRate == preCrossingQuality);
1997 BEAST_EXPECT(openExRate != openQuality);
1998 BEAST_EXPECT(openExRate == domainQuality);
1999 }
2000 }
2001
2002 void
2004 {
2005 testcase("LedgerStateFix BookExchangeRate");
2006
2007 // Use the pre-fix path to create a hybrid offer with a mismatched
2008 // sfExchangeRate, then apply LedgerStateFix to correct it.
2009 //
2010 // Steps:
2011 // - Create a partially-crossed hybrid offer (pre-fixCleanup3_2_0)
2012 // so the open-book directory has wrong sfExchangeRate.
2013 // - Re-enable fixCleanup3_2_0 and submit a LedgerStateFix to
2014 // repair the open-book directory's sfExchangeRate.
2015 //
2016 // Verify:
2017 // - Before fix: sfExchangeRate != getQuality(key).
2018 // - After fix: sfExchangeRate == getQuality(key).
2019
2020 {
2021 // Amendment gate: BookExchangeRate fixes require fixCleanup3_2_0.
2022 Env env(*this, features - fixCleanup3_2_0);
2023 Account const carol{"carol"};
2024
2025 env.fund(XRP(1000), carol);
2026 env.close();
2027
2029 }
2030
2031 {
2032 // Preflight check: BookExchangeRate fixes only accept their
2033 // required fix-specific field.
2034 Env env(*this, features);
2035 Account const carol{"carol"};
2036
2037 env.fund(XRP(1000), carol);
2038 env.close();
2039
2040 // BookExchangeRate fixes require sfBookDirectory.
2041 auto missingBookDirectory = ledger_state_fix::bookExchangeRate(carol, UInt256{1});
2042 missingBookDirectory.removeMember(sfBookDirectory.jsonName);
2043 env(missingBookDirectory, Ter(temINVALID));
2044
2045 // BookExchangeRate fixes reject fields that belong to other
2046 // LedgerStateFix types.
2047 auto extraOwner = ledger_state_fix::bookExchangeRate(carol, UInt256{1});
2048 extraOwner[sfOwner.jsonName] = carol.human();
2049 env(extraOwner, Ter(temINVALID));
2050 }
2051
2052 {
2053 Env env(*this, features);
2054 auto const setup = PermissionedDEX(env);
2055 auto const fixFee = drops(env.current()->fees().increment);
2056
2057 {
2058 // Preclaim check: the target directory must exist.
2059 env(ledger_state_fix::bookExchangeRate(setup.carol, UInt256{1}),
2060 Fee(fixFee),
2062 }
2063
2064 {
2065 // Preclaim check: the target directory must be a book root
2066 // page. Owner directories are ltDIR_NODE entries, but they do
2067 // not carry sfExchangeRate.
2068 auto const ownerDir = keylet::ownerDir(setup.bob.id());
2069 auto const ownerDirSle = env.le(ownerDir);
2070 BEAST_EXPECT(ownerDirSle);
2071 BEAST_EXPECT(!ownerDirSle->isFieldPresent(sfExchangeRate));
2072
2073 env(ledger_state_fix::bookExchangeRate(setup.carol, ownerDir.key),
2074 Fee(fixFee),
2076 }
2077
2078 {
2079 // Preclaim check: a correct sfExchangeRate leaves nothing to
2080 // repair.
2081 auto const bobOfferSeq{env.seq(setup.bob)};
2082 env(offer(setup.bob, XRP(100), setup.usd(40)));
2083 env.close();
2084
2085 auto const sle =
2086 env.le(keylet::offer(setup.bob.id(), SeqProxy::rawSequence(bobOfferSeq)));
2087 BEAST_EXPECT(sle);
2088
2089 auto const dirKey = sle->getFieldH256(sfBookDirectory);
2090 {
2091 auto const dirSle = env.le(Keylet(ltDIR_NODE, dirKey));
2092 BEAST_EXPECT(dirSle);
2093 auto const exchangeRate = dirSle->getFieldU64(sfExchangeRate);
2094 auto const quality = getQuality(dirKey);
2095 BEAST_EXPECT(exchangeRate == quality);
2096 }
2097
2098 env(ledger_state_fix::bookExchangeRate(setup.carol, dirKey),
2099 Fee(fixFee),
2101 }
2102 }
2103
2104 {
2105 // Repair path: start without fixCleanup3_2_0 to produce the
2106 // mismatch, then enable the amendment and fix it.
2107 Env env(*this, features - fixCleanup3_2_0);
2108 auto const& [gw_, domainOwner, alice_, bob_, carol_, USD, domainID, credType] =
2109 PermissionedDEX(env);
2110
2111 // Bob places a hybrid offer.
2112 env(offer(bob_, XRP(100), USD(40)), Txflags(tfHybrid), Domain(domainID));
2113 env.close();
2114
2115 // Alice partially crosses Bob.
2116 auto const aliceOfferSeq{env.seq(alice_)};
2117 env(offer(alice_, USD(100), XRP(300)), Txflags(tfHybrid), Domain(domainID));
2118 env.close();
2119
2120 auto const sle =
2121 env.le(keylet::offer(alice_.id(), SeqProxy::rawSequence(aliceOfferSeq)));
2122 BEAST_EXPECT(sle);
2123
2124 auto const openDirKey =
2125 sle->getFieldArray(sfAdditionalBooks)[0].getFieldH256(sfBookDirectory);
2126
2127 auto const preCrossingQuality = std::uint64_t{5623825668291712342ULL};
2128 auto const postCrossingQuality = std::uint64_t{5623825668291712341ULL};
2129
2130 // Confirm mismatch exists.
2131 {
2132 auto const dirSle = env.le(Keylet(ltDIR_NODE, openDirKey));
2133 BEAST_EXPECT(dirSle);
2134 auto const exchangeRate = dirSle->getFieldU64(sfExchangeRate);
2135 auto const quality = getQuality(openDirKey);
2136 BEAST_EXPECT(exchangeRate == preCrossingQuality);
2137 BEAST_EXPECT(quality == postCrossingQuality);
2138 BEAST_EXPECT(exchangeRate != quality);
2139 }
2140
2141 // Enable fixCleanup3_2_0 and apply the LedgerStateFix.
2142 env.enableFeature(fixCleanup3_2_0);
2143 env.close();
2144
2145 auto const fixFee = drops(env.current()->fees().increment);
2146 env(ledger_state_fix::bookExchangeRate(carol_, openDirKey), Fee(fixFee));
2147 env.close();
2148
2149 // Confirm sfExchangeRate now matches the key quality.
2150 {
2151 auto const dirSle = env.le(Keylet(ltDIR_NODE, openDirKey));
2152 BEAST_EXPECT(dirSle);
2153 auto const exchangeRate = dirSle->getFieldU64(sfExchangeRate);
2154 auto const quality = getQuality(openDirKey);
2155 BEAST_EXPECT(exchangeRate == postCrossingQuality);
2156 BEAST_EXPECT(quality == postCrossingQuality);
2157 BEAST_EXPECT(exchangeRate == quality);
2158 }
2159
2160 // Submitting again should fail — nothing to fix.
2161 env(ledger_state_fix::bookExchangeRate(carol_, openDirKey),
2162 Fee(fixFee),
2164 }
2165 }
2166
2167 void
2169 {
2170 bool const fixEnabled = features[fixCleanup3_2_0];
2171
2172 testcase << "Cancel regular offer via domain OfferCreate"
2173 << (fixEnabled ? " (fixCleanup3_2_0 enabled)" : " (fixCleanup3_2_0 disabled)");
2174
2175 // An OfferCreate with sfDomainID and sfOfferSequence pointing to
2176 // the user's own non-domain offer should atomically cancel the
2177 // regular offer and place the new domain offer.
2178 //
2179 // Pre-fixCleanup3_2_0: ValidPermissionedDEX flagged the deleted
2180 // regular offer, so the transaction failed with tecINVARIANT_FAILED.
2181 // Post-fixCleanup3_2_0: the invariant ignores deletions and the
2182 // transaction succeeds.
2183
2184 Env env(*this, features);
2185 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
2186 PermissionedDEX(env);
2187
2188 auto const regularSeq = env.seq(bob);
2189 env(offer(bob, XRP(10), USD(10)));
2190 env.close();
2191 BEAST_EXPECT(checkOffer(env, bob, regularSeq, XRP(10), USD(10), 0, false));
2192
2193 auto const domainSeq = env.seq(bob);
2194 if (fixEnabled)
2195 {
2196 env(offer(bob, XRP(20), USD(20)),
2197 Domain(domainID),
2198 Json(jss::OfferSequence, regularSeq));
2199 env.close();
2200 BEAST_EXPECT(!offerExists(env, bob, regularSeq));
2201 BEAST_EXPECT(checkOffer(env, bob, domainSeq, XRP(20), USD(20), 0, true));
2202 }
2203 else
2204 {
2205 env(offer(bob, XRP(20), USD(20)),
2206 Domain(domainID),
2207 Json(jss::OfferSequence, regularSeq),
2209 env.close();
2210 BEAST_EXPECT(offerExists(env, bob, regularSeq));
2211 BEAST_EXPECT(!offerExists(env, bob, domainSeq));
2212 }
2213 }
2214
2215 void
2217 {
2218 bool const fixEnabled = features[fixCleanup3_4_0];
2219
2220 testcase << "Domain offer indexed in the wrong domain book"
2221 << (fixEnabled ? " (fixCleanup3_4_0 enabled)" : " (fixCleanup3_4_0 disabled)");
2222
2223 // Bob (a member of domains A and B) places an offer in domain A's
2224 // book, which we then corrupt to claim domain B while it stays in
2225 // domain A's book. A payment routed through domain A meets this offer.
2226 //
2227 // - With fixCleanup3_4_0: OfferStream sees the offer's domain (B)
2228 // mismatch the book (A) and errors out -> tecPATH_PARTIAL.
2229 // - Without it: OfferStream only checks the offer's own domain (B,
2230 // which Bob is in), so it is used; the invariant then catches the
2231 // mismatch -> tecINVARIANT_FAILED.
2232 //
2233 // Either way the payment fails and the offer is left untouched.
2234
2235 Env env(*this, features);
2236 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
2237 PermissionedDEX(env);
2238
2239 // A second domain that Bob also belongs to.
2240 Account const bobAcct = bob;
2241 auto const domainID2 =
2242 setupDomain(env, {bobAcct}, Account("permdex-domainOwner2"), "permdex-cred2");
2243
2244 // Bob places a domain offer in domain A's book.
2245 auto const bobOfferSeq{env.seq(bob)};
2246 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
2247 env.close();
2248 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
2249
2250 // Corrupt the offer: point its sfDomainID at domain B while it stays
2251 // indexed in domain A's book directory.
2252 auto const offerKey = keylet::offer(bob.id(), SeqProxy::rawSequence(bobOfferSeq));
2253 env.app().getOpenLedger().modify([&offerKey, &domainID2](OpenView& view, beast::Journal) {
2254 auto const sle = view.read(offerKey);
2255 if (!sle)
2256 return false;
2257 auto replacement = std::make_shared<SLE>(*sle, sle->key());
2258 replacement->setFieldH256(sfDomainID, domainID2);
2259 view.rawReplace(replacement);
2260 return true;
2261 });
2262
2263 if (fixEnabled)
2264 {
2265 // With the fix: OfferStream rejects the mismatched offer.
2266 env(pay(alice, carol, USD(10)),
2267 Path(~USD),
2268 Sendmax(XRP(10)),
2269 Domain(domainID),
2271 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
2272 }
2273 else
2274 {
2275 // Without the fix: the offer is used, then the invariant
2276 // rejects the whole transaction.
2277 env(pay(alice, carol, USD(10)),
2278 Path(~USD),
2279 Sendmax(XRP(10)),
2280 Domain(domainID),
2282 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
2283 }
2284 }
2285
2286 void
2288 {
2289 bool const fixEnabled = features[fixCleanup3_4_0];
2290
2291 testcase << "Offer without a domain indexed in a domain book"
2292 << (fixEnabled ? " (fixCleanup3_4_0 enabled)" : " (fixCleanup3_4_0 disabled)");
2293
2294 // Same corruption as testDomainOfferInWrongBook, except the offer
2295 // loses sfDomainID entirely instead of pointing at another domain
2296 // while it stays indexed in domain A's book.
2297 //
2298 // - With fixCleanup3_4_0: OfferStream sees an offer that claims no
2299 // domain in a domain book and errors out -> tecPATH_PARTIAL.
2300 // - Without it: neither the domain mismatch check nor the domain
2301 // membership check fires (both are gated on sfDomainID being
2302 // present), and the invariant does not catch it either because the
2303 // offer is fully consumed and deleted. The payment succeeds using an
2304 // offer that was never credential checked.
2305
2306 Env env(*this, features);
2307 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
2308 PermissionedDEX(env);
2309
2310 // Bob places a domain offer in domain A's book.
2311 auto const bobOfferSeq{env.seq(bob)};
2312 env(offer(bob, XRP(10), USD(10)), Domain(domainID));
2313 env.close();
2314 BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
2315
2316 // Corrupt the offer: drop sfDomainID while it stays indexed in domain
2317 // A's book directory.
2318 auto const offerKey = keylet::offer(bob.id(), SeqProxy::rawSequence(bobOfferSeq));
2319 env.app().getOpenLedger().modify([&offerKey](OpenView& view, beast::Journal) {
2320 auto const sle = view.read(offerKey);
2321 if (!sle)
2322 return false;
2323 auto replacement = std::make_shared<SLE>(*sle, sle->key());
2324 replacement->makeFieldAbsent(sfDomainID);
2325 view.rawReplace(replacement);
2326 return true;
2327 });
2328
2329 auto const carolBefore = env.balance(carol, USD);
2330
2331 if (fixEnabled)
2332 {
2333 // With the fix: OfferStream rejects the domainless offer.
2334 env(pay(alice, carol, USD(10)),
2335 Path(~USD),
2336 Sendmax(XRP(10)),
2337 Domain(domainID),
2339 BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
2340 BEAST_EXPECT(env.balance(carol, USD) - carolBefore == USD(0));
2341 }
2342 else
2343 {
2344 // Without the fix: the offer is silently usable in the domain
2345 // book, and the payment goes through.
2346 env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
2347 BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
2348 BEAST_EXPECT(env.balance(carol, USD) - carolBefore == USD(10));
2349 }
2350 }
2351
2352 void
2354 {
2355 bool const fixEnabled = features[fixCleanup3_4_0];
2356
2357 testcase << "Replace domain offer via OfferCreate"
2358 << (fixEnabled ? " (fixCleanup3_4_0 enabled)" : " (fixCleanup3_4_0 disabled)");
2359
2360 Env env(*this, features);
2361 auto const& [gw, domainOwner, alice, bob, carol, USD, domainA, credType] =
2362 PermissionedDEX(env);
2363
2364 Account const domainOwnerB("permdex-domainOwnerB");
2365 auto const domainB =
2366 setupDomain(env, {alice, bob, carol, gw}, domainOwnerB, "permdex-other-domain");
2367 BEAST_EXPECT(domainA != domainB);
2368
2369 auto const oldSeq = env.seq(alice);
2370 env(offer(alice, USD(100), XRP(1)), Domain(domainA));
2371 env.close();
2372
2373 BEAST_EXPECT(checkOffer(env, alice, oldSeq, USD(100), XRP(1), 0, true));
2374 auto const oldOffer = env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(oldSeq)));
2375 if (!BEAST_EXPECT(oldOffer))
2376 return;
2377 BEAST_EXPECT(oldOffer->getFieldH256(sfDomainID) == domainA);
2378
2379 auto const newSeq = env.seq(alice);
2380 // The invariant should reject mixing active Permissioned DEX domains,
2381 // not a domain that is only touched because its offer is being deleted.
2382 if (fixEnabled)
2383 {
2384 env(offer(alice, USD(100), XRP(2)), Domain(domainB), Json(jss::OfferSequence, oldSeq));
2385 env.close();
2386
2387 BEAST_EXPECT(!offerExists(env, alice, oldSeq));
2388 BEAST_EXPECT(checkOffer(env, alice, newSeq, USD(100), XRP(2), 0, true));
2389 auto const newOffer = env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(newSeq)));
2390 if (!BEAST_EXPECT(newOffer))
2391 return;
2392 BEAST_EXPECT(newOffer->getFieldH256(sfDomainID) == domainB);
2393 }
2394 else
2395 {
2396 env(offer(alice, USD(100), XRP(2)),
2397 Domain(domainB),
2398 Json(jss::OfferSequence, oldSeq),
2400 env.close();
2401
2402 BEAST_EXPECT(checkOffer(env, alice, oldSeq, USD(100), XRP(1), 0, true));
2403 BEAST_EXPECT(!offerExists(env, alice, newSeq));
2404 }
2405 }
2406
2407public:
2408 void
2409 run() override
2410 {
2412
2413 // Test domain offer (w/o hybrid)
2414 testOfferCreate(all);
2415 testOfferCreate(all - fixCleanup3_2_0);
2416 testOfferCreate(all - fixCleanup3_4_0);
2417 testPayment(all);
2418 testPayment(all - fixCleanup3_2_0);
2419 testBookStep(all);
2420 testRippling(all);
2423 testAmmNotUsed(all);
2425 testAmmQualityNotLeaked(all - fixCleanup3_3_0);
2426 testAutoBridge(all);
2428 testExpiredCredentialCleanup(all - fixCleanup3_4_0);
2429
2430 // Test hybrid offers
2432 testHybridBookStep(all);
2433 testHybridInvalidOffer(all - fixCleanup3_3_0);
2438 testHybridMalformedOffer(all - fixCleanup3_1_3);
2440 testHybridOfferCrossingQuality(all - fixCleanup3_2_0);
2442
2443 // Cancelling a regular offer in a domain OfferCreate is allowed
2444 // only after fixCleanup3_2_0.
2446 testCancelRegularOfferWithDomainCreate(all - fixCleanup3_2_0);
2447
2448 // A domain offer indexed in the wrong domain book is caught only
2449 // after fixCleanup3_4_0. (Not an existing bug, but defensive testing)
2451 testDomainOfferInWrongBook(all - fixCleanup3_4_0);
2453 testDomainBookOfferMissingDomain(all - fixCleanup3_4_0);
2454
2456 testReplaceDomainOfferWithOtherDomainOffer(all - fixCleanup3_4_0);
2457 }
2458};
2459
2461
2462} // namespace xrpl::test
T any_of(T... args)
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
Value removeMember(char const *key)
Remove and return the named member.
Specifies an order book.
Definition Book.h:28
A currency issued by an account.
Definition Issue.h:18
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
void rawReplace(SLE::Ref sle) override
Unconditionally replace a state item.
Definition OpenView.cpp:244
SLE::const_pointer read(Keylet const &k) const override
Return the state item associated with a key.
Definition OpenView.cpp:168
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
void testRippling(FeatureBitset features)
static std::optional< UInt256 > getDefaultOfferDirKey(Env const &env, Account const &account, std::uint32_t offerSeq)
static bool checkDirectorySize(Env const &env, UInt256 directory, std::uint32_t dirSize)
void testOfferCreate(FeatureBitset features)
void testHybridOfferDirectories(FeatureBitset features)
static UInt256 getBookDirKey(Book const &book, STAmount const &takerPays, STAmount const &takerGets)
void testExpiredCredentialCleanup(FeatureBitset features)
void testHybridOfferCreate(FeatureBitset features)
void testCancelRegularOfferWithDomainCreate(FeatureBitset features)
void testHybridOpenBookAfterCredentialExpiry(FeatureBitset features)
void testHybridOfferCrossingQuality(FeatureBitset features)
void testAutoBridge(FeatureBitset features)
void testOfferTokenIssuerInDomain(FeatureBitset features)
void run() override
Runs the suite.
void testPayment(FeatureBitset features)
void testHybridBookStep(FeatureBitset features)
void testAmmNotUsed(FeatureBitset features)
void testBookStep(FeatureBitset features)
void testDomainOfferInWrongBook(FeatureBitset features)
static bool offerExists(Env const &env, Account const &account, std::uint32_t offerSeq)
void testAmmQualityNotLeaked(FeatureBitset features)
void testReplaceDomainOfferWithOtherDomainOffer(FeatureBitset features)
static bool checkOffer(Env const &env, Account const &account, std::uint32_t offerSeq, STAmount const &takerPays, STAmount const &takerGets, uint32_t const flags=0, bool const domainOffer=false)
void testDomainBookOfferMissingDomain(FeatureBitset features)
void testBookExchangeRateFix(FeatureBitset features)
void testHybridMalformedOffer(FeatureBitset features)
void testHybridInvalidOffer(FeatureBitset features)
void testRemoveUnfundedOffer(FeatureBitset features)
Convenience class to test AMM functionality.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
std::string const & human() const
Returns the human readable public key.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
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 enableFeature(UInt256 const feature)
Definition Env.cpp:709
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:766
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
Set the fee on a JTx.
Definition fee.h:20
Converts to IOU Issue or STAmount.
Inject raw JSON.
Definition jtx_json.h:16
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 emplace_back(T... args)
T make_shared(T... args)
Keylet quality(Keylet const &k, std::uint64_t const q) noexcept
The initial directory page for a specific quality.
Definition Indexes.cpp:304
Keylet offer(AccountID const &id, SeqProxy const &seq) noexcept
An offer from an account.
Definition Indexes.cpp:298
Keylet book(Book const &b)
The beginning of an order book.
Definition Indexes.cpp:269
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:403
Keylet page(UInt256 const &root, std::uint64_t const index=0) noexcept
A page in a directory.
Definition Indexes.cpp:409
Keylet credential(AccountID const &subject, AccountID const &issuer, Slice const &credType) noexcept
Definition Indexes.cpp:585
json::Value deleteCred(jtx::Account const &acc, jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:40
json::Value accept(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:29
json::Value create(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:16
Directory operations.
Definition directory.h:19
json::Value bookExchangeRate(jtx::Account const &acct, UInt256 const &bookDir)
Repair sfExchangeRate on a book directory's first page.
std::map< UInt256, json::Value > getObjects(Account const &account, Env &env, bool withType)
std::vector< Credential > Credentials
json::Value setTx(AccountID const &account, Credentials const &credentials, std::optional< UInt256 > domain)
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
json::Value offerCancel(Account const &account, std::uint32_t offerSeq)
Cancel an offer.
Definition offer.cpp:31
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
std::uint32_t ownerCount(Env const &env, Account const &account)
FeatureBitset testableAmendments()
Definition Env.h:92
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
UInt256 setupDomain(jtx::Env &env, std::vector< jtx::Account > const &accounts, jtx::Account const &domainOwner, std::string const &credType)
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
BaseUInt< 256 > Domain
Domain is a 256-bit hash representing a specific domain.
Definition UintTypes.h:59
std::uint64_t getQuality(UInt256 const &uBase)
Definition Indexes.cpp:194
Slice makeSlice(std::array< T, N > const &a)
Definition Slice.h:228
BaseUInt< 256 > UInt256
Definition base_uint.h:580
std::uint64_t getRate(STAmount const &offerOut, STAmount const &offerIn)
Definition STAmount.cpp:423
@ temINVALID
Definition TER.h:98
@ temINVALID_FLAG
Definition TER.h:99
@ temMALFORMED
Definition TER.h:75
@ temDISABLED
Definition TER.h:102
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecPATH_PARTIAL
Definition TER.h:290
@ tecPATH_DRY
Definition TER.h:302
@ tecOBJECT_NOT_FOUND
Definition TER.h:334
@ tecINVARIANT_FAILED
Definition TER.h:321
@ tecEXPIRED
Definition TER.h:322
@ tecNO_PERMISSION
Definition TER.h:313
T reserve(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
UInt256 key
Definition Keylet.h:21
T value_or(T... args)