xrpld
Loading...
Searching...
No Matches
Invariants_test.cpp
1#include <test/jtx/AMM.h>
2#include <test/jtx/Account.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/TestHelpers.h>
5#include <test/jtx/amount.h>
6#include <test/jtx/envconfig.h>
7#include <test/jtx/fee.h>
8#include <test/jtx/mpt.h>
9#include <test/jtx/pay.h>
10#include <test/jtx/permissioned_domains.h>
11#include <test/jtx/tags.h>
12#include <test/jtx/token.h>
13#include <test/jtx/trust.h>
14#include <test/jtx/vault.h>
15#include <test/unit_test/SuiteJournal.h>
16
17#include <xrpl/basics/Number.h>
18#include <xrpl/basics/base_uint.h>
19#include <xrpl/basics/chrono.h>
20#include <xrpl/beast/unit_test/suite.h>
21#include <xrpl/beast/utility/Journal.h>
22#include <xrpl/beast/utility/Zero.h>
23#include <xrpl/ledger/ApplyView.h>
24#include <xrpl/ledger/OpenView.h>
25#include <xrpl/ledger/helpers/AccountRootHelpers.h>
26#include <xrpl/ledger/helpers/DirectoryHelpers.h>
27#include <xrpl/ledger/helpers/RippleStateHelpers.h>
28#include <xrpl/protocol/AccountID.h>
29#include <xrpl/protocol/Book.h>
30#include <xrpl/protocol/Feature.h>
31#include <xrpl/protocol/Indexes.h>
32#include <xrpl/protocol/InnerObjectFormats.h>
33#include <xrpl/protocol/Issue.h>
34#include <xrpl/protocol/Keylet.h>
35#include <xrpl/protocol/LedgerFormats.h>
36#include <xrpl/protocol/MPTIssue.h>
37#include <xrpl/protocol/Protocol.h>
38#include <xrpl/protocol/Rules.h>
39#include <xrpl/protocol/SField.h>
40#include <xrpl/protocol/SOTemplate.h>
41#include <xrpl/protocol/STAmount.h>
42#include <xrpl/protocol/STArray.h>
43#include <xrpl/protocol/STLedgerEntry.h>
44#include <xrpl/protocol/STObject.h>
45#include <xrpl/protocol/STTx.h>
46#include <xrpl/protocol/SeqProxy.h>
47#include <xrpl/protocol/SystemParameters.h>
48#include <xrpl/protocol/TER.h>
49#include <xrpl/protocol/TxFlags.h>
50#include <xrpl/protocol/TxFormats.h>
51#include <xrpl/protocol/UintTypes.h>
52#include <xrpl/protocol/XRPAmount.h>
53#include <xrpl/protocol/jss.h>
54#include <xrpl/tx/ApplyContext.h>
55#include <xrpl/tx/Transactor.h>
56#include <xrpl/tx/applySteps.h>
57#include <xrpl/tx/invariants/AMMInvariant.h>
58#include <xrpl/tx/invariants/DirectoryInvariant.h>
59#include <xrpl/tx/invariants/VaultInvariant.h>
60
61#include <algorithm>
62#include <array>
63#include <cstddef>
64#include <cstdint>
65#include <functional>
66#include <initializer_list>
67#include <memory>
68#include <optional>
69#include <source_location>
70#include <string>
71#include <tuple>
72#include <utility>
73#include <vector>
74
75namespace xrpl {
76
77// Test-only factory — not part of the public API.
78// The returned Transactor holds a raw reference to ctx; the caller must ensure
79// the ApplyContext outlives the Transactor. Implemented in applySteps.cpp
80std::unique_ptr<Transactor>
82
83} // namespace xrpl
84
85namespace xrpl::test {
86
88{
89 // The optional Preclose function is used to process additional transactions
90 // on the ledger after creating two accounts, but before closing it, and
91 // before the Precheck function. These should only be valid functions, and
92 // not direct manipulations. Preclose is not commonly used.
94 bool(test::jtx::Account const& a, test::jtx::Account const& b, test::jtx::Env& env)>;
95
96 // this is common setup/method for running a failing invariant check. The
97 // precheck function is used to manipulate the ApplyContext with view
98 // changes that will cause the check to fail.
100 bool(test::jtx::Account const& a, test::jtx::Account const& b, ApplyContext& ac)>;
101
102 static FeatureBitset
104 {
105 return xrpl::test::jtx::testableAmendments() | fixCleanup3_1_3 | fixCleanup3_2_0;
106 }
107
110 {
111 return {*this, test::jtx::envconfig(), features, nullptr, beast::Severity::Disabled};
112 }
113
131 enum class TxAccount : int { None = 0, A1, A2 };
132 void
134 std::vector<std::string> const& expectLogs,
135 Precheck const& precheck,
136 XRPAmount fee = XRPAmount{},
137 STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
139 Preclose const& preclose = {},
140 TxAccount setTxAccount = TxAccount::None,
141 std::source_location const& loc = std::source_location::current())
142 {
145 expectLogs,
146 precheck,
147 fee,
148 tx,
149 ters,
150 preclose,
151 setTxAccount,
152 loc);
153 }
154
155 void
157 test::jtx::Env&& env,
158 std::vector<std::string> const& expectLogs,
159 Precheck const& precheck,
160 XRPAmount fee = XRPAmount{},
161 STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
163 Preclose const& preclose = {},
164 TxAccount setTxAccount = TxAccount::None,
165 std::source_location const& loc = std::source_location::current())
166 {
167 using namespace test::jtx;
168
169 Account const a1{"A1"};
170 Account const a2{"A2"};
171 env.fund(XRP(1000), a1, a2);
172 if (preclose)
173 BEAST_EXPECT(preclose(a1, a2, env));
174 env.close();
175
176 if (setTxAccount != TxAccount::None)
177 tx.setAccountID(sfAccount, setTxAccount == TxAccount::A1 ? a1.id() : a2.id());
178
179 doInvariantCheck(std::move(env), a1, a2, expectLogs, precheck, fee, tx, ters, loc);
180 }
181
182 void
184 // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
185 test::jtx::Env&& env,
186 test::jtx::Account const& a1,
187 test::jtx::Account const& a2,
188 std::vector<std::string> const& expectLogs,
189 Precheck const& precheck,
190 XRPAmount fee = XRPAmount{},
191 STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
193 std::source_location const& loc = std::source_location::current())
194 {
195 using namespace test::jtx;
196
197 OpenView ov{*env.current()};
198 test::StreamSink sink{beast::Severity::Warning};
199 beast::Journal const jlog{sink};
200 ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
201
202 // Invariants normally run in the Transaction's "apply" (operator()) context, and can always
203 // access global Rules.
204 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
205
206 BEAST_EXPECT(precheck(a1, a2, ac));
207
208 auto transactor = makeTransactor(ac);
209 if (!BEAST_EXPECT(transactor))
210 return;
211
212 // invoke check twice to cover tec and tef cases
213 if (!BEAST_EXPECT(ters.size() == 2))
214 return;
215
216 TER terActual = tesSUCCESS;
217 for (TER const& terExpect : ters)
218 {
219 terActual = transactor->checkInvariants(terActual, fee);
220 expect(
221 terExpect == terActual,
222 "expected: " + transToken(terExpect) + " got: " + transToken(terActual),
223 loc.file_name(),
224 loc.line());
225 auto const messages = sink.messages().str();
226
227 if (!isTesSuccess(terActual))
228 {
229 expect(
230 messages.starts_with("Invariant failed:") ||
231 messages.starts_with("Transaction caused an exception"),
232 messages,
233 loc.file_name(),
234 loc.line());
235 }
236
237 // std::cerr << messages << '\n';
238 for (auto const& m : expectLogs)
239 {
240 expect(messages.contains(m), m, loc.file_name(), loc.line());
241 }
242 }
243 }
244
245 void
247 {
248 using namespace test::jtx;
249 testcase << "XRP created";
251 {{"XRP net change was positive: 500"}},
252 [](Account const& a1, Account const&, ApplyContext& ac) {
253 // put a single account in the view and "manufacture" some XRP
254 auto const sle = ac.view().peek(keylet::account(a1.id()));
255 if (!sle)
256 return false;
257 auto amt = sle->getFieldAmount(sfBalance);
258 sle->setFieldAmount(sfBalance, amt + STAmount{500});
259 ac.view().update(sle);
260 return true;
261 });
262 }
263
264 void
266 {
267 using namespace test::jtx;
268 testcase << "account root removed";
269
270 // An account was deleted, but not by an AccountDelete transaction.
272 {{"an account root was deleted"}},
273 [](Account const& a1, Account const&, ApplyContext& ac) {
274 // remove an account from the view
275 auto sle = ac.view().peek(keylet::account(a1.id()));
276 if (!sle)
277 return false;
278 // Clear the balance so the "account deletion left behind a
279 // non-zero balance" check doesn't trip earlier than the desired
280 // check.
281 sle->at(sfBalance) = beast::kZero;
282 ac.view().erase(sle);
283 return true;
284 });
285
286 // Successful AccountDelete transaction that didn't delete an account.
287 //
288 // Note that this is a case where a second invocation of the invariant
289 // checker returns a tecINVARIANT_FAILED, not a tefINVARIANT_FAILED.
290 // After a discussion with the team, we believe that's okay.
292 {{"account deletion succeeded without deleting an account"}},
293 [](Account const&, Account const&, ApplyContext& ac) { return true; },
294 XRPAmount{},
295 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}},
297
298 // Successful AccountDelete that deleted more than one account.
300 {{"account deletion succeeded but deleted multiple accounts"}},
301 [](Account const& a1, Account const& a2, ApplyContext& ac) {
302 // remove two accounts from the view
303 auto sleA1 = ac.view().peek(keylet::account(a1.id()));
304 auto sleA2 = ac.view().peek(keylet::account(a2.id()));
305 if (!sleA1 || !sleA2)
306 return false;
307 // Clear the balance so the "account deletion left behind a
308 // non-zero balance" check doesn't trip earlier than the desired
309 // check.
310 sleA1->at(sfBalance) = beast::kZero;
311 sleA2->at(sfBalance) = beast::kZero;
312 ac.view().erase(sleA1);
313 ac.view().erase(sleA2);
314 return true;
315 },
316 XRPAmount{},
317 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
318 }
319
320 void
322 {
323 using namespace test::jtx;
324 testcase << "account root deletion left artifact";
325
327 {{"account deletion left behind a non-zero balance"}},
328 // NOLINTNEXTLINE(readability-identifier-naming)
329 [&](Account const& A1, Account const& A2, ApplyContext& ac) {
330 // A1 has a balance. Delete A1
331 auto const a1 = A1.id();
332 auto const sleA1 = ac.view().peek(keylet::account(a1));
333 if (!sleA1)
334 return false;
335 if (!BEAST_EXPECT(*sleA1->at(sfBalance) != beast::kZero))
336 return false;
337
338 ac.view().erase(sleA1);
339
340 return true;
341 },
342 XRPAmount{},
343 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
344
346 {{"account deletion left behind a non-zero owner count"}},
347 // NOLINTNEXTLINE(readability-identifier-naming)
348 [&](Account const& A1, Account const& A2, ApplyContext& ac) {
349 // Increment A1's owner count, then delete A1
350 auto const a1 = A1.id();
351 auto const sleA1 = ac.view().peek(keylet::account(a1));
352 if (!sleA1)
353 return false;
354 // Clear the balance so the "account deletion left behind a
355 // non-zero balance" check doesn't trip earlier than the desired
356 // check.
357 sleA1->at(sfBalance) = beast::kZero;
358 BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0);
359 increaseOwnerCount(ac.view(), sleA1, {}, 1, ac.journal);
360
361 ac.view().erase(sleA1);
362
363 return true;
364 },
365 XRPAmount{},
366 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
367
369 {{"account deletion left behind a sponsorship field"}},
370 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
371 auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
372 if (!sleA1)
373 return false;
374 sleA1->at(sfBalance) = beast::kZero;
375 sleA1->setFieldU32(sfSponsoredOwnerCount, 1);
376
377 ac.view().erase(sleA1);
378
379 return true;
380 },
381 XRPAmount{},
382 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
383
385 {{"account deletion left behind a sponsorship field"}},
386 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
387 auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
388 if (!sleA1)
389 return false;
390 sleA1->at(sfBalance) = beast::kZero;
391 sleA1->setFieldU32(sfSponsoringOwnerCount, 1);
392
393 ac.view().erase(sleA1);
394
395 return true;
396 },
397 XRPAmount{},
398 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
399
401 {{"account deletion left behind a sponsorship field"}},
402 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
403 auto const a1Id = a1.id();
404 auto const sleA1 = ac.view().peek(keylet::account(a1Id));
405 if (!sleA1)
406 return false;
407 sleA1->at(sfBalance) = beast::kZero;
408 sleA1->setFieldU32(sfSponsoringAccountCount, 1);
409
410 ac.view().erase(sleA1);
411
412 return true;
413 },
414 XRPAmount{},
415 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
416
418 {{"account deletion left behind a sponsorship field"}},
419 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
420 auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
421 if (!sleA1)
422 return false;
423 sleA1->at(sfBalance) = beast::kZero;
424 sleA1->setAccountID(sfSponsor, a2.id());
425
426 ac.view().erase(sleA1);
427
428 return true;
429 },
430 XRPAmount{},
431 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
432
434 Env{*this, FeatureBitset{featureSponsor}},
435 {{"account deletion left behind a sponsorship field"}},
436 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
437 auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
438 if (!sleA1)
439 return false;
440 sleA1->at(sfBalance) = beast::kZero;
441 sleA1->setAccountID(sfSponsor, a2.id());
442
443 ac.view().erase(sleA1);
444
445 return true;
446 },
447 XRPAmount{},
448 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
449
450 for (auto const& [keyletfunc, type, includeInTests] : kDirectAccountKeylets)
451 {
452 if (!includeInTests)
453 continue;
454
455 using namespace std::string_literals;
456
458 {{"account deletion left behind a "s + type.cStr() + " object"}},
459 // NOLINTNEXTLINE(readability-identifier-naming)
460 [&](Account const& A1, Account const& A2, ApplyContext& ac) {
461 // Add an object to the ledger for account A1, then delete
462 // A1
463 auto const a1 = A1.id();
464 auto sleA1 = ac.view().peek(keylet::account(a1));
465 if (!sleA1)
466 return false;
467
468 auto const key = std::invoke(keyletfunc, a1);
469 auto const newSLE = std::make_shared<SLE>(key);
470 ac.view().insert(newSLE);
471 // Clear the balance so the "account deletion left behind a
472 // non-zero balance" check doesn't trip earlier than the
473 // desired check.
474 sleA1->at(sfBalance) = beast::kZero;
475 ac.view().erase(sleA1);
476
477 return true;
478 },
479 XRPAmount{},
480 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
481 }
482
483 // NFT special case
485 {{"account deletion left behind a NFTokenPage object"}},
486 [&](Account const& a1, Account const&, ApplyContext& ac) {
487 // remove an account from the view
488 auto sle = ac.view().peek(keylet::account(a1.id()));
489 if (!sle)
490 return false;
491 // Clear the balance so the "account deletion left behind a
492 // non-zero balance" check doesn't trip earlier than the desired
493 // check.
494 sle->at(sfBalance) = beast::kZero;
495 sle->at(sfOwnerCount) = 0;
496 ac.view().erase(sle);
497 return true;
498 },
499 XRPAmount{},
500 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}},
502 [&](Account const& a1, Account const&, Env& env) {
503 // Preclose callback to mint the NFT which will be deleted in
504 // the Precheck callback above.
505 env(token::mint(a1));
506
507 return true;
508 });
509
510 // AMM special cases
511 AccountID ammAcctID;
512 uint256 ammKey;
513 Issue ammIssue;
515 {{"account deletion left behind a DirectoryNode object"}},
516 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
517 // Delete the AMM account without cleaning up the directory or
518 // deleting the AMM object
519 auto sle = ac.view().peek(keylet::account(ammAcctID));
520 if (!sle)
521 return false;
522
523 BEAST_EXPECT(sle->at(~sfAMMID));
524 BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
525
526 // Clear the balance so the "account deletion left behind a
527 // non-zero balance" check doesn't trip earlier than the desired
528 // check.
529 sle->at(sfBalance) = beast::kZero;
530 sle->at(sfOwnerCount) = 0;
531 ac.view().erase(sle);
532
533 return true;
534 },
535 XRPAmount{},
536 STTx{ttAMM_WITHDRAW, [](STObject& tx) {}},
538 [&](Account const& a1, Account const& a2, Env& env) {
539 // Preclose callback to create the AMM which will be partially
540 // deleted in the Precheck callback above.
541 AMM const amm(env, a1, XRP(100), a1["USD"](50));
542 ammAcctID = amm.ammAccount();
543 ammKey = amm.ammID();
544 ammIssue = amm.lptIssue();
545 return true;
546 });
548 {{"account deletion left behind a AMM object"}},
549 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
550 // Delete all the AMM's trust lines, remove the AMM from the AMM
551 // account's directory (this deletes the directory), and delete
552 // the AMM account. Do not delete the AMM object.
553 auto sle = ac.view().peek(keylet::account(ammAcctID));
554 if (!sle)
555 return false;
556
557 BEAST_EXPECT(sle->at(~sfAMMID));
558 BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
559
560 for (auto const& trustKeylet :
561 {keylet::trustLine(ammAcctID, a1["USD"]), keylet::trustLine(a1, ammIssue)})
562 {
563 auto const line = ac.view().peek(trustKeylet);
564 if (!line)
565 {
566 return false;
567 }
568
569 STAmount const lowLimit = line->at(sfLowLimit);
570 STAmount const highLimit = line->at(sfHighLimit);
571 BEAST_EXPECT(
573 ac.view(),
574 line,
575 lowLimit.getIssuer(),
576 highLimit.getIssuer(),
577 ac.journal) == tesSUCCESS);
578 }
579
580 auto const ammSle = ac.view().peek(keylet::amm(ammKey));
581 if (!BEAST_EXPECT(ammSle))
582 return false;
583 auto const ownerDirKeylet = keylet::ownerDir(ammAcctID);
584
585 BEAST_EXPECT(
586 ac.view().dirRemove(ownerDirKeylet, ammSle->at(sfOwnerNode), ammKey, false));
587 BEAST_EXPECT(
588 !ac.view().exists(ownerDirKeylet) || ac.view().emptyDirDelete(ownerDirKeylet));
589
590 // Clear the balance so the "account deletion left behind a
591 // non-zero balance" check doesn't trip earlier than the desired
592 // check.
593 sle->at(sfBalance) = beast::kZero;
594 sle->at(sfOwnerCount) = 0;
595 ac.view().erase(sle);
596
597 return true;
598 },
599 XRPAmount{},
600 STTx{ttAMM_WITHDRAW, [](STObject& tx) {}},
602 [&](Account const& a1, Account const& a2, Env& env) {
603 // Preclose callback to create the AMM which will be partially
604 // deleted in the Precheck callback above.
605 AMM const amm(env, a1, XRP(100), a1["USD"](50));
606 ammAcctID = amm.ammAccount();
607 ammKey = amm.ammID();
608 ammIssue = amm.lptIssue();
609 return true;
610 });
611 }
612
613 void
615 {
616 using namespace test::jtx;
617 testcase << "ledger entry types don't match";
619 {{"ledger entry type mismatch"}, {"XRP net change of -1000000000 doesn't match fee 0"}},
620 [](Account const& a1, Account const&, ApplyContext& ac) {
621 // replace an entry in the table with an SLE of a different type
622 auto const sle = ac.view().peek(keylet::account(a1.id()));
623 if (!sle)
624 return false;
625 auto const sleNew = std::make_shared<SLE>(ltTICKET, sle->key());
626 ac.rawView().rawReplace(sleNew);
627 return true;
628 });
629
631 {{"invalid ledger entry type added"}},
632 [](Account const& a1, Account const&, ApplyContext& ac) {
633 // add an entry in the table with an SLE of an invalid type
634 auto const sle = ac.view().peek(keylet::account(a1.id()));
635 if (!sle)
636 return false;
637
638 // make a dummy escrow ledger entry, then change the type to an
639 // unsupported value so that the valid type invariant check
640 // will fail.
641 auto const sleNew = std::make_shared<SLE>(
642 keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
643
644 // We don't use ltNICKNAME directly since it's marked deprecated
645 // to prevent accidental use elsewhere.
646 sleNew->type_ = static_cast<LedgerEntryType>('n');
647 ac.view().insert(sleNew);
648 return true;
649 });
650 }
651
652 void
654 {
655 using namespace test::jtx;
656 testcase << "trust lines with XRP not allowed";
658 {{"an XRP trust line was created"}},
659 [](Account const& a1, Account const& a2, ApplyContext& ac) {
660 // create simple trust SLE with xrp currency
661 auto const sleNew =
663 ac.view().insert(sleNew);
664 return true;
665 });
666 }
667
668 void
670 {
671 using namespace test::jtx;
672 testcase << "trust lines with deep freeze flag without freeze "
673 "not allowed";
675 {{"a trust line with deep freeze flag without normal freeze was "
676 "created"}},
677 [](Account const& a1, Account const& a2, ApplyContext& ac) {
678 auto const sleNew =
679 std::make_shared<SLE>(keylet::trustLine(a1, a2, a1["USD"].currency));
680 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
681 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
682
683 std::uint32_t uFlags = 0u;
684 uFlags |= lsfLowDeepFreeze;
685 sleNew->setFieldU32(sfFlags, uFlags);
686 ac.view().insert(sleNew);
687 return true;
688 });
689
691 {{"a trust line with deep freeze flag without normal freeze was "
692 "created"}},
693 [](Account const& a1, Account const& a2, ApplyContext& ac) {
694 auto const sleNew =
695 std::make_shared<SLE>(keylet::trustLine(a1, a2, a1["USD"].currency));
696 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
697 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
698 std::uint32_t uFlags = 0u;
699 uFlags |= lsfHighDeepFreeze;
700 sleNew->setFieldU32(sfFlags, uFlags);
701 ac.view().insert(sleNew);
702 return true;
703 });
704
706 {{"a trust line with deep freeze flag without normal freeze was "
707 "created"}},
708 [](Account const& a1, Account const& a2, ApplyContext& ac) {
709 auto const sleNew =
710 std::make_shared<SLE>(keylet::trustLine(a1, a2, a1["USD"].currency));
711 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
712 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
713 std::uint32_t uFlags = 0u;
714 uFlags |= lsfLowDeepFreeze | lsfHighDeepFreeze;
715 sleNew->setFieldU32(sfFlags, uFlags);
716 ac.view().insert(sleNew);
717 return true;
718 });
719
721 {{"a trust line with deep freeze flag without normal freeze was "
722 "created"}},
723 [](Account const& a1, Account const& a2, ApplyContext& ac) {
724 auto const sleNew =
725 std::make_shared<SLE>(keylet::trustLine(a1, a2, a1["USD"].currency));
726 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
727 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
728 std::uint32_t uFlags = 0u;
729 uFlags |= lsfLowDeepFreeze | lsfHighFreeze;
730 sleNew->setFieldU32(sfFlags, uFlags);
731 ac.view().insert(sleNew);
732 return true;
733 });
734
736 {{"a trust line with deep freeze flag without normal freeze was "
737 "created"}},
738 [](Account const& a1, Account const& a2, ApplyContext& ac) {
739 auto const sleNew =
740 std::make_shared<SLE>(keylet::trustLine(a1, a2, a1["USD"].currency));
741 sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
742 sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
743 std::uint32_t uFlags = 0u;
744 uFlags |= lsfLowFreeze | lsfHighDeepFreeze;
745 sleNew->setFieldU32(sfFlags, uFlags);
746 ac.view().insert(sleNew);
747 return true;
748 });
749 }
750
751 void
753 {
754 using namespace test::jtx;
755 testcase << "transfers when frozen";
756
757 Account const g1{"G1"};
758 // Helper function to establish the trustlines
759 auto const createTrustlines = [&](Account const& a1, Account const& a2, Env& env) {
760 // Preclose callback to establish trust lines with gateway
761 env.fund(XRP(1000), g1);
762
763 env.trust(g1["USD"](10000), a1);
764 env.trust(g1["USD"](10000), a2);
765 env.close();
766
767 env(pay(g1, a1, g1["USD"](1000)));
768 env(pay(g1, a2, g1["USD"](1000)));
769 env.close();
770
771 return true;
772 };
773
774 auto const a1FrozenByIssuer = [&](Account const& a1, Account const& a2, Env& env) {
775 createTrustlines(a1, a2, env);
776 env(trust(g1, a1["USD"](10000), tfSetFreeze));
777 env.close();
778
779 return true;
780 };
781
782 auto const a1DeepFrozenByIssuer = [&](Account const& a1, Account const& a2, Env& env) {
783 a1FrozenByIssuer(a1, a2, env);
784 env(trust(g1, a1["USD"](10000), tfSetDeepFreeze));
785 env.close();
786
787 return true;
788 };
789
790 auto const changeBalances = [&](Account const& a1,
791 Account const& a2,
792 ApplyContext& ac,
793 int a1Balance,
794 int a2Balance) {
795 auto const sleA1 = ac.view().peek(keylet::trustLine(a1, g1["USD"]));
796 auto const sleA2 = ac.view().peek(keylet::trustLine(a2, g1["USD"]));
797
798 sleA1->setFieldAmount(sfBalance, g1["USD"](a1Balance));
799 sleA2->setFieldAmount(sfBalance, g1["USD"](a2Balance));
800
801 ac.view().update(sleA1);
802 ac.view().update(sleA2);
803 };
804
805 // test: imitating frozen A1 making a payment to A2.
807 {{"Attempting to move frozen funds"}},
808 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
809 changeBalances(a1, a2, ac, -900, -1100);
810 return true;
811 },
812 XRPAmount{},
813 STTx{ttPAYMENT, [](STObject& tx) {}},
815 a1FrozenByIssuer);
816
817 // test: imitating deep frozen A1 making a payment to A2.
819 {{"Attempting to move frozen funds"}},
820 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
821 changeBalances(a1, a2, ac, -900, -1100);
822 return true;
823 },
824 XRPAmount{},
825 STTx{ttPAYMENT, [](STObject& tx) {}},
827 a1DeepFrozenByIssuer);
828
829 // test: imitating A2 making a payment to deep frozen A1.
831 {{"Attempting to move frozen funds"}},
832 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
833 changeBalances(a1, a2, ac, -1100, -900);
834 return true;
835 },
836 XRPAmount{},
837 STTx{ttPAYMENT, [](STObject& tx) {}},
839 a1DeepFrozenByIssuer);
840 }
841
842 void
844 {
845 using namespace test::jtx;
846 testcase << "XRP balance checks";
847
849 {{"Cannot return non-native STAmount as XRPAmount"}},
850 [](Account const& a1, Account const& a2, ApplyContext& ac) {
851 // non-native balance
852 auto const sle = ac.view().peek(keylet::account(a1.id()));
853 if (!sle)
854 return false;
855 STAmount const nonNative(a2["USD"](51));
856 sle->setFieldAmount(sfBalance, nonNative);
857 ac.view().update(sle);
858 return true;
859 });
860
862 {{"incorrect account XRP balance"}, {"XRP net change was positive: 99999999000000001"}},
863 [this](Account const& a1, Account const&, ApplyContext& ac) {
864 // balance exceeds genesis amount
865 auto const sle = ac.view().peek(keylet::account(a1.id()));
866 if (!sle)
867 return false;
868 // Use `drops(1)` to bypass a call to STAmount::canonicalize
869 // with an invalid value
870 sle->setFieldAmount(sfBalance, kInitialXrp + drops(1));
871 BEAST_EXPECT(!sle->getFieldAmount(sfBalance).negative());
872 ac.view().update(sle);
873 return true;
874 });
875
877 {{"incorrect account XRP balance"},
878 {"XRP net change of -1000000001 doesn't match fee 0"}},
879 [this](Account const& a1, Account const&, ApplyContext& ac) {
880 // balance is negative
881 auto const sle = ac.view().peek(keylet::account(a1.id()));
882 if (!sle)
883 return false;
884 sle->setFieldAmount(sfBalance, STAmount{1, true});
885 BEAST_EXPECT(sle->getFieldAmount(sfBalance).negative());
886 ac.view().update(sle);
887 return true;
888 });
889 }
890
891 void
893 {
894 using namespace test::jtx;
895 using namespace std::string_literals;
896 testcase << "Transaction fee checks";
897
899 {{"fee paid was negative: -1"}, {"XRP net change of 0 doesn't match fee -1"}},
900 [](Account const&, Account const&, ApplyContext&) { return true; },
901 XRPAmount{-1});
902
904 {{"fee paid exceeds system limit: "s + to_string(kInitialXrp)},
905 {"XRP net change of 0 doesn't match fee "s + to_string(kInitialXrp)}},
906 [](Account const&, Account const&, ApplyContext&) { return true; },
908
910 {{"fee paid is 20 exceeds fee specified in transaction."},
911 {"XRP net change of 0 doesn't match fee 20"}},
912 [](Account const&, Account const&, ApplyContext&) { return true; },
913 XRPAmount{20},
914 STTx{ttACCOUNT_SET, [](STObject& tx) { tx.setFieldAmount(sfFee, XRPAmount{10}); }});
915 }
916
917 void
919 {
920 using namespace test::jtx;
921 testcase << "no bad offers";
922
924 {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
925 // offer with negative takerpays
926 auto const sle = ac.view().peek(keylet::account(a1.id()));
927 if (!sle)
928 return false;
929 auto sleNew = std::make_shared<SLE>(
930 keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
931 sleNew->setAccountID(sfAccount, a1.id());
932 sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
933 sleNew->setFieldAmount(sfTakerPays, XRP(-1));
934 ac.view().insert(sleNew);
935 return true;
936 });
937
939 {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
940 // offer with negative takergets
941 auto const sle = ac.view().peek(keylet::account(a1.id()));
942 if (!sle)
943 return false;
944 auto sleNew = std::make_shared<SLE>(
945 keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
946 sleNew->setAccountID(sfAccount, a1.id());
947 sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
948 sleNew->setFieldAmount(sfTakerPays, a1["USD"](10));
949 sleNew->setFieldAmount(sfTakerGets, XRP(-1));
950 ac.view().insert(sleNew);
951 return true;
952 });
953
955 {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
956 // offer XRP to XRP
957 auto const sle = ac.view().peek(keylet::account(a1.id()));
958 if (!sle)
959 return false;
960 auto sleNew = std::make_shared<SLE>(
961 keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
962 sleNew->setAccountID(sfAccount, a1.id());
963 sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
964 sleNew->setFieldAmount(sfTakerPays, XRP(10));
965 sleNew->setFieldAmount(sfTakerGets, XRP(11));
966 ac.view().insert(sleNew);
967 return true;
968 });
969 }
970
971 void
973 {
974 using namespace test::jtx;
975 testcase << "no zero escrow";
976
978 {{"XRP net change of -1000000 doesn't match fee 0"},
979 {"escrow specifies invalid amount"}},
980 [](Account const& a1, Account const&, ApplyContext& ac) {
981 // escrow with negative amount
982 auto const sle = ac.view().peek(keylet::account(a1.id()));
983 if (!sle)
984 return false;
985 auto sleNew = std::make_shared<SLE>(
986 keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
987 sleNew->setFieldAmount(sfAmount, XRP(-1));
988 ac.view().insert(sleNew);
989 return true;
990 });
991
993 {{"XRP net change was positive: 100000000000000001"},
994 {"escrow specifies invalid amount"}},
995 [](Account const& a1, Account const&, ApplyContext& ac) {
996 // escrow with too-large amount
997 auto const sle = ac.view().peek(keylet::account(a1.id()));
998 if (!sle)
999 return false;
1000 auto sleNew = std::make_shared<SLE>(
1001 keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
1002 // Use `drops(1)` to bypass a call to STAmount::canonicalize
1003 // with an invalid value
1004 sleNew->setFieldAmount(sfAmount, kInitialXrp + drops(1));
1005 ac.view().insert(sleNew);
1006 return true;
1007 });
1008
1009 // IOU < 0
1011 {{"escrow specifies invalid amount"}},
1012 [](Account const& a1, Account const&, ApplyContext& ac) {
1013 // escrow with too-little iou
1014 auto const sle = ac.view().peek(keylet::account(a1.id()));
1015 if (!sle)
1016 return false;
1017 auto sleNew = std::make_shared<SLE>(
1018 keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
1019
1020 Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
1021 STAmount const amt(usd, -1);
1022 sleNew->setFieldAmount(sfAmount, amt);
1023 ac.view().insert(sleNew);
1024 return true;
1025 });
1026
1027 // IOU bad currency
1029 {{"escrow specifies invalid amount"}},
1030 [](Account const& a1, Account const&, ApplyContext& ac) {
1031 // escrow with bad iou currency
1032 auto const sle = ac.view().peek(keylet::account(a1.id()));
1033 if (!sle)
1034 return false;
1035 auto sleNew = std::make_shared<SLE>(
1036 keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
1037
1038 Issue const bad{badCurrency(), AccountID(0x4985601)};
1039 STAmount const amt(bad, 1);
1040 sleNew->setFieldAmount(sfAmount, amt);
1041 ac.view().insert(sleNew);
1042 return true;
1043 });
1044
1045 // MPT < 0
1047 {{"escrow specifies invalid amount"}},
1048 [](Account const& a1, Account const&, ApplyContext& ac) {
1049 // escrow with too-little mpt
1050 auto const sle = ac.view().peek(keylet::account(a1.id()));
1051 if (!sle)
1052 return false;
1053 auto sleNew = std::make_shared<SLE>(
1054 keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
1055
1056 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
1057 STAmount const amt(mpt, -1);
1058 sleNew->setFieldAmount(sfAmount, amt);
1059 ac.view().insert(sleNew);
1060 return true;
1061 });
1062
1063 // MPT OutstandingAmount < 0
1065 {{"escrow specifies invalid amount"}},
1066 [](Account const& a1, Account const&, ApplyContext& ac) {
1067 // mptissuance outstanding is negative
1068 auto const sle = ac.view().peek(keylet::account(a1.id()));
1069 if (!sle)
1070 return false;
1071
1072 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
1074 sleNew->setFieldU64(sfOutstandingAmount, -1);
1075 ac.view().insert(sleNew);
1076 return true;
1077 });
1078
1079 // MPT LockedAmount < 0
1081 {{"escrow specifies invalid amount"}},
1082 [](Account const& a1, Account const&, ApplyContext& ac) {
1083 // mptissuance locked is less than locked
1084 auto const sle = ac.view().peek(keylet::account(a1.id()));
1085 if (!sle)
1086 return false;
1087
1088 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
1090 sleNew->setFieldU64(sfLockedAmount, -1);
1091 ac.view().insert(sleNew);
1092 return true;
1093 });
1094
1095 // MPT OutstandingAmount < LockedAmount
1097 {{"escrow specifies invalid amount"}},
1098 [](Account const& a1, Account const&, ApplyContext& ac) {
1099 // mptissuance outstanding is less than locked
1100 auto const sle = ac.view().peek(keylet::account(a1.id()));
1101 if (!sle)
1102 return false;
1103
1104 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
1106 sleNew->setFieldU64(sfOutstandingAmount, 1);
1107 sleNew->setFieldU64(sfLockedAmount, 10);
1108 ac.view().insert(sleNew);
1109 return true;
1110 });
1111
1112 // MPT MPTAmount < 0
1114 {{"escrow specifies invalid amount"}},
1115 [](Account const& a1, Account const&, ApplyContext& ac) {
1116 // mptoken amount is negative
1117 auto const sle = ac.view().peek(keylet::account(a1.id()));
1118 if (!sle)
1119 return false;
1120
1121 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
1122 auto sleNew = std::make_shared<SLE>(keylet::mptoken(mpt.getMptID(), a1));
1123 sleNew->setFieldU64(sfMPTAmount, -1);
1124 ac.view().insert(sleNew);
1125 return true;
1126 });
1127
1128 // MPT LockedAmount < 0
1130 {{"escrow specifies invalid amount"}},
1131 [](Account const& a1, Account const&, ApplyContext& ac) {
1132 // mptoken locked amount is negative
1133 auto const sle = ac.view().peek(keylet::account(a1.id()));
1134 if (!sle)
1135 return false;
1136
1137 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
1138 auto sleNew = std::make_shared<SLE>(keylet::mptoken(mpt.getMptID(), a1));
1139 sleNew->setFieldU64(sfLockedAmount, -1);
1140 ac.view().insert(sleNew);
1141 return true;
1142 });
1143 }
1144
1145 void
1147 {
1148 using namespace test::jtx;
1149 testcase << "valid new account root";
1150
1152 {{"account root created illegally"}},
1153 [](Account const&, Account const&, ApplyContext& ac) {
1154 // Insert a new account root created by a non-payment into
1155 // the view.
1156 Account const a3{"A3"};
1157 Keylet const acctKeylet = keylet::account(a3);
1158 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1159 ac.view().insert(sleNew);
1160 return true;
1161 });
1162
1164 {{"multiple accounts created in a single transaction"}},
1165 [](Account const&, Account const&, ApplyContext& ac) {
1166 // Insert two new account roots into the view.
1167 {
1168 Account const a3{"A3"};
1169 Keylet const acctKeylet = keylet::account(a3);
1170 auto const sleA3 = std::make_shared<SLE>(acctKeylet);
1171 ac.view().insert(sleA3);
1172 }
1173 {
1174 Account const a4{"A4"};
1175 Keylet const acctKeylet = keylet::account(a4);
1176 auto const sleA4 = std::make_shared<SLE>(acctKeylet);
1177 ac.view().insert(sleA4);
1178 }
1179 return true;
1180 });
1181
1183 {{"account created with wrong starting sequence number"}},
1184 [](Account const&, Account const&, ApplyContext& ac) {
1185 // Insert a new account root with the wrong starting sequence.
1186 Account const a3{"A3"};
1187 Keylet const acctKeylet = keylet::account(a3);
1188 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1189 sleNew->setFieldU32(sfSequence, ac.view().seq() + 1);
1190 ac.view().insert(sleNew);
1191 return true;
1192 },
1193 XRPAmount{},
1194 STTx{ttPAYMENT, [](STObject& tx) {}});
1195
1197 {{"pseudo-account created by a wrong transaction type"}},
1198 [](Account const&, Account const&, ApplyContext& ac) {
1199 Account const a3{"A3"};
1200 Keylet const acctKeylet = keylet::account(a3);
1201 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1202 sleNew->setFieldU32(sfSequence, 0);
1203 sleNew->setFieldH256(sfAMMID, uint256(1));
1204 sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple);
1205 ac.view().insert(sleNew);
1206 return true;
1207 },
1208 XRPAmount{},
1209 STTx{ttPAYMENT, [](STObject& tx) {}});
1210
1212 {{"account created with wrong starting sequence number"}},
1213 [](Account const&, Account const&, ApplyContext& ac) {
1214 Account const a3{"A3"};
1215 Keylet const acctKeylet = keylet::account(a3);
1216 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1217 sleNew->setFieldU32(sfSequence, ac.view().seq());
1218 sleNew->setFieldH256(sfAMMID, uint256(1));
1219 sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
1220 ac.view().insert(sleNew);
1221 return true;
1222 },
1223 XRPAmount{},
1224 STTx{ttAMM_CREATE, [](STObject& tx) {}});
1225
1227 {{"pseudo-account created with wrong flags"}},
1228 [](Account const&, Account const&, ApplyContext& ac) {
1229 Account const a3{"A3"};
1230 Keylet const acctKeylet = keylet::account(a3);
1231 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1232 sleNew->setFieldU32(sfSequence, 0);
1233 sleNew->setFieldH256(sfAMMID, uint256(1));
1234 sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple);
1235 ac.view().insert(sleNew);
1236 return true;
1237 },
1238 XRPAmount{},
1239 STTx{ttVAULT_CREATE, [](STObject& tx) {}});
1240
1242 {{"pseudo-account created with wrong flags"}},
1243 [](Account const&, Account const&, ApplyContext& ac) {
1244 Account const a3{"A3"};
1245 Keylet const acctKeylet = keylet::account(a3);
1246 auto const sleNew = std::make_shared<SLE>(acctKeylet);
1247 sleNew->setFieldU32(sfSequence, 0);
1248 sleNew->setFieldH256(sfAMMID, uint256(1));
1249 sleNew->setFieldU32(
1250 sfFlags,
1251 lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth | lsfRequireDestTag);
1252 ac.view().insert(sleNew);
1253 return true;
1254 },
1255 XRPAmount{},
1256 STTx{ttAMM_CREATE, [](STObject& tx) {}});
1257 }
1258
1259 void
1261 {
1262 using namespace test::jtx;
1263 testcase << "NFTokenPage";
1264
1265 // lambda that returns an STArray of NFTokenIDs.
1266 uint256 const firstNFTID(
1267 "0000000000000000000000000000000000000001FFFFFFFFFFFFFFFF00000000");
1268 auto makeNFTokenIDs = [&firstNFTID](unsigned int nftCount) {
1269 SOTemplate const* nfTokenTemplate =
1271
1272 uint256 nftID(firstNFTID);
1273 STArray ret;
1274 for (int i = 0; i < nftCount; ++i)
1275 {
1276 STObject newNFToken(*nfTokenTemplate, sfNFToken, [&nftID](STObject& object) {
1277 object.setFieldH256(sfNFTokenID, nftID);
1278 });
1279 ret.pushBack(std::move(newNFToken));
1280 ++nftID;
1281 }
1282 return ret;
1283 };
1284
1286 {{"NFT page has invalid size"}},
1287 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1289 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(0));
1290
1291 ac.view().insert(nftPage);
1292 return true;
1293 });
1294
1296 {{"NFT page has invalid size"}},
1297 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1299 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(33));
1300
1301 ac.view().insert(nftPage);
1302 return true;
1303 });
1304
1306 {{"NFTs on page are not sorted"}},
1307 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1308 STArray nfTokens = makeNFTokenIDs(2);
1309 std::iter_swap(nfTokens.begin(), nfTokens.begin() + 1);
1310
1312 nftPage->setFieldArray(sfNFTokens, nfTokens);
1313
1314 ac.view().insert(nftPage);
1315 return true;
1316 });
1317
1319 {{"NFT contains empty URI"}},
1320 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1321 STArray nfTokens = makeNFTokenIDs(1);
1322 nfTokens[0].setFieldVL(sfURI, Blob{});
1323
1325 nftPage->setFieldArray(sfNFTokens, nfTokens);
1326
1327 ac.view().insert(nftPage);
1328 return true;
1329 });
1330
1332 {{"NFT page is improperly linked"}},
1333 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1335 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
1336 nftPage->setFieldH256(sfPreviousPageMin, keylet::nftokenPageMax(a1).key);
1337
1338 ac.view().insert(nftPage);
1339 return true;
1340 });
1341
1343 {{"NFT page is improperly linked"}},
1344 [&makeNFTokenIDs](Account const& a1, Account const& a2, ApplyContext& ac) {
1346 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
1347 nftPage->setFieldH256(sfPreviousPageMin, keylet::nftokenPageMin(a2).key);
1348
1349 ac.view().insert(nftPage);
1350 return true;
1351 });
1352
1354 {{"NFT page is improperly linked"}},
1355 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1357 nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
1358 nftPage->setFieldH256(sfNextPageMin, nftPage->key());
1359
1360 ac.view().insert(nftPage);
1361 return true;
1362 });
1363
1365 {{"NFT page is improperly linked"}},
1366 [&makeNFTokenIDs](Account const& a1, Account const& a2, ApplyContext& ac) {
1367 STArray nfTokens = makeNFTokenIDs(1);
1369 keylet::nftokenPageMax(a1), ++(nfTokens[0].getFieldH256(sfNFTokenID))));
1370 nftPage->setFieldArray(sfNFTokens, nfTokens);
1371 nftPage->setFieldH256(sfNextPageMin, keylet::nftokenPageMax(a2).key);
1372
1373 ac.view().insert(nftPage);
1374 return true;
1375 });
1376
1378 {{"NFT found in incorrect page"}},
1379 [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
1380 STArray nfTokens = makeNFTokenIDs(2);
1382 keylet::nftokenPageMax(a1), (nfTokens[1].getFieldH256(sfNFTokenID))));
1383 nftPage->setFieldArray(sfNFTokens, nfTokens);
1384
1385 ac.view().insert(nftPage);
1386 return true;
1387 });
1388 }
1389
1390 void
1392 {
1393 using namespace test::jtx;
1394
1395 bool const enforceAMMDelete = features[fixCleanup3_3_0];
1396 testcase << "AMM delete invariants" + std::string(enforceAMMDelete ? " fix" : "");
1397
1398 Env env(*this, features);
1399 Account const issuer{"issuer"};
1400 Issue const lptIssue{Currency(0x4c50540000000000), issuer.id()};
1401 STAmount const zeroLP{lptIssue, 0};
1402 STAmount const nonZeroLP{lptIssue, 1};
1403
1404 auto const makeAMM = [](STAmount const& lptBalance) {
1405 auto sleAMM = std::make_shared<SLE>(keylet::amm(uint256(1)));
1406 sleAMM->setFieldAmount(sfLPTokenBalance, lptBalance);
1407 return sleAMM;
1408 };
1409
1410 auto const checkInvariant = [&](TxType txType,
1411 TER result,
1412 std::optional<STAmount> const& deletedLPBalance,
1413 bool expected,
1414 std::string const& expectedLog) {
1416 beast::Journal const jlog{sink};
1417 ValidAMM invariant;
1418
1419 if (deletedLPBalance)
1420 invariant.visitEntry(true, makeAMM(*deletedLPBalance), nullptr);
1421
1422 bool const actual = invariant.finalize(
1423 STTx{txType, [](STObject&) {}}, result, XRPAmount{}, *env.current(), jlog);
1424
1425 BEAST_EXPECTS(actual == expected, "unexpected AMM delete invariant result");
1426 auto const messages = sink.messages().str();
1427 auto const expectedLogWhenEnforced = enforceAMMDelete ? expectedLog : "";
1428 if (!expectedLogWhenEnforced.empty())
1429 {
1430 BEAST_EXPECTS(messages.contains(expectedLogWhenEnforced), expectedLogWhenEnforced);
1431 }
1432 else
1433 {
1434 BEAST_EXPECTS(messages.empty(), messages);
1435 }
1436 };
1437
1438 checkInvariant(
1439 ttPAYMENT,
1440 tesSUCCESS,
1441 nonZeroLP,
1442 !enforceAMMDelete,
1443 "Invariant failed: AMM failed, unexpected AMM deletion by");
1444 checkInvariant(
1445 ttAMM_DELETE,
1446 tesSUCCESS,
1447 std::nullopt,
1448 !enforceAMMDelete,
1449 "Invariant failed: AMMDelete failed, AMM object remained on tesSUCCESS");
1450 checkInvariant(
1451 ttAMM_DELETE,
1452 tesSUCCESS,
1453 nonZeroLP,
1454 !enforceAMMDelete,
1455 "Invariant failed: AMMDelete failed, AMM object deleted with non-zero LP balance");
1456 checkInvariant(
1457 ttAMM_DELETE,
1459 zeroLP,
1460 !enforceAMMDelete,
1461 "Invariant failed: AMMDelete failed, AMM object deleted when result is not tesSUCCESS");
1462
1463 checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, nonZeroLP, true, "");
1464 checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, nonZeroLP, true, "");
1465
1466 checkInvariant(ttAMM_DELETE, tesSUCCESS, zeroLP, true, "");
1467 checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, zeroLP, true, "");
1468 checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, zeroLP, true, "");
1469 }
1470
1471 static SLE::pointer
1473 ApplyContext& ac,
1474 test::jtx::Account const& a1,
1475 test::jtx::Account const& a2,
1476 std::uint32_t numCreds = 2,
1477 std::uint32_t seq = 10)
1478 {
1479 Keylet const pdKeylet = keylet::permissionedDomain(a1.id(), SeqProxy::rawSequence(seq));
1480 auto sle = std::make_shared<SLE>(pdKeylet);
1481
1482 sle->setAccountID(sfOwner, a1);
1483 sle->setFieldU32(sfSequence, seq);
1484
1485 if (numCreds != 0u)
1486 {
1487 // This array is sorted naturally, but if you are going to change
1488 // this behavior, don't forget to use credentials::makeSorted
1489 STArray credentials(sfAcceptedCredentials, numCreds);
1490 for (std::size_t n = 0; n < numCreds; ++n)
1491 {
1492 auto cred = STObject::makeInnerObject(sfCredential);
1493 cred.setAccountID(sfIssuer, a2);
1494 auto credType = "cred_type" + std::to_string(n);
1495 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
1496 credentials.pushBack(std::move(cred));
1497 }
1498 sle->setFieldArray(sfAcceptedCredentials, credentials);
1499 }
1500
1501 ac.view().insert(sle);
1502 return sle;
1503 };
1504
1505 void
1507 {
1508 using namespace test::jtx;
1509
1510 bool const fixEnabled = features[fixCleanup3_1_3];
1513
1514 testcase << "PermissionedDomain" + std::string(fixEnabled ? " fix" : "");
1515
1517 makeEnv(features),
1518 {{"permissioned domain with no rules."}},
1519 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1520 return createPermissionedDomain(ac, a1, a2, 0).get();
1521 },
1522 XRPAmount{},
1523 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1524 fixEnabled ? failTers : badTers);
1525
1526 testcase << "PermissionedDomain 2";
1527
1528 static constexpr auto kTooBig = kMaxPermissionedDomainCredentialsArraySize + 1;
1530 makeEnv(features),
1531 {{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
1532 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1533 return !!createPermissionedDomain(ac, a1, a2, kTooBig);
1534 },
1535 XRPAmount{},
1536 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1537 fixEnabled ? failTers : badTers);
1538
1539 testcase << "PermissionedDomain 3";
1541 makeEnv(features),
1542 {{"permissioned domain credentials aren't sorted"}},
1543 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1544 auto slePd = createPermissionedDomain(ac, a1, a2, 0);
1545
1546 STArray credentials(sfAcceptedCredentials, 2);
1547 for (std::size_t n = 0; n < 2; ++n)
1548 {
1549 auto cred = STObject::makeInnerObject(sfCredential);
1550 cred.setAccountID(sfIssuer, a2);
1551 auto credType = std::string("cred_type") + std::to_string(9 - n);
1552 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
1553 credentials.pushBack(std::move(cred));
1554 }
1555 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1556 ac.view().update(slePd);
1557 return true;
1558 },
1559 XRPAmount{},
1560 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1561 fixEnabled ? failTers : badTers);
1562
1563 testcase << "PermissionedDomain 4";
1565 makeEnv(features),
1566 {{"permissioned domain credentials aren't unique"}},
1567 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1568 auto slePd = createPermissionedDomain(ac, a1, a2, 0);
1569
1570 STArray credentials(sfAcceptedCredentials, 2);
1571 for (std::size_t n = 0; n < 2; ++n)
1572 {
1573 auto cred = STObject::makeInnerObject(sfCredential);
1574 cred.setAccountID(sfIssuer, a2);
1575 cred.setFieldVL(sfCredentialType, Slice("cred_type", 9));
1576 credentials.pushBack(std::move(cred));
1577 }
1578 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1579 ac.view().update(slePd);
1580 return true;
1581 },
1582 XRPAmount{},
1583 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1584 fixEnabled ? failTers : badTers);
1585
1586 testcase << "PermissionedDomain Set 1";
1588 makeEnv(features),
1589 {{"permissioned domain with no rules."}},
1590 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1591 // create PD
1592 auto slePd = createPermissionedDomain(ac, a1, a2);
1593
1594 // update PD with empty rules
1595 {
1596 STArray const credentials(sfAcceptedCredentials, 2);
1597 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1598 ac.view().update(slePd);
1599 }
1600
1601 return true;
1602 },
1603 XRPAmount{},
1604 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1605 fixEnabled ? failTers : badTers);
1606
1607 testcase << "PermissionedDomain Set 2";
1609 makeEnv(features),
1610 {{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
1611 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1612 // create PD
1613 auto slePd = createPermissionedDomain(ac, a1, a2);
1614
1615 // update PD
1616 {
1617 STArray credentials(sfAcceptedCredentials, kTooBig);
1618
1619 for (std::size_t n = 0; n < kTooBig; ++n)
1620 {
1621 auto cred = STObject::makeInnerObject(sfCredential);
1622 cred.setAccountID(sfIssuer, a2);
1623 auto credType = "cred_type2" + std::to_string(n);
1624 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
1625 credentials.pushBack(std::move(cred));
1626 }
1627
1628 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1629 ac.view().update(slePd);
1630 }
1631
1632 return true;
1633 },
1634 XRPAmount{},
1635 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1636 fixEnabled ? failTers : badTers);
1637
1638 testcase << "PermissionedDomain Set 3";
1640 makeEnv(features),
1641 {{"permissioned domain credentials aren't sorted"}},
1642 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1643 // create PD
1644 auto slePd = createPermissionedDomain(ac, a1, a2);
1645
1646 // update PD
1647 {
1648 STArray credentials(sfAcceptedCredentials, 2);
1649 for (std::size_t n = 0; n < 2; ++n)
1650 {
1651 auto cred = STObject::makeInnerObject(sfCredential);
1652 cred.setAccountID(sfIssuer, a2);
1653 auto credType = std::string("cred_type2") + std::to_string(9 - n);
1654 cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
1655 credentials.pushBack(std::move(cred));
1656 }
1657
1658 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1659 ac.view().update(slePd);
1660 }
1661
1662 return true;
1663 },
1664 XRPAmount{},
1665 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1666 fixEnabled ? failTers : badTers);
1667
1668 testcase << "PermissionedDomain Set 4";
1670 makeEnv(features),
1671 {{"permissioned domain credentials aren't unique"}},
1672 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1673 // create PD
1674 auto slePd = createPermissionedDomain(ac, a1, a2);
1675
1676 // update PD
1677 {
1678 STArray credentials(sfAcceptedCredentials, 2);
1679 for (std::size_t n = 0; n < 2; ++n)
1680 {
1681 auto cred = STObject::makeInnerObject(sfCredential);
1682 cred.setAccountID(sfIssuer, a2);
1683 cred.setFieldVL(sfCredentialType, Slice("cred_type", 9));
1684 credentials.pushBack(std::move(cred));
1685 }
1686 slePd->setFieldArray(sfAcceptedCredentials, credentials);
1687 ac.view().update(slePd);
1688 }
1689
1690 return true;
1691 },
1692 XRPAmount{},
1693 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1694 fixEnabled ? failTers : badTers);
1695
1697
1698 std::vector<std::string> const badMoreThan1{
1699 {"transaction affected more than 1 permissioned domain entry."}};
1700 std::vector<std::string> const emptyV;
1701 std::vector<std::string> const badNoDomains{{"no domain objects affected by"}};
1702 std::vector<std::string> const badNotDeleted{
1703 {"domain object modified, but not deleted by "}};
1704 std::vector<std::string> const badDeleted{{"domain object deleted by"}};
1705 std::vector<std::string> const badTx{
1706 {"domain object(s) affected by an unauthorized transaction."}};
1707
1708 {
1709 testcase << "PermissionedDomain set 2 domains ";
1711 makeEnv(features),
1712 fixEnabled ? badMoreThan1 : emptyV,
1713 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1714 createPermissionedDomain(ac, a1, a2);
1715 createPermissionedDomain(ac, a1, a2, 2, 11);
1716 return true;
1717 },
1718 XRPAmount{},
1719 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1720 fixEnabled ? failTers : goodTers);
1721 }
1722
1723 {
1724 testcase << "PermissionedDomain del 2 domains";
1725
1726 Env env1(*this, features);
1727
1728 Account const a1{"A1"};
1729 Account const a2{"A2"};
1730 env1.fund(XRP(1000), a1, a2);
1731 env1.close();
1732
1733 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
1734 [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
1735 env1.close();
1736
1738 std::move(env1),
1739 a1,
1740 a2,
1741 fixEnabled ? badMoreThan1 : emptyV,
1742 [&pd1, &pd2](Account const&, Account const&, ApplyContext& ac) {
1743 auto sle1 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd1});
1744 auto sle2 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd2});
1745 ac.view().erase(sle1);
1746 ac.view().erase(sle2);
1747 return true;
1748 },
1749 XRPAmount{},
1750 STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
1751 fixEnabled ? failTers : goodTers);
1752 }
1753
1754 {
1755 testcase << "PermissionedDomain set 0 domains ";
1757 makeEnv(features),
1758 fixEnabled ? badNoDomains : emptyV,
1759 [](Account const&, Account const&, ApplyContext&) { return true; },
1760 XRPAmount{},
1761 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1762 fixEnabled ? badTers : goodTers);
1763 }
1764
1765 {
1766 testcase << "PermissionedDomain del 0 domains";
1767
1768 Env env1(*this, features);
1769
1770 Account const a1{"A1"};
1771 Account const a2{"A2"};
1772 env1.fund(XRP(1000), a1, a2);
1773 env1.close();
1774
1775 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
1776 [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
1777 env1.close();
1778
1780 makeEnv(features),
1781 a1,
1782 a2,
1783 fixEnabled ? badNoDomains : emptyV,
1784 [](Account const&, Account const&, ApplyContext&) { return true; },
1785 XRPAmount{},
1786 STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
1787 fixEnabled ? badTers : goodTers);
1788 }
1789
1790 {
1791 testcase << "PermissionedDomain set, delete domain";
1792
1793 Env env1(*this, features);
1794
1795 Account const a1{"A1"};
1796 Account const a2{"A2"};
1797 env1.fund(XRP(1000), a1, a2);
1798 env1.close();
1799
1800 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
1801 env1.close();
1802
1804 std::move(env1),
1805 a1,
1806 a2,
1807 fixEnabled ? badDeleted : emptyV,
1808 [&pd1](Account const&, Account const&, ApplyContext& ac) {
1809 auto sle1 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd1});
1810 ac.view().erase(sle1);
1811 return true;
1812 },
1813 XRPAmount{},
1814 STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
1815 fixEnabled ? failTers : goodTers);
1816 }
1817
1818 {
1819 testcase << "PermissionedDomain del, create domain ";
1821 makeEnv(features),
1822 fixEnabled ? badNotDeleted : emptyV,
1823 [](Account const& a1, Account const& a2, ApplyContext& ac) {
1824 createPermissionedDomain(ac, a1, a2);
1825 return true;
1826 },
1827 XRPAmount{},
1828 STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
1829 fixEnabled ? failTers : goodTers);
1830 }
1831
1832 {
1833 testcase << "PermissionedDomain invalid tx";
1834
1836 fixEnabled ? badTx : emptyV,
1837 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1838 createPermissionedDomain(ac, a1, a2);
1839 return true;
1840 },
1841 XRPAmount{},
1842 STTx{ttPAYMENT, [](STObject&) {}},
1843 failTers);
1844 }
1845 }
1846
1847 void
1849 {
1850 testcase << "valid pseudo accounts";
1851
1852 using namespace jtx;
1853
1854 AccountID pseudoAccountID;
1855 Preclose const createPseudo = [&, this](Account const& a, Account const& b, Env& env) {
1856 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
1857
1858 // Create vault
1859 Vault const vault{env};
1860 auto [tx, vKeylet] = vault.create({.owner = a, .asset = xrpAsset});
1861 env(tx);
1862 env.close();
1863 if (auto const vSle = env.le(vKeylet); BEAST_EXPECT(vSle))
1864 {
1865 pseudoAccountID = vSle->at(sfAccount);
1866 }
1867
1868 return BEAST_EXPECT(env.le(keylet::account(pseudoAccountID)));
1869 };
1870
1871 /* Cases to check
1872 "pseudo-account has 0 pseudo-account fields set"
1873 "pseudo-account has 2 pseudo-account fields set"
1874 "pseudo-account sequence changed"
1875 "pseudo-account flags are not set"
1876 "pseudo-account has a regular key"
1877 "pseudo-account has a sponsorship field"
1878 */
1879 struct Mod
1880 {
1881 std::string expectedFailure;
1882 std::function<void(SLE::pointer&)> func;
1883 };
1884 auto const mods = std::to_array<Mod>({
1885 {
1886 .expectedFailure = "pseudo-account has 0 pseudo-account fields set",
1887 .func =
1888 [this](SLE::pointer& sle) {
1889 BEAST_EXPECT(sle->at(~sfVaultID));
1890 sle->at(~sfVaultID) = std::nullopt;
1891 },
1892 },
1893 {
1894 .expectedFailure = "pseudo-account sequence changed",
1895 .func = [](SLE::pointer& sle) { sle->at(sfSequence) = 12345; },
1896 },
1897 {
1898 .expectedFailure = "pseudo-account flags are not set",
1899 .func = [](SLE::pointer& sle) { sle->at(sfFlags) = lsfNoFreeze; },
1900 },
1901 {
1902 .expectedFailure = "pseudo-account has a regular key",
1903 .func = [](SLE::pointer& sle) { sle->at(sfRegularKey) = Account("regular").id(); },
1904 },
1905 {
1906 .expectedFailure = "pseudo-account has a sponsorship field",
1907 .func = [](SLE::pointer& sle) { sle->at(sfSponsoredOwnerCount) = 1; },
1908 },
1909 {
1910 .expectedFailure = "pseudo-account has a sponsorship field",
1911 .func = [](SLE::pointer& sle) { sle->at(sfSponsoringOwnerCount) = 1; },
1912 },
1913 {
1914 .expectedFailure = "pseudo-account has a sponsorship field",
1915 .func = [](SLE::pointer& sle) { sle->at(sfSponsoringAccountCount) = 1; },
1916 },
1917 {
1918 .expectedFailure = "pseudo-account has a sponsorship field",
1919 .func = [](SLE::pointer& sle) { sle->at(sfSponsor) = Account("sponsor").id(); },
1920 },
1921 });
1922
1923 for (auto const& mod : mods)
1924 {
1926 {{mod.expectedFailure}},
1927 [&](Account const& a1, Account const&, ApplyContext& ac) {
1928 auto sle = ac.view().peek(keylet::account(pseudoAccountID));
1929 if (!sle)
1930 return false;
1931 mod.func(sle);
1932 ac.view().update(sle);
1933 return true;
1934 },
1935 XRPAmount{},
1936 STTx{ttACCOUNT_SET, [](STObject& tx) {}},
1938 createPseudo);
1939 }
1940 for (auto const pField : getPseudoAccountFields())
1941 {
1942 // createPseudo creates a vault, so sfVaultID will be set, and
1943 // setting it again will not cause an error
1944 if (pField == &sfVaultID)
1945 continue;
1947 {{"pseudo-account has 2 pseudo-account fields set"}},
1948 [&](Account const& a1, Account const&, ApplyContext& ac) {
1949 auto sle = ac.view().peek(keylet::account(pseudoAccountID));
1950 if (!sle)
1951 return false;
1952
1953 auto const vaultID = ~sle->at(~sfVaultID);
1954 BEAST_EXPECT(vaultID && !sle->isFieldPresent(*pField));
1955 sle->setFieldH256(*pField, *vaultID);
1956
1957 ac.view().update(sle);
1958 return true;
1959 },
1960 XRPAmount{},
1961 STTx{ttACCOUNT_SET, [](STObject& tx) {}},
1963 createPseudo);
1964 }
1965
1966 // Take one of the regular accounts and set the sequence to 0, which
1967 // will make it look like a pseudo-account
1969 {{"pseudo-account has 0 pseudo-account fields set"},
1970 {"pseudo-account sequence changed"},
1971 {"pseudo-account flags are not set"}},
1972 [&](Account const& a1, Account const&, ApplyContext& ac) {
1973 auto sle = ac.view().peek(keylet::account(a1.id()));
1974 if (!sle)
1975 return false;
1976 sle->at(sfSequence) = 0;
1977 ac.view().update(sle);
1978 return true;
1979 });
1980 }
1981
1984 test::jtx::Env& env,
1985 test::jtx::Account const& a1,
1986 test::jtx::Account const& a2,
1987 std::uint32_t numCreds = 2)
1988 {
1989 using namespace test::jtx;
1990
1992
1993 for (std::size_t n = 0; n < numCreds; ++n)
1994 {
1995 auto credType = "cred_type" + std::to_string(n);
1996 credentials.push_back({.issuer = a2, .credType = credType});
1997 }
1998
1999 std::uint32_t const seq = env.seq(a1);
2000 env(pdomain::setTx(a1, credentials));
2001 uint256 const key = pdomain::getNewDomain(env.meta());
2002
2003 // std::cout << "PD, acc: " << A1.id() << ", seq: " << seq << ", k: " <<
2004 // key << std::endl;
2005 return {seq, key};
2006 }
2007
2008 void
2010 {
2011 using namespace test::jtx;
2012
2013 bool const fixEnabled = features[fixCleanup3_1_3];
2014
2015 testcase << "PermissionedDEX" + std::string(fixEnabled ? " fix" : "");
2016
2018 makeEnv(features),
2019 {{"domain doesn't exist"}},
2020 [](Account const& a1, Account const&, ApplyContext& ac) {
2021 Keylet const offerKey = keylet::offer(a1.id(), SeqProxy::rawSequence(10));
2022 auto sleOffer = std::make_shared<SLE>(offerKey);
2023 sleOffer->setAccountID(sfAccount, a1);
2024 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2025 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2026 ac.view().insert(sleOffer);
2027 return true;
2028 },
2029 XRPAmount{},
2030 STTx{
2031 ttOFFER_CREATE,
2032 [](STObject& tx) {
2033 tx.setFieldH256(
2034 sfDomainID,
2035 uint256{"F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E33"
2036 "70F3649CE134E5"});
2037 Account const a1{"A1"};
2038 tx.setFieldAmount(sfTakerPays, a1["USD"](10));
2039 tx.setFieldAmount(sfTakerGets, XRP(1));
2040 }},
2042
2043 // missing domain ID in offer object
2045 makeEnv(features),
2046 {{"hybrid offer is malformed"}},
2047 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2048 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
2049 auto sleOffer = std::make_shared<SLE>(offerKey);
2050 sleOffer->setAccountID(sfAccount, a2);
2051 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2052 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2053 sleOffer->setFlag(lsfHybrid);
2054
2055 STArray bookArr;
2056 bookArr.pushBack(STObject::makeInnerObject(sfBook));
2057 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
2058 ac.view().insert(sleOffer);
2059 return true;
2060 },
2061 XRPAmount{},
2062 STTx{ttOFFER_CREATE, [&](STObject&) {}},
2064
2065 // more than one entry in sfAdditionalBooks
2066 {
2067 Env env1(*this, features);
2068
2069 Account const a1{"A1"};
2070 Account const a2{"A2"};
2071 env1.fund(XRP(1000), a1, a2);
2072 env1.close();
2073
2074 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
2075 env1.close();
2076
2078 std::move(env1),
2079 a1,
2080 a2,
2081 {{"hybrid offer is malformed"}},
2082 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
2083 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
2084 auto sleOffer = std::make_shared<SLE>(offerKey);
2085 sleOffer->setAccountID(sfAccount, a2);
2086 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2087 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2088 sleOffer->setFlag(lsfHybrid);
2089 sleOffer->setFieldH256(sfDomainID, pd1);
2090
2091 STArray bookArr;
2092 bookArr.pushBack(STObject::makeInnerObject(sfBook));
2093 bookArr.pushBack(STObject::makeInnerObject(sfBook));
2094 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
2095 ac.view().insert(sleOffer);
2096 return true;
2097 },
2098 XRPAmount{},
2099 STTx{ttOFFER_CREATE, [&](STObject&) {}},
2101 }
2102
2103 // empty sfAdditionalBooks (size 0)
2104 {
2105 Env env1(*this, features);
2106
2107 Account const a1{"A1"};
2108 Account const a2{"A2"};
2109 env1.fund(XRP(1000), a1, a2);
2110 env1.close();
2111
2112 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
2113 env1.close();
2114
2116 std::move(env1),
2117 a1,
2118 a2,
2119 fixEnabled ? std::vector<std::string>{{"hybrid offer is malformed"}}
2121 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
2122 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
2123 auto sleOffer = std::make_shared<SLE>(offerKey);
2124 sleOffer->setAccountID(sfAccount, a2);
2125 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2126 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2127 sleOffer->setFlag(lsfHybrid);
2128 sleOffer->setFieldH256(sfDomainID, pd1);
2129
2130 STArray const bookArr; // empty array, size 0
2131 sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
2132 ac.view().insert(sleOffer);
2133 return true;
2134 },
2135 XRPAmount{},
2136 STTx{ttOFFER_CREATE, [&](STObject&) {}},
2139 }
2140
2141 // hybrid offer missing sfAdditionalBooks
2142 {
2143 Env env1(*this, features);
2144
2145 Account const a1{"A1"};
2146 Account const a2{"A2"};
2147 env1.fund(XRP(1000), a1, a2);
2148 env1.close();
2149
2150 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
2151 env1.close();
2152
2154 std::move(env1),
2155 a1,
2156 a2,
2157 {{"hybrid offer is malformed"}},
2158 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
2159 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
2160 auto sleOffer = std::make_shared<SLE>(offerKey);
2161 sleOffer->setAccountID(sfAccount, a2);
2162 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2163 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2164 sleOffer->setFlag(lsfHybrid);
2165 sleOffer->setFieldH256(sfDomainID, pd1);
2166 ac.view().insert(sleOffer);
2167 return true;
2168 },
2169 XRPAmount{},
2170 STTx{ttOFFER_CREATE, [&](STObject&) {}},
2172 }
2173
2174 {
2175 Env env1(*this, features);
2176
2177 Account const a1{"A1"};
2178 Account const a2{"A2"};
2179 env1.fund(XRP(1000), a1, a2);
2180 env1.close();
2181
2182 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
2183 [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
2184 env1.close();
2185
2187 std::move(env1),
2188 a1,
2189 a2,
2190 {{"transaction consumed wrong domains"}},
2191 [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
2192 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
2193 auto sleOffer = std::make_shared<SLE>(offerKey);
2194 sleOffer->setAccountID(sfAccount, a2);
2195 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2196 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2197 sleOffer->setFieldH256(sfDomainID, pd1);
2198 ac.view().insert(sleOffer);
2199 return true;
2200 },
2201 XRPAmount{},
2202 STTx{
2203 ttOFFER_CREATE,
2204 [&pd2, &a1](STObject& tx) {
2205 tx.setFieldH256(sfDomainID, pd2);
2206 tx.setFieldAmount(sfTakerPays, a1["USD"](10));
2207 tx.setFieldAmount(sfTakerGets, XRP(1));
2208 }},
2210 }
2211
2212 {
2213 Env env1(*this, features);
2214
2215 Account const a1{"A1"};
2216 Account const a2{"A2"};
2217 env1.fund(XRP(1000), a1, a2);
2218 env1.close();
2219
2220 [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
2221 env1.close();
2222
2224 std::move(env1),
2225 a1,
2226 a2,
2227 {{"domain transaction affected regular offers"}},
2228 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2229 Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
2230 auto sleOffer = std::make_shared<SLE>(offerKey);
2231 sleOffer->setAccountID(sfAccount, a2);
2232 sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
2233 sleOffer->setFieldAmount(sfTakerGets, XRP(1));
2234 ac.view().insert(sleOffer);
2235 return true;
2236 },
2237 XRPAmount{},
2238 STTx{
2239 ttOFFER_CREATE,
2240 [&](STObject& tx) {
2241 Account const a1{"A1"};
2242 tx.setFieldH256(sfDomainID, pd1);
2243 tx.setFieldAmount(sfTakerPays, a1["USD"](10));
2244 tx.setFieldAmount(sfTakerGets, XRP(1));
2245 }},
2247 }
2248 }
2249
2250 void
2252 {
2253 using namespace test::jtx;
2254 testcase << "book directory exchange rate";
2255
2256 auto const getBookRootKey = [](Account const& account, std::uint64_t quality) {
2257 Book const book{xrpIssue(), account["USD"], std::nullopt};
2258 return keylet::quality(keylet::book(book), quality);
2259 };
2260
2261 // Root book-directory pages carry exchange-rate metadata that must
2262 // match the quality encoded in the directory key.
2263 auto const makeRootPage = [](Keylet const& dir, std::uint64_t exchangeRate) {
2264 auto sleDir = std::make_shared<SLE>(dir);
2265 sleDir->setFieldH256(sfRootIndex, dir.key);
2266 STVector256 indexes;
2267 indexes.pushBack(uint256{1});
2268 sleDir->setFieldV256(sfIndexes, indexes);
2269 sleDir->setFieldU64(sfExchangeRate, exchangeRate);
2270 return sleDir;
2271 };
2272
2273 // Child pages do not carry quality metadata; they only point back to
2274 // the root directory.
2275 auto const makeChildPage = [](Keylet const& rootDir) {
2276 auto sleDir = std::make_shared<SLE>(keylet::page(rootDir, 1));
2277 sleDir->setFieldH256(sfRootIndex, rootDir.key);
2278 STVector256 indexes;
2279 indexes.pushBack(uint256{2});
2280 sleDir->setFieldV256(sfIndexes, indexes);
2281 return sleDir;
2282 };
2283
2284 auto const makeOfferCreateTx = [] {
2285 return STTx{ttOFFER_CREATE, [](STObject& tx) {
2286 Account const account{"A1"};
2287 tx.setFieldAmount(sfTakerPays, XRP(1));
2288 tx.setFieldAmount(sfTakerGets, account["USD"](1));
2289 }};
2290 };
2292
2293 // Creating a root book directory with mismatched exchange-rate
2294 // metadata violates the invariant.
2296 {{"book directory exchange rate does not match directory quality"}},
2297 [&](Account const& a1, Account const&, ApplyContext& ac) {
2298 auto const directoryQuality = STAmount::kURateOne;
2299 auto const dir = getBookRootKey(a1, directoryQuality);
2300 ac.view().insert(makeRootPage(dir, directoryQuality + 1));
2301 return true;
2302 },
2303 XRPAmount{},
2304 makeOfferCreateTx(),
2305 failTers);
2306
2307 // A new child page must point to an existing root page.
2309 {{"book directory root missing"}},
2310 [&](Account const& a1, Account const&, ApplyContext& ac) {
2311 auto const directoryQuality = STAmount::kURateOne;
2312 auto const rootDir = getBookRootKey(a1, directoryQuality);
2313 // Insert only the child page. It points at rootDir, but the
2314 // corresponding root page is intentionally missing.
2315 ac.view().insert(makeChildPage(rootDir));
2316 return true;
2317 },
2318 XRPAmount{},
2319 makeOfferCreateTx(),
2320 failTers);
2321
2322 // Legacy bad-root tolerance:
2323 // - The view contains a pre-existing root page with bad sfExchangeRate
2324 // metadata.
2325 // - The simulated transaction only creates a child page pointing to
2326 // that root.
2327 // - The invariant must pass because this transaction did not create
2328 // the bad root, only adding a child page.
2329 {
2330 Env env{*this, defaultAmendments()};
2331 Account const a1{"A1"};
2332 env.fund(XRP(1000), a1);
2333 env.close();
2334
2335 OpenView view{*env.current()};
2336 auto const directoryQuality = STAmount::kURateOne;
2337 auto const rootDir = getBookRootKey(a1, directoryQuality);
2338 view.rawInsert(makeRootPage(rootDir, directoryQuality + 1));
2339
2340 ValidBookDirectory invariant;
2341 invariant.visitEntry(false, nullptr, makeChildPage(rootDir));
2342
2344 beast::Journal const jlog{sink};
2345 BEAST_EXPECT(
2346 invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
2347 }
2348
2349 // A bad root is rejected when added, ignored when a legacy bad root is
2350 // modified without changing sfRootIndex or deleted, and checked when a
2351 // modified directory changes sfRootIndex.
2352 {
2353 Env env{*this, defaultAmendments()};
2354 Account const a1{"A1"};
2355 env.fund(XRP(1000), a1);
2356 env.close();
2357
2358 OpenView view{*env.current()};
2359 auto const directoryQuality = STAmount::kURateOne;
2360 auto const rootDir = getBookRootKey(a1, directoryQuality);
2361 auto const missingRootDir = getBookRootKey(a1, directoryQuality + 1);
2362 auto const badRoot = makeRootPage(rootDir, directoryQuality + 1);
2363 view.rawInsert(badRoot);
2364
2366 beast::Journal const jlog{sink};
2367
2368 {
2369 // add
2370 ValidBookDirectory invariant;
2371 invariant.visitEntry(false, nullptr, badRoot);
2372
2373 BEAST_EXPECT(
2374 !invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
2375 }
2376 {
2377 // modify (without changing the sfRootIndex)
2378 ValidBookDirectory invariant;
2379 invariant.visitEntry(false, badRoot, badRoot);
2380
2381 BEAST_EXPECT(
2382 invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
2383 }
2384 {
2385 // modify (changing sfRootIndex to a missing root)
2386 auto const childBefore = makeChildPage(rootDir);
2387 auto const childAfter = std::make_shared<SLE>(*childBefore, childBefore->key());
2388 childAfter->setFieldH256(sfRootIndex, missingRootDir.key);
2389
2390 ValidBookDirectory invariant;
2391 invariant.visitEntry(false, childBefore, childAfter);
2392
2394 beast::Journal const missingRootJlog{missingRootSink};
2395 BEAST_EXPECT(!invariant.finalize(
2396 makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, missingRootJlog));
2397 BEAST_EXPECT(
2398 missingRootSink.messages().str().contains("book directory root missing"));
2399 }
2400 {
2401 // delete
2402 view.rawErase(badRoot);
2403 BEAST_EXPECT(!view.exists(rootDir));
2404
2405 ValidBookDirectory invariant;
2406 invariant.visitEntry(true, badRoot, badRoot);
2407 BEAST_EXPECT(
2408 invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
2409 }
2410 }
2411 }
2412
2413 Keylet
2415 {
2416 using namespace jtx;
2417
2418 // Create vault
2419 uint256 vaultID;
2420 Vault const vault{env};
2421 auto [tx, vKeylet] = vault.create({.owner = a, .asset = asset});
2422 env(tx);
2423 BEAST_EXPECT(env.le(vKeylet));
2424
2425 vaultID = vKeylet.key;
2426
2427 // Create Loan Broker
2428 using namespace loan_broker;
2429
2430 auto const loanBrokerKeylet = keylet::loanBroker(a.id(), SeqProxy::rawSequence(env.seq(a)));
2431 // Create a Loan Broker with all default values.
2432 env(set(a, vaultID), Fee(kIncrement));
2433
2434 return loanBrokerKeylet;
2435 };
2436
2437 void
2439 {
2440 testcase("no modified unmodifiable fields");
2441 using namespace jtx;
2442
2443 // Initialize with a placeholder value because there's no default ctor
2444 Keylet loanBrokerKeylet = keylet::amendments();
2445 Preclose const createLoanBroker = [&, this](Account const& a, Account const& b, Env& env) {
2446 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
2447
2448 loanBrokerKeylet = this->createLoanBroker(a, env, xrpAsset);
2449 return BEAST_EXPECT(env.le(loanBrokerKeylet));
2450 };
2451
2452 {
2453 auto const mods = std::to_array<std::function<void(SLE::pointer&)>>({
2454 [](SLE::pointer& sle) { sle->at(sfSequence) += 1; },
2455 [](SLE::pointer& sle) { sle->at(sfOwnerNode) += 1; },
2456 [](SLE::pointer& sle) { sle->at(sfVaultNode) += 1; },
2457 [](SLE::pointer& sle) { sle->at(sfVaultID) = uint256(1u); },
2458 [](SLE::pointer& sle) { sle->at(sfAccount) = sle->at(sfOwner); },
2459 [](SLE::pointer& sle) { sle->at(sfOwner) = sle->at(sfAccount); },
2460 [](SLE::pointer& sle) { sle->at(sfManagementFeeRate) += 1; },
2461 [](SLE::pointer& sle) { sle->at(sfCoverRateMinimum) += 1; },
2462 [](SLE::pointer& sle) { sle->at(sfCoverRateLiquidation) += 1; },
2463 [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
2464 [](SLE::pointer& sle) { sle->at(sfLedgerIndex) = sle->at(sfVaultID).value(); },
2465 });
2466
2467 for (auto const& mod : mods)
2468 {
2470 {{"changed an unchangeable field"}},
2471 [&](Account const& a1, Account const&, ApplyContext& ac) {
2472 auto sle = ac.view().peek(loanBrokerKeylet);
2473 if (!sle)
2474 return false;
2475 mod(sle);
2476 ac.view().update(sle);
2477 return true;
2478 },
2479 XRPAmount{},
2480 STTx{ttACCOUNT_SET, [](STObject& tx) {}},
2483 }
2484 }
2485
2486 // TODO: Loan Object
2487
2488 // VaultKind, SubscriptionDate and RedemptionDate are immutable once set at creation.
2489 // Enforced by NoModifiedUnmodifiableFields on ltVAULT via kFieldChanged.
2490 Keylet closedEndedVaultKeylet = keylet::amendments();
2491 Preclose const createClosedEndedVault = [&, this](
2492 Account const& a, Account const&, Env& env) {
2493 auto const sub = env.now().time_since_epoch().count() + 60;
2494 auto const red = sub + kMinInvestmentPeriod + 1'000'000;
2495 Vault const vault{env};
2496 auto [tx, keylet] = vault.create(
2497 {.owner = a,
2498 .asset = xrpIssue(),
2499 .vaultKind = std::to_underlying(VaultKind::ClosedEnded),
2500 .subscriptionDate = sub,
2501 .redemptionDate = red});
2502 env(tx);
2503 closedEndedVaultKeylet = keylet;
2504 return BEAST_EXPECT(env.le(closedEndedVaultKeylet));
2505 };
2506
2507 {
2508 // Each mutation must keep the vault otherwise valid so that only the immutability check
2509 // fires. Shifting both dates by the same offset preserves the gap; bumping sfVaultKind
2510 // stays within the recognised range.
2511 auto const mods = std::to_array<std::function<void(SLE::pointer&)>>({
2512 [](SLE::pointer& sle) { sle->at(sfVaultKind) += 1; },
2513 [](SLE::pointer& sle) { sle->at(sfSubscriptionDate) += 1; },
2514 [](SLE::pointer& sle) { sle->at(sfRedemptionDate) += 1; },
2515 });
2516
2517 for (auto const& mod : mods)
2518 {
2520 {{"changed an unchangeable field"}},
2521 [&](Account const&, Account const&, ApplyContext& ac) {
2522 auto sle = ac.view().peek(closedEndedVaultKeylet);
2523 if (!sle)
2524 return false;
2525 mod(sle);
2526 ac.view().update(sle);
2527 return true;
2528 },
2529 XRPAmount{},
2530 STTx{ttACCOUNT_SET, [](STObject&) {}},
2532 createClosedEndedVault);
2533 }
2534 }
2535
2536 {
2537 auto const mods = std::to_array<std::function<void(SLE::pointer&)>>({
2538 [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
2539 [](SLE::pointer& sle) { sle->at(sfLedgerIndex) = uint256(1u); },
2540 });
2541
2542 for (auto const& mod : mods)
2543 {
2545 {{"changed an unchangeable field"}},
2546 [&](Account const& a1, Account const&, ApplyContext& ac) {
2547 auto sle = ac.view().peek(keylet::account(a1.id()));
2548 if (!sle)
2549 return false;
2550 mod(sle);
2551 ac.view().update(sle);
2552 return true;
2553 });
2554 }
2555 }
2556 }
2557
2558 void
2560 {
2561 testcase << "valid loan broker";
2562
2563 using namespace jtx;
2564
2565 enum class Asset { XRP, IOU, MPT };
2566 auto const assetTypes = std::to_array({Asset::XRP, Asset::IOU, Asset::MPT});
2567
2568 for (auto const assetType : assetTypes)
2569 {
2570 // Initialize with a placeholder value because there's no default
2571 // ctor
2572 auto const setupAsset =
2573 [&](Account const& alice, Account const& issuer, Env& env) -> PrettyAsset {
2574 switch (assetType)
2575 {
2576 case Asset::IOU: {
2577 PrettyAsset const iouAsset = issuer["IOU"];
2578 env(trust(alice, iouAsset(1000)));
2579 env(pay(issuer, alice, iouAsset(1000)));
2580 env.close();
2581 return iouAsset;
2582 }
2583 case Asset::MPT: {
2584 MPTTester mptt{env, issuer, kMptInitNoFund};
2585 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
2586 PrettyAsset const mptAsset = mptt.issuanceID();
2587 mptt.authorize({.account = alice});
2588 env(pay(issuer, alice, mptAsset(1000)));
2589 env.close();
2590 return mptAsset;
2591 }
2592 case Asset::XRP:
2593 default:
2594 return PrettyAsset{xrpIssue(), 1'000'000};
2595 }
2596 };
2597
2598 Keylet loanBrokerKeylet = keylet::amendments();
2600 [&, this](Account const& alice, Account const& issuer, Env& env) {
2601 auto const asset = setupAsset(alice, issuer, env);
2602 loanBrokerKeylet = this->createLoanBroker(alice, env, asset);
2603 return BEAST_EXPECT(env.le(loanBrokerKeylet));
2604 };
2605
2606 // Ensure the test scenarios are set up completely. The test cases
2607 // will need to recompute any of these values it needs for itself
2608 // rather than trying to return a bunch of items
2609 auto setupTest = [&, this](Account const& a1, Account const&, ApplyContext& ac)
2611 if (loanBrokerKeylet.type != ltLOAN_BROKER)
2612 return {};
2613 auto sleBroker = ac.view().peek(loanBrokerKeylet);
2614 if (!sleBroker)
2615 return {};
2616 if (!BEAST_EXPECT(sleBroker->at(sfOwnerCount) == 0))
2617 return {};
2618 // Need to touch sleBroker so that it is included in the
2619 // modified entries for the invariant to find
2620 ac.view().update(sleBroker);
2621
2622 // The pseudo-account holds the directory, so get it
2623 auto const pseudoAccountID = sleBroker->at(sfAccount);
2624 auto const pseudoAccountKeylet = keylet::account(pseudoAccountID);
2625 // Strictly speaking, we don't need to load the
2626 // ACCOUNT_ROOT, but check anyway
2627 auto slePseudo = ac.view().peek(pseudoAccountKeylet);
2628 if (!BEAST_EXPECT(slePseudo))
2629 return {};
2630 // Make sure the directory doesn't already exist
2631 auto const dirKeylet = keylet::ownerDir(pseudoAccountID);
2632 auto sleDir = ac.view().peek(dirKeylet);
2633 auto const describe = describeOwnerDir(pseudoAccountID);
2634 if (!sleDir)
2635 {
2636 // Create the directory
2637 BEAST_EXPECT(
2639 ac.view(), dirKeylet, loanBrokerKeylet.key, describe) == 0);
2640
2641 sleDir = ac.view().peek(dirKeylet);
2642 }
2643
2644 return std::make_pair(slePseudo, sleDir);
2645 };
2646
2648 {{"Loan Broker with zero OwnerCount has multiple directory "
2649 "pages"}},
2650 [&setupTest, this](Account const& a1, Account const& a2, ApplyContext& ac) {
2651 auto test = setupTest(a1, a2, ac);
2652 if (!test || !test->first || !test->second)
2653 return false;
2654
2655 auto slePseudo = test->first;
2656 auto sleDir = test->second;
2657 auto const describe = describeOwnerDir(slePseudo->at(sfAccount));
2658
2659 BEAST_EXPECT(
2661 ac.view(),
2662 0,
2663 sleDir,
2664 0,
2665 sleDir,
2666 slePseudo->key(),
2667 keylet::page(sleDir->key(), 0),
2668 describe) == 1);
2669
2670 return true;
2671 },
2672 XRPAmount{},
2673 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2676
2678 {{"Loan Broker with zero OwnerCount has multiple indexes in "
2679 "the Directory root"}},
2680 [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
2681 auto test = setupTest(a1, a2, ac);
2682 if (!test || !test->first || !test->second)
2683 return false;
2684
2685 auto slePseudo = test->first;
2686 auto sleDir = test->second;
2687 auto indexes = sleDir->getFieldV256(sfIndexes);
2688
2689 // Put some extra garbage into the directory
2690 for (auto const& key : {slePseudo->key(), sleDir->key()})
2691 {
2692 ::xrpl::directory::insertKey(ac.view(), sleDir, 0, false, indexes, key);
2693 }
2694
2695 return true;
2696 },
2697 XRPAmount{},
2698 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2701
2703 {{"Loan Broker directory corrupt"}},
2704 [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
2705 auto test = setupTest(a1, a2, ac);
2706 if (!test || !test->first || !test->second)
2707 return false;
2708
2709 auto slePseudo = test->first;
2710 auto sleDir = test->second;
2711 auto const describe = describeOwnerDir(slePseudo->at(sfAccount));
2712 // Empty vector will overwrite the existing entry for the
2713 // holding, if any, avoiding the "has multiple indexes"
2714 // failure.
2715 STVector256 indexes;
2716
2717 // Put one meaningless key into the directory
2718 auto const key = keylet::account(Account("random").id()).key;
2719 ::xrpl::directory::insertKey(ac.view(), sleDir, 0, false, indexes, key);
2720
2721 return true;
2722 },
2723 XRPAmount{},
2724 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2727
2729 {{"Loan Broker with zero OwnerCount has an unexpected entry in "
2730 "the directory"}},
2731 [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
2732 auto test = setupTest(a1, a2, ac);
2733 if (!test || !test->first || !test->second)
2734 return false;
2735
2736 auto slePseudo = test->first;
2737 auto sleDir = test->second;
2738 // Empty vector will overwrite the existing entry for the
2739 // holding, if any, avoiding the "has multiple indexes"
2740 // failure.
2741 STVector256 indexes;
2742
2744 ac.view(), sleDir, 0, false, indexes, slePseudo->key());
2745
2746 return true;
2747 },
2748 XRPAmount{},
2749 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2752
2754 {{"Loan Broker sequence number decreased"}},
2755 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2756 if (loanBrokerKeylet.type != ltLOAN_BROKER)
2757 return false;
2758 auto sleBroker = ac.view().peek(loanBrokerKeylet);
2759 if (!sleBroker)
2760 return false;
2761 if (!BEAST_EXPECT(sleBroker->at(sfLoanSequence) > 0))
2762 return false;
2763 // Need to touch sleBroker so that it is included in the
2764 // modified entries for the invariant to find
2765 ac.view().update(sleBroker);
2766
2767 sleBroker->at(sfLoanSequence) -= 1;
2768
2769 return true;
2770 },
2771 XRPAmount{},
2772 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2775
2776 // Test: cover available less than pseudo-account asset balance
2777 {
2778 Keylet brokerKeylet = keylet::amendments();
2779 Preclose const createBrokerWithCover =
2780 [&, this](Account const& alice, Account const& issuer, Env& env) {
2781 auto const asset = setupAsset(alice, issuer, env);
2782 brokerKeylet = this->createLoanBroker(alice, env, asset);
2783 if (!BEAST_EXPECT(env.le(brokerKeylet)))
2784 return false;
2785 env(loan_broker::coverDeposit(alice, brokerKeylet.key, asset(10)));
2786 env.close();
2787 return BEAST_EXPECT(env.le(brokerKeylet));
2788 };
2789
2791 {{"Loan Broker cover available is less than pseudo-account asset balance"}},
2792 [&](Account const&, Account const&, ApplyContext& ac) {
2793 auto sle = ac.view().peek(brokerKeylet);
2794 if (!BEAST_EXPECT(sle))
2795 return false;
2796 // Pseudo-account holds 10 units, set cover to 5
2797 sle->at(sfCoverAvailable) = Number(5);
2798 ac.view().update(sle);
2799 return true;
2800 },
2801 XRPAmount{},
2802 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2804 createBrokerWithCover);
2805 }
2806
2807 // Test: cover available greater than pseudo-account asset balance
2808 // (requires fixCleanup3_1_3)
2810 {{"Loan Broker cover available is greater than pseudo-account asset balance"}},
2811 [&](Account const&, Account const&, ApplyContext& ac) {
2812 auto sle = ac.view().peek(loanBrokerKeylet);
2813 if (!BEAST_EXPECT(sle))
2814 return false;
2815 // Pseudo-account has no cover deposited; set cover
2816 // higher than any incidental balance
2817 sle->at(sfCoverAvailable) = Number(1'000'000);
2818 ac.view().update(sle);
2819 return true;
2820 },
2821 XRPAmount{},
2822 STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
2825 }
2826 }
2827
2828 void
2829 testVault() // NOLINT(readability-function-size)
2830 {
2831 using namespace test::jtx;
2832
2833 struct AccountAmount
2834 {
2835 AccountID account;
2836 int amount;
2837 };
2838 struct Adjustments
2839 {
2840 // NOLINTBEGIN(readability-redundant-member-init)
2841 std::optional<int> assetsTotal = std::nullopt;
2842 std::optional<int> assetsAvailable = std::nullopt;
2843 std::optional<int> lossUnrealized = std::nullopt;
2844 std::optional<int> assetsMaximum = std::nullopt;
2845 std::optional<int> sharesTotal = std::nullopt;
2846 std::optional<int> vaultAssets = std::nullopt;
2847 std::optional<AccountAmount> accountAssets = std::nullopt;
2848 std::optional<AccountAmount> accountShares = std::nullopt;
2849 // NOLINTEND(readability-redundant-member-init)
2850 };
2851 constexpr auto kAdjust = [&](ApplyView& ac, xrpl::Keylet keylet, Adjustments args) {
2852 auto sleVault = ac.peek(keylet);
2853 if (!sleVault)
2854 return false;
2855
2856 auto const mptIssuanceID = (*sleVault)[sfShareMPTID];
2857 auto sleShares = ac.peek(keylet::mptokenIssuance(mptIssuanceID));
2858 if (!sleShares)
2859 return false;
2860
2861 // These two fields are adjusted in absolute terms
2862 if (args.lossUnrealized)
2863 (*sleVault)[sfLossUnrealized] = *args.lossUnrealized;
2864 if (args.assetsMaximum)
2865 (*sleVault)[sfAssetsMaximum] = *args.assetsMaximum;
2866
2867 // Remaining fields are adjusted in terms of difference
2868 if (args.assetsTotal)
2869 (*sleVault)[sfAssetsTotal] = *(*sleVault)[sfAssetsTotal] + *args.assetsTotal;
2870 if (args.assetsAvailable)
2871 {
2872 (*sleVault)[sfAssetsAvailable] =
2873 *(*sleVault)[sfAssetsAvailable] + *args.assetsAvailable;
2874 }
2875 ac.update(sleVault);
2876
2877 if (args.sharesTotal)
2878 {
2879 (*sleShares)[sfOutstandingAmount] =
2880 *(*sleShares)[sfOutstandingAmount] + *args.sharesTotal;
2881 ac.update(sleShares);
2882 }
2883
2884 auto const assets = *(*sleVault)[sfAsset];
2885 auto const pseudoId = *(*sleVault)[sfAccount];
2886 if (args.vaultAssets)
2887 {
2888 if (assets.native())
2889 {
2890 auto slePseudoAccount = ac.peek(keylet::account(pseudoId));
2891 if (!slePseudoAccount)
2892 return false;
2893 (*slePseudoAccount)[sfBalance] =
2894 *(*slePseudoAccount)[sfBalance] + *args.vaultAssets;
2895 ac.update(slePseudoAccount);
2896 }
2897 else if (assets.holds<MPTIssue>())
2898 {
2899 auto const mptId = assets.get<MPTIssue>().getMptID();
2900 auto sleMPToken = ac.peek(keylet::mptoken(mptId, pseudoId));
2901 if (!sleMPToken)
2902 return false;
2903 (*sleMPToken)[sfMPTAmount] = *(*sleMPToken)[sfMPTAmount] + *args.vaultAssets;
2904 ac.update(sleMPToken);
2905 }
2906 else
2907 {
2908 return false; // Not supporting testing with IOU
2909 }
2910 }
2911
2912 if (args.accountAssets)
2913 {
2914 auto const& pair = *args.accountAssets;
2915 if (assets.native())
2916 {
2917 auto sleAccount = ac.peek(keylet::account(pair.account));
2918 if (!sleAccount)
2919 return false;
2920 (*sleAccount)[sfBalance] = *(*sleAccount)[sfBalance] + pair.amount;
2921 ac.update(sleAccount);
2922 }
2923 else if (assets.holds<MPTIssue>())
2924 {
2925 auto const mptID = assets.get<MPTIssue>().getMptID();
2926 auto sleMPToken = ac.peek(keylet::mptoken(mptID, pair.account));
2927 if (!sleMPToken)
2928 return false;
2929 (*sleMPToken)[sfMPTAmount] = *(*sleMPToken)[sfMPTAmount] + pair.amount;
2930 ac.update(sleMPToken);
2931 }
2932 else
2933 {
2934 return false; // Not supporting testing with IOU
2935 }
2936 }
2937
2938 if (args.accountShares)
2939 {
2940 auto const& pair = *args.accountShares;
2941 auto sleMPToken = ac.peek(keylet::mptoken(mptIssuanceID, pair.account));
2942 if (!sleMPToken)
2943 return false;
2944 (*sleMPToken)[sfMPTAmount] = *(*sleMPToken)[sfMPTAmount] + pair.amount;
2945 ac.update(sleMPToken);
2946 }
2947 return true;
2948 };
2949
2950 static constexpr auto kArgs = [](AccountID id, int adjustment, auto fn) -> Adjustments {
2951 Adjustments sample = {
2952 .assetsTotal = adjustment,
2953 .assetsAvailable = adjustment,
2954 .lossUnrealized = 0,
2955 .sharesTotal = adjustment,
2956 .vaultAssets = adjustment,
2957 .accountAssets = //
2958 AccountAmount{.account = id, .amount = -adjustment},
2959 .accountShares = //
2960 AccountAmount{.account = id, .amount = adjustment}};
2961 fn(sample);
2962 return sample;
2963 };
2964
2965 Account const a3{"A3"};
2966 Account const a4{"A4"};
2967 auto const precloseXrp = [&](Account const& a1, Account const& a2, Env& env) -> bool {
2968 env.fund(XRP(1000), a3, a4);
2969 Vault const vault{env};
2970 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
2971 env(tx);
2972 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
2973 env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)}));
2974 env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)}));
2975 return true;
2976 };
2977
2978 testcase << "Vault general checks";
2980 {"vault deletion succeeded without deleting a vault"},
2981 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
2982 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
2983 auto sleVault = ac.view().peek(keylet);
2984 if (!sleVault)
2985 return false;
2986 ac.view().update(sleVault);
2987 return true;
2988 },
2989 XRPAmount{},
2990 STTx{ttVAULT_DELETE, [](STObject&) {}},
2992 [&](Account const& a1, Account const& a2, Env& env) {
2993 Vault const vault{env};
2994 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
2995 env(tx);
2996 return true;
2997 });
2998
3000 {"vault updated by a wrong transaction type",
3001 "deleted Vault without deleting its pseudo-account"},
3002 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3003 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3004 auto sleVault = ac.view().peek(keylet);
3005 if (!sleVault)
3006 return false;
3007 ac.view().erase(sleVault);
3008 return true;
3009 },
3010 XRPAmount{},
3011 STTx{ttPAYMENT, [](STObject&) {}},
3013 [&](Account const& a1, Account const& a2, Env& env) {
3014 Vault const vault{env};
3015 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3016 env(tx);
3017 return true;
3018 });
3019
3021 {"vault updated by a wrong transaction type"},
3022 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3023 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3024 auto sleVault = ac.view().peek(keylet);
3025 if (!sleVault)
3026 return false;
3027 ac.view().update(sleVault);
3028 return true;
3029 },
3030 XRPAmount{},
3031 STTx{ttPAYMENT, [](STObject&) {}},
3033 [&](Account const& a1, Account const& a2, Env& env) {
3034 Vault const vault{env};
3035 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3036 env(tx);
3037 return true;
3038 });
3039
3041 {"vault updated by a wrong transaction type"},
3042 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3043 auto const sequence = ac.view().seq();
3044 auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
3045 auto sleVault = std::make_shared<SLE>(vaultKeylet);
3046 auto const vaultPage = ac.view().dirInsert(
3047 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
3048 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
3049 sleVault->setAccountID(sfAccount, a1.id());
3050 ac.view().insert(sleVault);
3051 return true;
3052 },
3053 XRPAmount{},
3054 STTx{ttPAYMENT, [](STObject&) {}},
3056
3058 {"vault deleted by a wrong transaction type",
3059 "deleted Vault without deleting its pseudo-account"},
3060 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3061 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3062 auto sleVault = ac.view().peek(keylet);
3063 if (!sleVault)
3064 return false;
3065 ac.view().erase(sleVault);
3066 return true;
3067 },
3068 XRPAmount{},
3069 STTx{ttVAULT_SET, [](STObject&) {}},
3071 [&](Account const& a1, Account const& a2, Env& env) {
3072 Vault const vault{env};
3073 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3074 env(tx);
3075 return true;
3076 });
3077
3079 {"vault operation updated more than single vault",
3080 "deleted Vault without deleting its pseudo-account"},
3081 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3082 {
3083 auto const keylet =
3084 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3085 auto sleVault = ac.view().peek(keylet);
3086 if (!sleVault)
3087 return false;
3088 ac.view().erase(sleVault);
3089 }
3090 {
3091 auto const keylet =
3092 keylet::vault(a2.id(), SeqProxy::rawSequence(ac.view().seq()));
3093 auto sleVault = ac.view().peek(keylet);
3094 if (!sleVault)
3095 return false;
3096 ac.view().erase(sleVault);
3097 }
3098 return true;
3099 },
3100 XRPAmount{},
3101 STTx{ttVAULT_DELETE, [](STObject&) {}},
3103 [&](Account const& a1, Account const& a2, Env& env) {
3104 Vault const vault{env};
3105 {
3106 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3107 env(tx);
3108 }
3109 {
3110 auto [tx, _] = vault.create({.owner = a2, .asset = xrpIssue()});
3111 env(tx);
3112 }
3113 return true;
3114 });
3115
3117 {"vault operation updated more than single vault"},
3118 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3119 auto const sequence = ac.view().seq();
3120 auto const insertVault = [&](Account const a) {
3121 auto const vaultKeylet = keylet::vault(a.id(), SeqProxy::rawSequence(sequence));
3122 auto sleVault = std::make_shared<SLE>(vaultKeylet);
3123 auto const vaultPage = ac.view().dirInsert(
3124 keylet::ownerDir(a.id()), sleVault->key(), describeOwnerDir(a.id()));
3125 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
3126 sleVault->setAccountID(sfAccount, a.id());
3127 ac.view().insert(sleVault);
3128 };
3129 insertVault(a1);
3130 insertVault(a2);
3131 return true;
3132 },
3133 XRPAmount{},
3134 STTx{ttVAULT_CREATE, [](STObject&) {}},
3136
3138 {"deleted vault must also delete shares",
3139 "deleted Vault without deleting its pseudo-account"},
3140 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3141 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3142 auto sleVault = ac.view().peek(keylet);
3143 if (!sleVault)
3144 return false;
3145 ac.view().erase(sleVault);
3146 return true;
3147 },
3148 XRPAmount{},
3149 STTx{ttVAULT_DELETE, [](STObject&) {}},
3151 [&](Account const& a1, Account const& a2, Env& env) {
3152 Vault const vault{env};
3153 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3154 env(tx);
3155 return true;
3156 });
3157
3159 {"deleted vault must have no shares outstanding",
3160 "deleted vault must have no assets outstanding",
3161 "deleted vault must have no assets available"},
3162 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3163 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3164 auto sleVault = ac.view().peek(keylet);
3165 if (!sleVault)
3166 return false;
3167 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3168 if (!sleShares)
3169 return false;
3170 ac.view().erase(sleVault);
3171 ac.view().erase(sleShares);
3172 return true;
3173 },
3174 XRPAmount{},
3175 STTx{ttVAULT_DELETE, [](STObject&) {}},
3177 [&](Account const& a1, Account const& a2, Env& env) {
3178 Vault const vault{env};
3179 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3180 env(tx);
3181 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
3182 return true;
3183 });
3184
3186 {"vault operation succeeded without modifying a vault"},
3187 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3188 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3189 auto sleVault = ac.view().peek(keylet);
3190 if (!sleVault)
3191 return false;
3192 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3193 if (!sleShares)
3194 return false;
3195 // Note, such an "orphaned" update of MPT issuance attached to a
3196 // vault is invalid; ttVAULT_SET must also update Vault object.
3197 sleShares->setFieldH256(sfDomainID, uint256(13));
3198 ac.view().update(sleShares);
3199 return true;
3200 },
3201 XRPAmount{},
3202 STTx{ttVAULT_SET, [](STObject& tx) {}},
3204 precloseXrp,
3206
3208 {"vault operation succeeded without modifying a vault"},
3209 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
3210 XRPAmount{},
3211 STTx{ttVAULT_CREATE, [](STObject&) {}},
3213 [&](Account const& a1, Account const& a2, Env& env) {
3214 Vault const vault{env};
3215 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3216 env(tx);
3217 return true;
3218 });
3219
3221 {"vault operation succeeded without modifying a vault"},
3222 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
3223 XRPAmount{},
3224 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
3226 [&](Account const& a1, Account const& a2, Env& env) {
3227 Vault const vault{env};
3228 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3229 env(tx);
3230 return true;
3231 });
3232
3234 {"vault operation succeeded without modifying a vault"},
3235 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
3236 XRPAmount{},
3237 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
3239 [&](Account const& a1, Account const& a2, Env& env) {
3240 Vault const vault{env};
3241 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3242 env(tx);
3243 return true;
3244 });
3245
3247 {"vault operation succeeded without modifying a vault"},
3248 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
3249 XRPAmount{},
3250 STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
3252 [&](Account const& a1, Account const& a2, Env& env) {
3253 Vault const vault{env};
3254 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3255 env(tx);
3256 return true;
3257 });
3258
3260 {"vault operation succeeded without modifying a vault"},
3261 [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
3262 XRPAmount{},
3263 STTx{ttVAULT_DELETE, [](STObject&) {}},
3265 [&](Account const& a1, Account const& a2, Env& env) {
3266 Vault const vault{env};
3267 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3268 env(tx);
3269 return true;
3270 });
3271
3273 {"updated vault must have shares"},
3274 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3275 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3276 auto sleVault = ac.view().peek(keylet);
3277 if (!sleVault)
3278 return false;
3279 (*sleVault)[sfAssetsMaximum] = 200;
3280 ac.view().update(sleVault);
3281
3282 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3283 if (!sleShares)
3284 return false;
3285 ac.view().erase(sleShares);
3286 return true;
3287 },
3288 XRPAmount{},
3289 STTx{ttVAULT_SET, [](STObject&) {}},
3291 [&](Account const& a1, Account const& a2, Env& env) {
3292 Vault const vault{env};
3293 auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
3294 env(tx);
3295 return true;
3296 });
3297
3299 {"vault operation succeeded without updating shares",
3300 "assets available must not be greater than assets outstanding"},
3301 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3302 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3303 auto sleVault = ac.view().peek(keylet);
3304 if (!sleVault)
3305 return false;
3306 (*sleVault)[sfAssetsTotal] = 9;
3307 ac.view().update(sleVault);
3308 return true;
3309 },
3310 XRPAmount{},
3311 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
3313 [&](Account const& a1, Account const& a2, Env& env) {
3314 Vault const vault{env};
3315 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3316 env(tx);
3317 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
3318 return true;
3319 });
3320
3322 {"set must not change assets outstanding",
3323 "set must not change assets available",
3324 "set must not change shares outstanding",
3325 "set must not change vault balance",
3326 "assets available must not be negative",
3327 "assets available must not be greater than assets outstanding",
3328 "assets outstanding must not be negative"},
3329 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3330 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3331 auto sleVault = ac.view().peek(keylet);
3332 if (!sleVault)
3333 return false;
3334 auto slePseudoAccount = ac.view().peek(keylet::account(*(*sleVault)[sfAccount]));
3335 if (!slePseudoAccount)
3336 return false;
3337 (*slePseudoAccount)[sfBalance] = *(*slePseudoAccount)[sfBalance] - 10;
3338 ac.view().update(slePseudoAccount);
3339
3340 // Move 10 drops to A4 to enforce total XRP balance
3341 auto sleA4 = ac.view().peek(keylet::account(a4.id()));
3342 if (!sleA4)
3343 return false;
3344 (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
3345 ac.view().update(sleA4);
3346
3347 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
3348 sample.assetsAvailable = (kDropsPerXrp * -100).value();
3349 sample.assetsTotal = (kDropsPerXrp * -200).value();
3350 sample.sharesTotal = -1;
3351 }));
3352 },
3353 XRPAmount{},
3354 STTx{ttVAULT_SET, [](STObject& tx) {}},
3356 precloseXrp,
3358
3360 {"violation of vault immutable data"},
3361 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3362 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3363 auto sleVault = ac.view().peek(keylet);
3364 if (!sleVault)
3365 return false;
3366 sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, MPTIssue(MPTID(42))});
3367 ac.view().update(sleVault);
3368 return true;
3369 },
3370 XRPAmount{},
3371 STTx{ttVAULT_SET, [](STObject& tx) {}},
3373 precloseXrp);
3374
3376 {"violation of vault immutable data"},
3377 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3378 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3379 auto sleVault = ac.view().peek(keylet);
3380 if (!sleVault)
3381 return false;
3382 sleVault->setAccountID(sfAccount, a2.id());
3383 ac.view().update(sleVault);
3384 return true;
3385 },
3386 XRPAmount{},
3387 STTx{ttVAULT_SET, [](STObject& tx) {}},
3389 precloseXrp);
3390
3392 {"violation of vault immutable data"},
3393 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3394 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3395 auto sleVault = ac.view().peek(keylet);
3396 if (!sleVault)
3397 return false;
3398 (*sleVault)[sfShareMPTID] = MPTID(42);
3399 ac.view().update(sleVault);
3400 return true;
3401 },
3402 XRPAmount{},
3403 STTx{ttVAULT_SET, [](STObject& tx) {}},
3405 precloseXrp);
3406
3408 {"vault transaction must not change loss unrealized",
3409 "set must not change assets outstanding"},
3410 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3411 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3412 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
3413 sample.lossUnrealized = 13;
3414 sample.assetsTotal = 20;
3415 }));
3416 },
3417 XRPAmount{},
3418 STTx{ttVAULT_SET, [](STObject& tx) {}},
3420 precloseXrp,
3422
3424 {"loss unrealized must not exceed the difference "
3425 "between assets outstanding and available",
3426 "vault transaction must not change loss unrealized"},
3427 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3428 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3429 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 100, [&](Adjustments& sample) {
3430 sample.lossUnrealized = 13;
3431 }));
3432 },
3433 XRPAmount{},
3434 STTx{
3435 ttVAULT_DEPOSIT, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
3437 precloseXrp,
3439
3440 // A negative loss unrealized must trip the invariant. ttLOAN_MANAGE is
3441 // allowed to change loss unrealized, so it isolates this check from the
3442 // "must not change loss unrealized" invariant. Gated behind
3443 // fixCleanup3_4_0 (see below).
3445 {"loss unrealized must not be negative"},
3446 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3447 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3448 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
3449 sample.lossUnrealized = -1;
3450 }));
3451 },
3452 XRPAmount{},
3453 STTx{ttLOAN_MANAGE, [](STObject& tx) {}},
3455 precloseXrp,
3457
3458 // Without fixCleanup3_4_0 the same state must NOT trip the invariant,
3459 // preserving pre-amendment behavior (no fork risk).
3461 makeEnv(defaultAmendments() - fixCleanup3_4_0),
3462 {},
3463 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3464 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3465 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
3466 sample.lossUnrealized = -1;
3467 }));
3468 },
3469 XRPAmount{},
3470 STTx{ttLOAN_MANAGE, [](STObject& tx) {}},
3472 precloseXrp,
3474
3476 {"set assets outstanding must not exceed assets maximum"},
3477 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3478 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3479 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
3480 sample.assetsMaximum = 1;
3481 }));
3482 },
3483 XRPAmount{},
3484 STTx{ttVAULT_SET, [](STObject& tx) {}},
3486 precloseXrp,
3488
3490 {"assets maximum must not be negative"},
3491 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3492 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3493 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
3494 sample.assetsMaximum = -1;
3495 }));
3496 },
3497 XRPAmount{},
3498 STTx{ttVAULT_SET, [](STObject& tx) {}},
3500 precloseXrp,
3502
3504 {"set must not change shares outstanding",
3505 "updated zero sized vault must have no assets outstanding",
3506 "updated zero sized vault must have no assets available"},
3507 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3508 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3509 auto sleVault = ac.view().peek(keylet);
3510 if (!sleVault)
3511 return false;
3512 ac.view().update(sleVault);
3513 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3514 if (!sleShares)
3515 return false;
3516 (*sleShares)[sfOutstandingAmount] = 0;
3517 ac.view().update(sleShares);
3518 return true;
3519 },
3520 XRPAmount{},
3521 STTx{ttVAULT_SET, [](STObject& tx) {}},
3523 precloseXrp,
3525
3527 {"updated shares must not exceed maximum"},
3528 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3529 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3530 auto sleVault = ac.view().peek(keylet);
3531 if (!sleVault)
3532 return false;
3533 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3534 if (!sleShares)
3535 return false;
3536 (*sleShares)[sfMaximumAmount] = 10;
3537 ac.view().update(sleShares);
3538
3539 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
3540 },
3541 XRPAmount{},
3542 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
3544 precloseXrp,
3546
3548 {"updated shares must not exceed maximum"},
3549 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3550 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3551 kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
3552
3553 auto sleVault = ac.view().peek(keylet);
3554 if (!sleVault)
3555 return false;
3556 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3557 if (!sleShares)
3558 return false;
3559 (*sleShares)[sfOutstandingAmount] = kMaxMpTokenAmount + 1;
3560 ac.view().update(sleShares);
3561 return true;
3562 },
3563 XRPAmount{},
3564 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
3566 precloseXrp,
3568
3569 testcase << "Vault create";
3571 {
3572 "created vault must be empty",
3573 "updated zero sized vault must have no assets outstanding",
3574 "create operation must not have updated a vault",
3575 },
3576 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3577 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3578 auto sleVault = ac.view().peek(keylet);
3579 if (!sleVault)
3580 return false;
3581 (*sleVault)[sfAssetsTotal] = 9;
3582 ac.view().update(sleVault);
3583 return true;
3584 },
3585 XRPAmount{},
3586 STTx{ttVAULT_CREATE, [](STObject&) {}},
3588 [&](Account const& a1, Account const& a2, Env& env) {
3589 Vault const vault{env};
3590 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3591 env(tx);
3592 return true;
3593 });
3594
3596 {
3597 "created vault must be empty",
3598 "updated zero sized vault must have no assets available",
3599 "assets available must not be greater than assets outstanding",
3600 "create operation must not have updated a vault",
3601 },
3602 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3603 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3604 auto sleVault = ac.view().peek(keylet);
3605 if (!sleVault)
3606 return false;
3607 (*sleVault)[sfAssetsAvailable] = 9;
3608 ac.view().update(sleVault);
3609 return true;
3610 },
3611 XRPAmount{},
3612 STTx{ttVAULT_CREATE, [](STObject&) {}},
3614 [&](Account const& a1, Account const& a2, Env& env) {
3615 Vault const vault{env};
3616 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3617 env(tx);
3618 return true;
3619 });
3620
3622 {
3623 "created vault must be empty",
3624 "loss unrealized must not exceed the difference between assets "
3625 "outstanding and available",
3626 "vault transaction must not change loss unrealized",
3627 "create operation must not have updated a vault",
3628 },
3629 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3630 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3631 auto sleVault = ac.view().peek(keylet);
3632 if (!sleVault)
3633 return false;
3634 (*sleVault)[sfLossUnrealized] = 1;
3635 ac.view().update(sleVault);
3636 return true;
3637 },
3638 XRPAmount{},
3639 STTx{ttVAULT_CREATE, [](STObject&) {}},
3641 [&](Account const& a1, Account const& a2, Env& env) {
3642 Vault const vault{env};
3643 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3644 env(tx);
3645 return true;
3646 });
3647
3649 {
3650 "created vault must be empty",
3651 "create operation must not have updated a vault",
3652 },
3653 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3654 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3655 auto sleVault = ac.view().peek(keylet);
3656 if (!sleVault)
3657 return false;
3658 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3659 if (!sleShares)
3660 return false;
3661 ac.view().update(sleVault);
3662 (*sleShares)[sfOutstandingAmount] = 9;
3663 ac.view().update(sleShares);
3664 return true;
3665 },
3666 XRPAmount{},
3667 STTx{ttVAULT_CREATE, [](STObject&) {}},
3669 [&](Account const& a1, Account const& a2, Env& env) {
3670 Vault const vault{env};
3671 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3672 env(tx);
3673 return true;
3674 });
3675
3677 {
3678 "assets maximum must not be negative",
3679 "create operation must not have updated a vault",
3680 },
3681 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3682 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3683 auto sleVault = ac.view().peek(keylet);
3684 if (!sleVault)
3685 return false;
3686 (*sleVault)[sfAssetsMaximum] = Number(-1);
3687 ac.view().update(sleVault);
3688 return true;
3689 },
3690 XRPAmount{},
3691 STTx{ttVAULT_CREATE, [](STObject&) {}},
3693 [&](Account const& a1, Account const& a2, Env& env) {
3694 Vault const vault{env};
3695 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3696 env(tx);
3697 return true;
3698 });
3699
3701 {"create operation must not have updated a vault",
3702 "shares issuer and vault pseudo-account must be the same",
3703 "shares issuer must be a pseudo-account",
3704 "shares issuer pseudo-account must point back to the vault"},
3705 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3706 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3707 auto sleVault = ac.view().peek(keylet);
3708 if (!sleVault)
3709 return false;
3710 auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
3711 if (!sleShares)
3712 return false;
3713 ac.view().update(sleVault);
3714 (*sleShares)[sfIssuer] = a1.id();
3715 ac.view().update(sleShares);
3716 return true;
3717 },
3718 XRPAmount{},
3719 STTx{ttVAULT_CREATE, [](STObject&) {}},
3721 [&](Account const& a1, Account const& a2, Env& env) {
3722 Vault const vault{env};
3723 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
3724 env(tx);
3725 return true;
3726 });
3727
3729 {"vault created by a wrong transaction type", "account root created illegally"},
3730 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3731 // The code below will create a valid vault with (almost) all
3732 // the invariants holding. Except one: it is created by the
3733 // wrong transaction type.
3734 auto const sequence = ac.view().seq();
3735 auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
3736 auto sleVault = std::make_shared<SLE>(vaultKeylet);
3737 auto const vaultPage = ac.view().dirInsert(
3738 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
3739 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
3740
3741 auto pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
3742 // Create pseudo-account.
3743 auto sleAccount = std::make_shared<SLE>(keylet::account(pseudoId));
3744 sleAccount->setAccountID(sfAccount, pseudoId);
3745 sleAccount->setFieldAmount(sfBalance, STAmount{});
3746 std::uint32_t const seqno = //
3747 ac.view().rules().enabled(featureSingleAssetVault) //
3748 ? 0 //
3749 : sequence;
3750 sleAccount->setFieldU32(sfSequence, seqno);
3751 sleAccount->setFieldU32(
3752 sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
3753 sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
3754 ac.view().insert(sleAccount);
3755
3756 auto const sharesMptId = makeMptID(sequence, pseudoId);
3757 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
3758 auto sleShares = std::make_shared<SLE>(sharesKeylet);
3759 auto const sharesPage = ac.view().dirInsert(
3760 keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
3761 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
3762
3763 sleShares->at(sfFlags) = 0;
3764 sleShares->at(sfIssuer) = pseudoId;
3765 sleShares->at(sfOutstandingAmount) = 0;
3766 sleShares->at(sfSequence) = sequence;
3767
3768 sleVault->at(sfAccount) = pseudoId;
3769 sleVault->at(sfFlags) = 0;
3770 sleVault->at(sfSequence) = sequence;
3771 sleVault->at(sfOwner) = a1.id();
3772 sleVault->at(sfAssetsTotal) = Number(0);
3773 sleVault->at(sfAssetsAvailable) = Number(0);
3774 sleVault->at(sfLossUnrealized) = Number(0);
3775 sleVault->at(sfShareMPTID) = sharesMptId;
3776 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
3777
3778 ac.view().insert(sleVault);
3779 ac.view().insert(sleShares);
3780 return true;
3781 },
3782 XRPAmount{},
3783 STTx{ttVAULT_SET, [](STObject&) {}},
3785
3787 {"shares issuer and vault pseudo-account must be the same",
3788 "shares issuer pseudo-account must point back to the vault"},
3789 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3790 auto const sequence = ac.view().seq();
3791 auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
3792 auto sleVault = std::make_shared<SLE>(vaultKeylet);
3793 auto const vaultPage = ac.view().dirInsert(
3794 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
3795 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
3796
3797 auto pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
3798 // Create pseudo-account.
3799 auto sleAccount = std::make_shared<SLE>(keylet::account(pseudoId));
3800 sleAccount->setAccountID(sfAccount, pseudoId);
3801 sleAccount->setFieldAmount(sfBalance, STAmount{});
3802 std::uint32_t const seqno = //
3803 ac.view().rules().enabled(featureSingleAssetVault) //
3804 ? 0 //
3805 : sequence;
3806 sleAccount->setFieldU32(sfSequence, seqno);
3807 sleAccount->setFieldU32(
3808 sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
3809 // sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
3810 // Setting wrong vault key
3811 sleAccount->setFieldH256(sfVaultID, uint256(42));
3812 ac.view().insert(sleAccount);
3813
3814 auto const sharesMptId = makeMptID(sequence, pseudoId);
3815 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
3816 auto sleShares = std::make_shared<SLE>(sharesKeylet);
3817 auto const sharesPage = ac.view().dirInsert(
3818 keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
3819 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
3820
3821 sleShares->at(sfFlags) = 0;
3822 sleShares->at(sfIssuer) = pseudoId;
3823 sleShares->at(sfOutstandingAmount) = 0;
3824 sleShares->at(sfSequence) = sequence;
3825
3826 // sleVault->at(sfAccount) = pseudoId;
3827 // Setting wrong pseudo account ID
3828 sleVault->at(sfAccount) = a2.id();
3829 sleVault->at(sfFlags) = 0;
3830 sleVault->at(sfSequence) = sequence;
3831 sleVault->at(sfOwner) = a1.id();
3832 sleVault->at(sfAssetsTotal) = Number(0);
3833 sleVault->at(sfAssetsAvailable) = Number(0);
3834 sleVault->at(sfLossUnrealized) = Number(0);
3835 sleVault->at(sfShareMPTID) = sharesMptId;
3836 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
3837
3838 ac.view().insert(sleVault);
3839 ac.view().insert(sleShares);
3840 return true;
3841 },
3842 XRPAmount{},
3843 STTx{ttVAULT_CREATE, [](STObject&) {}},
3845
3847 {"shares issuer and vault pseudo-account must be the same", "shares issuer must exist"},
3848 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3849 auto const sequence = ac.view().seq();
3850 auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
3851 auto sleVault = std::make_shared<SLE>(vaultKeylet);
3852 auto const vaultPage = ac.view().dirInsert(
3853 keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
3854 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
3855
3856 auto const sharesMptId = makeMptID(sequence, a2.id());
3857 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
3858 auto sleShares = std::make_shared<SLE>(sharesKeylet);
3859 auto const sharesPage = ac.view().dirInsert(
3860 keylet::ownerDir(a2.id()), sharesKeylet, describeOwnerDir(a2.id()));
3861 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
3862
3863 sleShares->at(sfFlags) = 0;
3864 // Setting wrong pseudo account ID
3865 sleShares->at(sfIssuer) = AccountID(42);
3866 sleShares->at(sfOutstandingAmount) = 0;
3867 sleShares->at(sfSequence) = sequence;
3868
3869 sleVault->at(sfAccount) = a2.id();
3870 sleVault->at(sfFlags) = 0;
3871 sleVault->at(sfSequence) = sequence;
3872 sleVault->at(sfOwner) = a1.id();
3873 sleVault->at(sfAssetsTotal) = Number(0);
3874 sleVault->at(sfAssetsAvailable) = Number(0);
3875 sleVault->at(sfLossUnrealized) = Number(0);
3876 sleVault->at(sfShareMPTID) = sharesMptId;
3877 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
3878
3879 ac.view().insert(sleVault);
3880 ac.view().insert(sleShares);
3881 return true;
3882 },
3883 XRPAmount{},
3884 STTx{ttVAULT_CREATE, [](STObject&) {}},
3886
3887 testcase << "Vault deposit";
3889 {"deposit must change vault balance"},
3890 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3891 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3892 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) {
3893 sample.vaultAssets.reset();
3894 }));
3895 },
3896 XRPAmount{},
3897 STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
3899 precloseXrp);
3900
3902 {"deposit assets outstanding must not exceed assets maximum"},
3903 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3904 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3905 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 200, [&](Adjustments& sample) {
3906 sample.assetsMaximum = 1;
3907 }));
3908 },
3909 XRPAmount{},
3910 STTx{
3911 ttVAULT_DEPOSIT, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
3913 precloseXrp,
3915
3916 // This really convoluted unit tests makes the zero balance on the
3917 // depositor, by sending them the same amount as the transaction fee.
3918 // The operation makes no sense, but the defensive check in
3919 // ValidVault::finalize is otherwise impossible to trigger.
3921 {"deposit must increase vault balance", "deposit must change depositor balance"},
3922 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3923 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3924
3925 // Move 10 drops to A4 to enforce total XRP balance
3926 auto sleA4 = ac.view().peek(keylet::account(a4.id()));
3927 if (!sleA4)
3928 return false;
3929 (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
3930 ac.view().update(sleA4);
3931
3932 return kAdjust(ac.view(), keylet, kArgs(a3.id(), -10, [&](Adjustments& sample) {
3933 sample.accountAssets->amount = -100;
3934 }));
3935 },
3936 XRPAmount{100},
3937 STTx{
3938 ttVAULT_DEPOSIT,
3939 [&](STObject& tx) {
3940 tx[sfFee] = XRPAmount(100);
3941 tx[sfAccount] = a3.id();
3942 }},
3944 precloseXrp);
3945
3947 {"deposit must increase vault balance",
3948 "deposit must decrease depositor balance",
3949 "deposit must change vault and depositor balance by equal amount",
3950 "deposit and assets outstanding must add up",
3951 "deposit and assets available must add up"},
3952 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3953 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3954
3955 // Move 10 drops from A2 to A3 to enforce total XRP balance
3956 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
3957 if (!sleA3)
3958 return false;
3959 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10;
3960 ac.view().update(sleA3);
3961
3962 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
3963 sample.vaultAssets = -20;
3964 sample.accountAssets->amount = 10;
3965 }));
3966 },
3967 XRPAmount{},
3968 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
3970 precloseXrp,
3972
3974 {"deposit must change depositor balance"},
3975 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3976 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3977
3978 // Move 10 drops from A3 to vault to enforce total XRP balance
3979 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
3980 if (!sleA3)
3981 return false;
3982 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 10;
3983 ac.view().update(sleA3);
3984
3985 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
3986 sample.accountAssets->amount = 0;
3987 }));
3988 },
3989 XRPAmount{},
3990 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
3992 precloseXrp,
3994
3996 {"deposit must change depositor shares"},
3997 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
3998 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
3999 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
4000 sample.accountShares.reset();
4001 }));
4002 },
4003 XRPAmount{},
4004 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
4006 precloseXrp,
4008
4010 {"deposit must change vault shares"},
4011 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4012 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4013
4014 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments& sample) {
4015 sample.sharesTotal = 0;
4016 }));
4017 },
4018 XRPAmount{},
4019 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
4021 precloseXrp,
4023
4025 {"deposit must increase depositor shares",
4026 "deposit must change depositor and vault shares by equal amount",
4027 "deposit must not change vault balance by more than deposited "
4028 "amount"},
4029 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4030 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4031 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
4032 sample.accountShares->amount = -5;
4033 sample.sharesTotal = -10;
4034 }));
4035 },
4036 XRPAmount{},
4037 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(5); }},
4039 precloseXrp,
4041
4043 {"deposit and assets outstanding must add up"},
4044 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4045 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
4046 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
4047 ac.view().update(sleA3);
4048
4049 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4050 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
4051 sample.assetsTotal = 11;
4052 }));
4053 },
4054 XRPAmount{2000},
4055 STTx{
4056 ttVAULT_DEPOSIT,
4057 [&](STObject& tx) {
4058 tx[sfAmount] = XRPAmount(10);
4059 tx[sfDelegate] = a3.id();
4060 tx[sfFee] = XRPAmount(2000);
4061 }},
4063 precloseXrp,
4065
4067 {"deposit and assets outstanding must add up",
4068 "deposit and assets available must add up"},
4069 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4070 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4071 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
4072 sample.assetsTotal = 7;
4073 sample.assetsAvailable = 7;
4074 }));
4075 },
4076 XRPAmount{},
4077 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
4079 precloseXrp,
4081
4082 testcase << "Vault withdrawal";
4084 {"withdrawal must change vault balance"},
4085 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4086 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4087 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) {
4088 sample.vaultAssets.reset();
4089 }));
4090 },
4091 XRPAmount{},
4092 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
4094 precloseXrp);
4095
4096 // Almost identical to the really convoluted test for deposit, where the
4097 // depositor spends only the transaction fee. In case of withdrawal,
4098 // this test is almost the same as normal withdrawal where the
4099 // sfDestination would have been A4, but has been omitted.
4101 {"withdrawal must change one destination balance"},
4102 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4103 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4104
4105 // Move 10 drops to A4 to enforce total XRP balance
4106 auto sleA4 = ac.view().peek(keylet::account(a4.id()));
4107 if (!sleA4)
4108 return false;
4109 (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
4110 ac.view().update(sleA4);
4111
4112 return kAdjust(ac.view(), keylet, kArgs(a3.id(), -10, [&](Adjustments& sample) {
4113 sample.accountAssets->amount = -100;
4114 }));
4115 },
4116 XRPAmount{100},
4117 STTx{
4118 ttVAULT_WITHDRAW,
4119 [&](STObject& tx) {
4120 tx[sfFee] = XRPAmount(100);
4121 tx[sfAccount] = a3.id();
4122 // This commented out line causes the invariant violation.
4123 // tx[sfDestination] = A4.id();
4124 }},
4126 precloseXrp);
4127
4129 {
4130 "withdrawal must change vault and destination balance by equal amount",
4131 "withdrawal must decrease vault balance",
4132 "withdrawal must increase destination balance",
4133 "withdrawal and assets outstanding must add up",
4134 "withdrawal and assets available must add up",
4135 },
4136 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4137 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4138
4139 // Move 10 drops from A2 to A3 to enforce total XRP balance
4140 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
4141 if (!sleA3)
4142 return false;
4143 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10;
4144 ac.view().update(sleA3);
4145
4146 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4147 sample.vaultAssets = 10;
4148 sample.accountAssets->amount = -20;
4149 }));
4150 },
4151 XRPAmount{},
4152 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
4154 precloseXrp,
4156
4158 {"withdrawal must change one destination balance"},
4159 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4160 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4161 if (!kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4162 *sample.vaultAssets -= 5;
4163 })))
4164 return false;
4165 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
4166 if (!sleA3)
4167 return false;
4168 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 5;
4169 ac.view().update(sleA3);
4170 return true;
4171 },
4172 XRPAmount{},
4173 STTx{ttVAULT_WITHDRAW, [&](STObject& tx) { tx.setAccountID(sfDestination, a3.id()); }},
4175 precloseXrp,
4177
4179 {"withdrawal must change depositor shares"},
4180 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4181 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4182 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4183 sample.accountShares.reset();
4184 }));
4185 },
4186 XRPAmount{},
4187 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
4189 precloseXrp,
4191
4193 {"withdrawal must change vault shares"},
4194 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4195 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4196 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [](Adjustments& sample) {
4197 sample.sharesTotal = 0;
4198 }));
4199 },
4200 XRPAmount{},
4201 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
4203 precloseXrp,
4205
4207 {"withdrawal must decrease depositor shares",
4208 "withdrawal must change depositor and vault shares by equal "
4209 "amount"},
4210 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4211 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4212 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4213 sample.accountShares->amount = 5;
4214 sample.sharesTotal = 10;
4215 }));
4216 },
4217 XRPAmount{},
4218 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
4220 precloseXrp,
4222
4224 {"withdrawal and assets outstanding must add up",
4225 "withdrawal and assets available must add up"},
4226 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4227 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4228 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4229 sample.assetsTotal = -15;
4230 sample.assetsAvailable = -15;
4231 }));
4232 },
4233 XRPAmount{},
4234 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
4236 precloseXrp,
4238
4240 {"withdrawal and assets outstanding must add up"},
4241 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4242 auto sleA3 = ac.view().peek(keylet::account(a3.id()));
4243 (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
4244 ac.view().update(sleA3);
4245
4246 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4247 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4248 sample.assetsTotal = -7;
4249 }));
4250 },
4251 XRPAmount{2000},
4252 STTx{
4253 ttVAULT_WITHDRAW,
4254 [&](STObject& tx) {
4255 tx[sfAmount] = XRPAmount(10);
4256 tx[sfDelegate] = a3.id();
4257 tx[sfFee] = XRPAmount(2000);
4258 }},
4260 precloseXrp,
4262
4263 auto const precloseMpt = [&](Account const& a1, Account const& a2, Env& env) -> bool {
4264 env.fund(XRP(1000), a3, a4);
4265
4266 // Create MPT asset
4267 {
4268 json::Value jv;
4269 jv[sfAccount] = a3.human();
4270 jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
4271 jv[sfFlags] = tfMPTCanTransfer;
4272 env(jv);
4273 env.close();
4274 }
4275
4276 auto const mptID = makeMptID(env.seq(a3) - 1, a3);
4277 Asset const asset = MPTIssue(mptID);
4278 // Authorize A1 A2 A4
4279 {
4280 json::Value jv;
4281 jv[sfAccount] = a1.human();
4282 jv[sfTransactionType] = jss::MPTokenAuthorize;
4283 jv[sfMPTokenIssuanceID] = to_string(mptID);
4284 env(jv);
4285 jv[sfAccount] = a2.human();
4286 env(jv);
4287 jv[sfAccount] = a4.human();
4288 env(jv);
4289
4290 env.close();
4291 }
4292 // Send tokens to A1 A2 A4
4293 {
4294 env(pay(a3, a1, asset(1000)));
4295 env(pay(a3, a2, asset(1000)));
4296 env(pay(a3, a4, asset(1000)));
4297 env.close();
4298 }
4299
4300 Vault const vault{env};
4301 auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
4302 env(tx);
4303 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = asset(10)}));
4304 env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = asset(10)}));
4305 env(vault.deposit({.depositor = a4, .id = keylet.key, .amount = asset(10)}));
4306 return true;
4307 };
4308
4310 {"withdrawal must decrease depositor shares",
4311 "withdrawal must change depositor and vault shares by equal "
4312 "amount"},
4313 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4314 auto const keylet =
4315 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
4316 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
4317 sample.accountShares->amount = 5;
4318 }));
4319 },
4320 XRPAmount{},
4321 STTx{ttVAULT_WITHDRAW, [&](STObject& tx) { tx[sfAccount] = a3.id(); }},
4323 precloseMpt,
4325
4326 testcase << "Vault clawback";
4328 {"clawback must change vault balance"},
4329 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4330 auto const keylet =
4331 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
4332 return kAdjust(ac.view(), keylet, kArgs(a2.id(), -1, [&](Adjustments& sample) {
4333 sample.vaultAssets.reset();
4334 }));
4335 },
4336 XRPAmount{},
4337 STTx{ttVAULT_CLAWBACK, [&](STObject& tx) { tx[sfAccount] = a3.id(); }},
4339 precloseMpt);
4340
4341 // Not the same as below check: attempt to clawback XRP
4343 {"clawback may only be performed by the asset issuer"},
4344 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4345 auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
4346 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {}));
4347 },
4348 XRPAmount{},
4349 STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
4351 precloseXrp);
4352
4353 // Not the same as above check: attempt to clawback MPT by bad account
4355 {"clawback may only be performed by the asset issuer"},
4356 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4357 auto const keylet =
4358 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
4359 return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {}));
4360 },
4361 XRPAmount{},
4362 STTx{ttVAULT_CLAWBACK, [&](STObject& tx) { tx[sfAccount] = a4.id(); }},
4364 precloseMpt);
4365
4367 {"clawback must decrease vault balance",
4368 "clawback must decrease holder shares",
4369 "clawback must change vault shares"},
4370 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4371 auto const keylet =
4372 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
4373 return kAdjust(ac.view(), keylet, kArgs(a4.id(), 10, [&](Adjustments& sample) {
4374 sample.sharesTotal = 0;
4375 }));
4376 },
4377 XRPAmount{},
4378 STTx{
4379 ttVAULT_CLAWBACK,
4380 [&](STObject& tx) {
4381 tx[sfAccount] = a3.id();
4382 tx[sfHolder] = a4.id();
4383 }},
4385 precloseMpt);
4386
4388 {"clawback must change holder shares"},
4389 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4390 auto const keylet =
4391 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
4392 return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) {
4393 sample.accountShares.reset();
4394 }));
4395 },
4396 XRPAmount{},
4397 STTx{
4398 ttVAULT_CLAWBACK,
4399 [&](STObject& tx) {
4400 tx[sfAccount] = a3.id();
4401 tx[sfHolder] = a4.id();
4402 }},
4404 precloseMpt);
4405
4407 {"clawback must change holder and vault shares by equal amount",
4408 "clawback and assets outstanding must add up",
4409 "clawback and assets available must add up"},
4410 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4411 auto const keylet =
4412 keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
4413 return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) {
4414 sample.accountShares->amount = -8;
4415 sample.assetsTotal = -7;
4416 sample.assetsAvailable = -7;
4417 }));
4418 },
4419 XRPAmount{},
4420 STTx{
4421 ttVAULT_CLAWBACK,
4422 [&](STObject& tx) {
4423 tx[sfAccount] = a3.id();
4424 tx[sfHolder] = a4.id();
4425 }},
4427 precloseMpt);
4428
4429 // ─────────────────────────────────────────────────────────────
4430 // Closed-ended vault invariants added in ValidVault::finalize (create must supply both
4431 // dates and satisfy the redemption-buffer gap), deposit only in Subscription / NoPhase,
4432 // withdraw not in Investment, loan origination only in Investment.
4433
4434 using d = NetClock::duration;
4435 using tp = NetClock::time_point;
4436
4437 auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded);
4438
4439 // Vault keylet captured by precloseClosedEnded so precheck does not have to rederive it
4440 // from ac.view().seq(), which depends on how many env.close() calls preclose issued.
4441 Keylet closedEndedKeylet = keylet::amendments();
4442
4443 // Preclose that creates a closed-ended vault (in Subscription), optionally seeds it with
4444 // three deposits (so a1/a2/a3 hold a share MPToken that kAdjust can then adjust), and
4445 // optionally advances parent close time past SubscriptionDate. A negative @p advanceBySub
4446 // leaves the vault in Subscription.
4447 auto const precloseClosedEnded = [&](std::int32_t advanceBySub, bool doDeposit) {
4448 return [&, advanceBySub, doDeposit](
4449 Account const& a1, Account const& a2, Env& env) -> bool {
4450 env.fund(XRP(1000), a3, a4);
4451 auto const sub = env.now().time_since_epoch().count() + 60;
4452 auto const red = sub + kMinInvestmentPeriod + 1'000'000;
4453 Vault const vault{env};
4454 auto [tx, keylet] = vault.create(
4455 {.owner = a1,
4456 .asset = xrpIssue(),
4457 .vaultKind = closedEnded,
4458 .subscriptionDate = sub,
4459 .redemptionDate = red});
4460 env(tx);
4461 closedEndedKeylet = keylet;
4462 if (doDeposit)
4463 {
4464 env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
4465 env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)}));
4466 env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)}));
4467 }
4468 if (advanceBySub >= 0)
4469 env.close(tp{d{sub + advanceBySub}});
4470 return true;
4471 };
4472 };
4473
4474 // Manually insert a bare closed-ended vault (+ pseudo-account + share MPTokenIssuance)
4475 // directly into the view, bypassing the transactor path. Used to synthesize ttVAULT_CREATE
4476 // states no legitimate transactor would produce.
4477 auto const insertBareClosedEndedVault =
4478 [closedEnded](
4479 ApplyContext& ac,
4480 Account const& owner,
4481 std::optional<std::uint32_t> subscriptionDate,
4482 std::optional<std::uint32_t> redemptionDate) -> bool {
4483 auto const sequence = ac.view().seq();
4484 auto const vaultKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(sequence));
4485 auto sleVault = std::make_shared<SLE>(vaultKeylet);
4486 auto const vaultPage = ac.view().dirInsert(
4487 keylet::ownerDir(owner.id()), sleVault->key(), describeOwnerDir(owner.id()));
4488 if (!vaultPage)
4489 return false;
4490 sleVault->setFieldU64(sfOwnerNode, *vaultPage);
4491
4492 auto const pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
4493 auto sleAccount = std::make_shared<SLE>(keylet::account(pseudoId));
4494 sleAccount->setAccountID(sfAccount, pseudoId);
4495 sleAccount->setFieldAmount(sfBalance, STAmount{});
4496 sleAccount->setFieldU32(sfSequence, 0);
4497 sleAccount->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
4498 sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
4499 ac.view().insert(sleAccount);
4500
4501 auto const sharesMptId = makeMptID(sequence, pseudoId);
4502 auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
4503 auto sleShares = std::make_shared<SLE>(sharesKeylet);
4504 auto const sharesPage = ac.view().dirInsert(
4505 keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
4506 if (!sharesPage)
4507 return false;
4508 sleShares->setFieldU64(sfOwnerNode, *sharesPage);
4509 sleShares->at(sfFlags) = 0;
4510 sleShares->at(sfIssuer) = pseudoId;
4511 sleShares->at(sfOutstandingAmount) = 0;
4512 sleShares->at(sfSequence) = sequence;
4513
4514 sleVault->at(sfAccount) = pseudoId;
4515 sleVault->at(sfFlags) = 0;
4516 sleVault->at(sfSequence) = sequence;
4517 sleVault->at(sfOwner) = owner.id();
4518 sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, Asset{xrpIssue()}});
4519 sleVault->at(sfAssetsTotal) = Number(0);
4520 sleVault->at(sfAssetsAvailable) = Number(0);
4521 sleVault->at(sfLossUnrealized) = Number(0);
4522 sleVault->at(sfShareMPTID) = sharesMptId;
4523 sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
4524 sleVault->at(sfVaultKind) = closedEnded;
4525 if (subscriptionDate)
4526 sleVault->at(sfSubscriptionDate) = *subscriptionDate;
4527 if (redemptionDate)
4528 sleVault->at(sfRedemptionDate) = *redemptionDate;
4529
4530 ac.view().insert(sleVault);
4531 ac.view().insert(sleShares);
4532 return true;
4533 };
4534
4535 testcase << "Vault create closed-ended";
4536
4537 // A fresh closed-ended vault must carry both SubscriptionDate and RedemptionDate.
4539 {"closed-ended vault must have SubscriptionDate and RedemptionDate"},
4540 [&](Account const& a1, Account const&, ApplyContext& ac) {
4541 return insertBareClosedEndedVault(ac, a1, std::nullopt, std::nullopt);
4542 },
4543 XRPAmount{},
4544 STTx{ttVAULT_CREATE, [](STObject&) {}},
4546
4547 // Gap smaller than MIN_INVESTMENT_PERIOD but with RedemptionDate > SubscriptionDate;
4548 // exercises the sub-minimum branch of the gap check.
4550 {"closed-ended vault RedemptionDate - SubscriptionDate must be "
4551 "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"},
4552 [&](Account const& a1, Account const&, ApplyContext& ac) {
4553 std::uint32_t const sub = 1'000'000'000;
4554 std::uint32_t const red = sub + kMinInvestmentPeriod - 1;
4555 return insertBareClosedEndedVault(ac, a1, sub, red);
4556 },
4557 XRPAmount{},
4558 STTx{ttVAULT_CREATE, [](STObject&) {}},
4560
4561 // RedemptionDate strictly before SubscriptionDate; the signed int64 gap is negative and
4562 // is caught by the sub-minimum branch of the gap check.
4564 {"closed-ended vault RedemptionDate - SubscriptionDate must be "
4565 "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"},
4566 [&](Account const& a1, Account const&, ApplyContext& ac) {
4567 std::uint32_t const sub = 1'000'000'000;
4568 std::uint32_t const red = sub - 1;
4569 return insertBareClosedEndedVault(ac, a1, sub, red);
4570 },
4571 XRPAmount{},
4572 STTx{ttVAULT_CREATE, [](STObject&) {}},
4574
4575 // Gap exactly MAX_INVESTMENT_PERIOD is out of range (bound is half-open on the right).
4577 {"closed-ended vault RedemptionDate - SubscriptionDate must be "
4578 "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"},
4579 [&](Account const& a1, Account const&, ApplyContext& ac) {
4580 std::uint32_t const sub = 1'000'000'000;
4581 std::uint32_t const red = sub + kMaxInvestmentPeriod;
4582 return insertBareClosedEndedVault(ac, a1, sub, red);
4583 },
4584 XRPAmount{},
4585 STTx{ttVAULT_CREATE, [](STObject&) {}},
4587
4588 testcase << "Vault deposit closed-ended";
4589
4590 // A deposit into a closed-ended vault that has advanced past SubscriptionDate. kArgs
4591 // simulates an otherwise valid deposit shape so only the phase invariant fires.
4593 {"deposit only allowed in Subscription or NoPhase"},
4594 [&](Account const&, Account const& a2, ApplyContext& ac) {
4595 return kAdjust(
4596 ac.view(), closedEndedKeylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
4597 },
4598 XRPAmount{},
4599 STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
4601 precloseClosedEnded(/*advanceBySub=*/1, /*doDeposit=*/true),
4603
4604 testcase << "Vault withdrawal closed-ended";
4605
4606 // A withdrawal from a closed-ended vault in the Investment phase.
4608 {"withdrawal not allowed during Investment phase"},
4609 [&](Account const&, Account const& a2, ApplyContext& ac) {
4610 return kAdjust(
4611 ac.view(), closedEndedKeylet, kArgs(a2.id(), -10, [](Adjustments&) {}));
4612 },
4613 XRPAmount{},
4614 STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
4616 precloseClosedEnded(/*advanceBySub=*/1, /*doDeposit=*/true),
4618
4619 testcase << "Vault loan set";
4620
4621 // ttLOAN_SET against a closed-ended vault that is not in Investment. finalizeLoanSet fires
4622 // on any vault mutation; touching the vault SLE with no field change is sufficient.
4624 {"loan origination only allowed in Investment phase"},
4625 [&](Account const&, Account const&, ApplyContext& ac) {
4626 auto sleVault = ac.view().peek(closedEndedKeylet);
4627 if (!sleVault)
4628 return false;
4629 ac.view().update(sleVault);
4630 return true;
4631 },
4632 XRPAmount{},
4633 STTx{ttLOAN_SET, [](STObject&) {}},
4635 precloseClosedEnded(/*advanceBySub=*/-1, /*doDeposit=*/false));
4636
4637 testcase << "Vault loan set - closed-ended final payment past "
4638 "RedemptionDate";
4639
4640 // A newly-created loan against a closed-ended vault must satisfy StartDate +
4641 // PaymentInterval * PaymentRemaining < RedemptionDate. LoanSet::preclaim enforces the same
4642 // bound; this test synthesises an invalid loan directly in the ApplyView so the invariant
4643 // catches it even when preclaim is bypassed.
4644 Keylet closedEndedBrokerKeylet = keylet::amendments();
4645 std::uint32_t closedEndedRed = 0;
4647 {"closed-ended loan final payment must precede RedemptionDate"},
4648 [&](Account const& a1, Account const&, ApplyContext& ac) {
4649 // Touch the vault so ValidVault::finalizeLoanSet sees an
4650 // entry in afterVault_; the vault is in Investment, so
4651 // finalizeLoanSet itself passes.
4652 auto sleVault = ac.view().peek(closedEndedKeylet);
4653 if (!sleVault)
4654 return false;
4655 ac.view().update(sleVault);
4656
4657 // Read the broker's next loan sequence to build the loan
4658 // keylet the same way LoanSet::doApply would.
4659 auto sleBroker = ac.view().peek(closedEndedBrokerKeylet);
4660 if (!sleBroker)
4661 return false;
4662 std::uint32_t const loanSeq = sleBroker->at(sfLoanSequence);
4663
4664 // Synthesize a Loan whose final scheduled payment lands
4665 // exactly at RedemptionDate: StartDate = red, interval = 60,
4666 // remaining = 1 => red + 60 >= red.
4667 auto sleLoan = std::make_shared<SLE>(
4668 keylet::loan(closedEndedBrokerKeylet.key, SeqProxy::rawSequence(loanSeq)));
4669 sleLoan->at(sfLoanBrokerID) = closedEndedBrokerKeylet.key;
4670 sleLoan->at(sfLoanSequence) = loanSeq;
4671 sleLoan->at(sfBorrower) = a1.id();
4672 sleLoan->at(sfStartDate) = closedEndedRed;
4673 sleLoan->at(sfPaymentInterval) = 60;
4674 sleLoan->at(sfPaymentRemaining) = 1;
4675 sleLoan->at(sfTotalValueOutstanding) = Number(100);
4676 sleLoan->at(sfPeriodicPayment) = Number(1);
4677 ac.view().insert(sleLoan);
4678 return true;
4679 },
4680 XRPAmount{},
4681 STTx{ttLOAN_SET, [](STObject&) {}},
4683 [&](Account const& a1, Account const&, Env& env) -> bool {
4684 auto const sub = env.now().time_since_epoch().count() + 60;
4685 auto const red = sub + kMinInvestmentPeriod + 1'000'000;
4686 closedEndedRed = red;
4687
4688 Vault const vault{env};
4689 auto [tx, keylet] = vault.create(
4690 {.owner = a1,
4691 .asset = xrpIssue(),
4692 .vaultKind = closedEnded,
4693 .subscriptionDate = sub,
4694 .redemptionDate = red});
4695 env(tx);
4696 closedEndedKeylet = keylet;
4697
4698 // Create the loan broker; LoanBrokerSet has no phase gate.
4699 closedEndedBrokerKeylet =
4700 keylet::loanBroker(a1.id(), SeqProxy::rawSequence(env.seq(a1)));
4701 env(loan_broker::set(a1, keylet.key));
4702
4703 // Advance parent close time into Investment so
4704 // ValidVault::finalizeLoanSet is satisfied.
4705 env.close(tp{d{sub + 1}});
4706 return true;
4707 });
4708 }
4709
4710 void
4712 {
4713 using namespace test::jtx;
4714 testcase << "MPT";
4715
4716 MPTIssue const nonCanonicalMPTIssue{makeMptID(1, AccountID(0x4985601))};
4717 auto const nonCanonicalMPTAmount = [&](SField const& field) {
4718 return STAmount{
4719 field,
4720 nonCanonicalMPTIssue,
4722 0,
4723 false,
4725 };
4726 auto const negativeMPTAmount = [&](SField const& field) {
4727 return STAmount{field, nonCanonicalMPTIssue, 2, 0, true, STAmount::Unchecked{}};
4728 };
4729 auto const nonCanonicalMPTPayment = [&]() {
4730 return STTx{ttPAYMENT, [&](STObject& tx) {
4731 tx.setFieldAmount(sfAmount, nonCanonicalMPTAmount(sfAmount));
4732 }};
4733 };
4734
4736 makeEnv(defaultAmendments() - fixCleanup3_2_0),
4737 {},
4738 [](Account const&, Account const&, ApplyContext&) { return true; },
4739 XRPAmount{},
4740 nonCanonicalMPTPayment(),
4742
4744 {{"ledger entry contains non-canonical MPT or XRP amount"}},
4745 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4746 auto const sle = ac.view().peek(keylet::account(a1.id()));
4747 if (!sle)
4748 return false;
4749
4750 auto sleNew = std::make_shared<SLE>(
4751 keylet::check(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
4752 sleNew->setAccountID(sfAccount, a1.id());
4753 sleNew->setAccountID(sfDestination, a2.id());
4754 sleNew->setFieldAmount(sfSendMax, nonCanonicalMPTAmount(sfSendMax));
4755 ac.view().insert(sleNew);
4756 return true;
4757 });
4758
4760 {{"ledger entry contains non-canonical MPT or XRP amount"}},
4761 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4762 auto const sle = ac.view().peek(keylet::account(a1.id()));
4763 if (!sle)
4764 return false;
4765
4766 auto sleNew = std::make_shared<SLE>(
4767 keylet::check(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
4768 sleNew->setAccountID(sfAccount, a1.id());
4769 sleNew->setAccountID(sfDestination, a2.id());
4770 sleNew->setFieldAmount(sfSendMax, negativeMPTAmount(sfSendMax));
4771 ac.view().insert(sleNew);
4772 return true;
4773 });
4774
4775 // MPT OutstandingAmount > MaximumAmount
4777 {{"OutstandingAmount overflow"}},
4778 [](Account const& a1, Account const&, ApplyContext& ac) {
4779 // mptissuance outstanding is negative
4780 auto const sle = ac.view().peek(keylet::account(a1.id()));
4781 if (!sle)
4782 return false;
4783
4784 MPTIssue const mpt{makeMptID(sle->getFieldU32(sfSequence), a1)};
4786 sleNew->setFieldU64(sfOutstandingAmount, 110);
4787 sleNew->setFieldU64(sfMaximumAmount, 100);
4788 ac.view().insert(sleNew);
4789 return true;
4790 });
4791
4792 // MPTToken amount doesn't add up to OutstandingAmount
4794 {{"invalid OutstandingAmount balance"}},
4795 [](Account const& a1, Account const& a2, ApplyContext& ac) {
4796 // mptissuance outstanding is negative
4797 auto const sle = ac.view().peek(keylet::account(a1.id()));
4798 if (!sle)
4799 return false;
4800
4801 MPTIssue const mpt{makeMptID(sle->getFieldU32(sfSequence), a1)};
4803 sleNew->setFieldU64(sfOutstandingAmount, 100);
4804 sleNew->setFieldU64(sfMaximumAmount, 100);
4805 ac.view().insert(sleNew);
4806
4807 sleNew = std::make_shared<SLE>(keylet::mptoken(mpt.getMptID(), a2));
4808 sleNew->setFieldU64(sfMPTAmount, 90);
4809 ac.view().insert(sleNew);
4810
4811 return true;
4812 });
4813
4814 // Overflow/Invalid balance on payment
4815 auto testPayment = [&](std::string const& log, auto&& update) {
4816 MPTID id;
4818 {{log}},
4819 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
4820 return update(id, ac, a1);
4821 },
4822 XRPAmount{},
4823 STTx{ttPAYMENT, [](STObject& tx) {}},
4825 [&](Account const& a1, Account const& a2, Env& env) {
4826 Account const gw("gw");
4827 env.fund(XRP(1'000), gw);
4828 MPTTester const mpt(
4829 {.env = env, .issuer = gw, .holders = {a1}, .pay = 100, .maxAmt = 100});
4830 id = mpt.issuanceID();
4831 return true;
4832 });
4833 };
4834 testPayment(
4835 "invalid OutstandingAmount balance",
4836 [&](MPTID const& id, ApplyContext& ac, Account const& a1) {
4837 auto sle = ac.view().peek(keylet::mptoken(id, a1));
4838 if (!sle)
4839 return false;
4840 sle->setFieldU64(sfMPTAmount, 101);
4841 ac.view().update(sle);
4842 return true;
4843 });
4844 testPayment(
4845 "OutstandingAmount overflow", [&](MPTID const& id, ApplyContext& ac, Account const&) {
4846 auto sle = ac.view().peek(keylet::mptokenIssuance(id));
4847 if (!sle)
4848 return false;
4849 sle->setFieldU64(sfOutstandingAmount, 101);
4850 ac.view().update(sle);
4851 return true;
4852 });
4853
4854 // Invalid IOU clawback delta must fail once MPTokensV2 enforces before/after validation.
4855 {
4856 Env env(*this, defaultAmendments());
4857 Account const issuer{"issuer"};
4858 Account const holder{"holder"};
4859 Account const other{"other"};
4860 env.fund(XRP(1'000), issuer, holder, other);
4861 auto const usd = issuer["USD"];
4862 env.trust(usd(100), holder);
4863 env(pay(issuer, holder, usd(100)));
4864 env.close();
4865
4867 std::move(env),
4868 holder,
4869 other,
4870 {{"Invariant failed: trustline clawback balance change is invalid"}},
4871 [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
4872 auto sle =
4873 ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
4874 if (!sle)
4875 return false;
4876
4877 STAmount balance{Issue{usd.currency, issuer.id()}, 80};
4878 if (holder.id() > issuer.id())
4879 balance.negate();
4880 sle->setFieldAmount(sfBalance, balance);
4881 ac.view().update(sle);
4882 return true;
4883 },
4884 XRPAmount{},
4885 STTx{
4886 ttCLAWBACK,
4887 [&](STObject& tx) {
4888 tx[sfAccount] = issuer.id();
4889 tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
4890 }},
4892 }
4893
4894 // Full IOU clawback may delete the trustline; missing after-SLE represents zero balance.
4895 {
4896 Env env(*this, defaultAmendments());
4897 Account const issuer{"issuer"};
4898 Account const holder{"holder"};
4899 Account const other{"other"};
4900 env.fund(XRP(1'000), issuer, holder, other);
4901 auto const usd = issuer["USD"];
4902 env.trust(usd(100), holder);
4903 env(pay(issuer, holder, usd(100)));
4904 env.close();
4905
4907 std::move(env),
4908 holder,
4909 other,
4910 {},
4911 [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
4912 auto const sle =
4913 ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
4914 if (!sle)
4915 return false;
4916
4917 ac.view().erase(sle);
4918 return true;
4919 },
4920 XRPAmount{},
4921 STTx{
4922 ttCLAWBACK,
4923 [&](STObject& tx) {
4924 tx[sfAccount] = issuer.id();
4925 tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 100};
4926 }},
4928 }
4929
4930 // Pre-MPTokensV2 invalid IOU clawback delta logs but remains non-enforcing.
4931 {
4932 Env env(*this, defaultAmendments() - featureMPTokensV2);
4933 Account const issuer{"issuer"};
4934 Account const holder{"holder"};
4935 Account const other{"other"};
4936 env.fund(XRP(1'000), issuer, holder, other);
4937 auto const usd = issuer["USD"];
4938 env.trust(usd(100), holder);
4939 env(pay(issuer, holder, usd(100)));
4940 env.close();
4941
4943 std::move(env),
4944 holder,
4945 other,
4946 {{"Invariant failed: trustline clawback balance change is invalid"}},
4947 [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
4948 auto sle =
4949 ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
4950 if (!sle)
4951 return false;
4952
4953 STAmount balance{Issue{usd.currency, issuer.id()}, 80};
4954 if (holder.id() > issuer.id())
4955 balance.negate();
4956 sle->setFieldAmount(sfBalance, balance);
4957 ac.view().update(sle);
4958 return true;
4959 },
4960 XRPAmount{},
4961 STTx{
4962 ttCLAWBACK,
4963 [&](STObject& tx) {
4964 tx[sfAccount] = issuer.id();
4965 tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
4966 }},
4968 }
4969
4970 // Invalid MPT clawback delta must fail when raw MPToken debit mismatches sfAmount.
4971 {
4972 Env env(*this, defaultAmendments());
4973 Account const issuer{"issuer"};
4974 Account const holder{"holder"};
4975 Account const other{"other"};
4976 env.fund(XRP(1'000), issuer, holder, other);
4977 MPTTester const mpt(
4978 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
4979 auto const id = mpt.issuanceID();
4980
4982 std::move(env),
4983 holder,
4984 other,
4985 {{"Invariant failed: MPT clawback balance change is invalid"}},
4986 [id](Account const& holder, Account const&, ApplyContext& ac) {
4987 auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
4988 auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
4989 if (!sleToken || !sleIssuance)
4990 return false;
4991
4992 sleToken->setFieldU64(sfMPTAmount, 80);
4993 sleIssuance->setFieldU64(sfOutstandingAmount, 80);
4994 ac.view().update(sleToken);
4995 ac.view().update(sleIssuance);
4996 return true;
4997 },
4998 XRPAmount{},
4999 STTx{
5000 ttCLAWBACK,
5001 [&](STObject& tx) {
5002 tx[sfAccount] = issuer.id();
5003 tx[sfHolder] = holder.id();
5004 tx[sfAmount] = STAmount{MPTIssue{id}, 10};
5005 }},
5007 }
5008
5009 // A clawback that mutates both IOU and MPT entries must fail under MPTokensV2.
5010 {
5011 Env env(*this, defaultAmendments());
5012 Account const issuer{"issuer"};
5013 Account const holder{"holder"};
5014 Account const other{"other"};
5015 env.fund(XRP(1'000), issuer, holder, other);
5016 auto const usd = issuer["USD"];
5017 env.trust(usd(100), holder);
5018 env(pay(issuer, holder, usd(100)));
5019 MPTTester const mpt(
5020 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
5021 auto const id = mpt.issuanceID();
5022
5024 std::move(env),
5025 holder,
5026 other,
5027 {{"Invariant failed: trustline and MPToken both changed"}},
5028 [issuer, usd, id](Account const& holder, Account const&, ApplyContext& ac) {
5029 auto const sleLine =
5030 ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
5031 auto const sleToken = ac.view().peek(keylet::mptoken(id, holder.id()));
5032 auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
5033 if (!sleLine || !sleToken || !sleIssuance)
5034 return false;
5035
5036 STAmount balance{Issue{usd.currency, issuer.id()}, 90};
5037 if (holder.id() > issuer.id())
5038 balance.negate();
5039 sleLine->setFieldAmount(sfBalance, balance);
5040 sleToken->setFieldU64(sfMPTAmount, 90);
5041 sleIssuance->setFieldU64(sfOutstandingAmount, 90);
5042 ac.view().update(sleLine);
5043 ac.view().update(sleToken);
5044 ac.view().update(sleIssuance);
5045 return true;
5046 },
5047 XRPAmount{},
5048 STTx{
5049 ttCLAWBACK,
5050 [&](STObject& tx) {
5051 tx[sfAccount] = issuer.id();
5052 tx[sfHolder] = holder.id();
5053 tx[sfAmount] = STAmount{MPTIssue{id}, 10};
5054 }},
5056 }
5057
5058 // Clawback that modifies a trustline other than the one implied by the
5059 // tx amount: clawbackTrustLineBalanceInHolderTerms returns nullopt for
5060 // the mismatched line.
5061 {
5062 Env env(*this, defaultAmendments());
5063 Account const issuer{"issuer"};
5064 Account const holder{"holder"};
5065 Account const other{"other"};
5066 env.fund(XRP(1'000), issuer, holder, other);
5067 auto const usd = issuer["USD"];
5068 auto const eur = issuer["EUR"];
5069 env.trust(eur(100), holder);
5070 env(pay(issuer, holder, eur(100)));
5071 env.close();
5072
5074 std::move(env),
5075 holder,
5076 other,
5077 {{"Invariant failed: trustline clawback changed the wrong line"}},
5078 [issuer, eur](Account const& holder, Account const&, ApplyContext& ac) {
5079 auto sle =
5080 ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), eur.currency));
5081 if (!sle)
5082 return false;
5083 STAmount balance{Issue{eur.currency, issuer.id()}, 90};
5084 if (holder.id() > issuer.id())
5085 balance.negate();
5086 sle->setFieldAmount(sfBalance, balance);
5087 ac.view().update(sle);
5088 return true;
5089 },
5090 XRPAmount{},
5091 STTx{
5092 ttCLAWBACK,
5093 [&](STObject& tx) {
5094 tx[sfAccount] = issuer.id();
5095 tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
5096 }},
5098 }
5099
5100 // Clawback leaving the holder's balance negative.
5101 {
5102 Env env(*this, defaultAmendments());
5103 Account const issuer{"issuer"};
5104 Account const holder{"holder"};
5105 Account const other{"other"};
5106 env.fund(XRP(1'000), issuer, holder, other);
5107 auto const usd = issuer["USD"];
5108 env.trust(usd(100), holder);
5109 env(pay(issuer, holder, usd(100)));
5110 env.close();
5111
5113 std::move(env),
5114 holder,
5115 other,
5116 {{"Invariant failed: trustline or MPT balance is negative"}},
5117 [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
5118 auto sle =
5119 ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
5120 if (!sle)
5121 return false;
5122 // Make the holder's balance negative from their perspective.
5123 STAmount balance{Issue{usd.currency, issuer.id()}, 80};
5124 if (holder.id() < issuer.id())
5125 balance.negate();
5126 sle->setFieldAmount(sfBalance, balance);
5127 ac.view().update(sle);
5128 return true;
5129 },
5130 XRPAmount{},
5131 STTx{
5132 ttCLAWBACK,
5133 [&](STObject& tx) {
5134 tx[sfAccount] = issuer.id();
5135 tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
5136 }},
5138 }
5139
5140 // IOU-amount clawback while only an MPToken changed: no trustline was
5141 // recorded, so iou_.before is empty.
5142 {
5143 Env env(*this, defaultAmendments());
5144 Account const issuer{"issuer"};
5145 Account const holder{"holder"};
5146 Account const other{"other"};
5147 env.fund(XRP(1'000), issuer, holder, other);
5148 auto const usd = issuer["USD"];
5149 MPTTester const mpt(
5150 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
5151 auto const id = mpt.issuanceID();
5152
5154 std::move(env),
5155 holder,
5156 other,
5157 {{"Invariant failed: trustline clawback changed the wrong line"}},
5158 [id](Account const& holder, Account const&, ApplyContext& ac) {
5159 auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
5160 auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
5161 if (!sleToken || !sleIssuance)
5162 return false;
5163 sleToken->setFieldU64(sfMPTAmount, 90);
5164 sleIssuance->setFieldU64(sfOutstandingAmount, 90);
5165 ac.view().update(sleToken);
5166 ac.view().update(sleIssuance);
5167 return true;
5168 },
5169 XRPAmount{},
5170 STTx{
5171 ttCLAWBACK,
5172 [&](STObject& tx) {
5173 tx[sfAccount] = issuer.id();
5174 tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
5175 }},
5177 }
5178
5179 // Valid trustline change but a zero clawback amount.
5180 {
5181 Env env(*this, defaultAmendments());
5182 Account const issuer{"issuer"};
5183 Account const holder{"holder"};
5184 Account const other{"other"};
5185 env.fund(XRP(1'000), issuer, holder, other);
5186 auto const usd = issuer["USD"];
5187 env.trust(usd(100), holder);
5188 env(pay(issuer, holder, usd(100)));
5189 env.close();
5190
5192 std::move(env),
5193 holder,
5194 other,
5195 {{"Invariant failed: trustline clawback amount is invalid"}},
5196 [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
5197 auto sle =
5198 ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
5199 if (!sle)
5200 return false;
5201 STAmount balance{Issue{usd.currency, issuer.id()}, 90};
5202 if (holder.id() > issuer.id())
5203 balance.negate();
5204 sle->setFieldAmount(sfBalance, balance);
5205 ac.view().update(sle);
5206 return true;
5207 },
5208 XRPAmount{},
5209 STTx{
5210 ttCLAWBACK,
5211 [&](STObject& tx) {
5212 tx[sfAccount] = issuer.id();
5213 tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 0};
5214 }},
5216 }
5217
5218 // MPT clawback tx missing the Holder field.
5219 {
5220 Env env(*this, defaultAmendments());
5221 Account const issuer{"issuer"};
5222 Account const holder{"holder"};
5223 Account const other{"other"};
5224 env.fund(XRP(1'000), issuer, holder, other);
5225 MPTTester const mpt(
5226 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
5227 auto const id = mpt.issuanceID();
5228
5230 std::move(env),
5231 holder,
5232 other,
5233 {{"Invariant failed: MPT clawback missing holder"}},
5234 [id](Account const& holder, Account const&, ApplyContext& ac) {
5235 auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
5236 auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
5237 if (!sleToken || !sleIssuance)
5238 return false;
5239 sleToken->setFieldU64(sfMPTAmount, 90);
5240 sleIssuance->setFieldU64(sfOutstandingAmount, 90);
5241 ac.view().update(sleToken);
5242 ac.view().update(sleIssuance);
5243 return true;
5244 },
5245 XRPAmount{},
5246 STTx{
5247 ttCLAWBACK,
5248 [&](STObject& tx) {
5249 tx[sfAccount] = issuer.id();
5250 tx[sfAmount] = STAmount{MPTIssue{id}, 10};
5251 }},
5253 }
5254
5255 // MPT clawback where the holder's MPToken was deleted (after is empty).
5256 {
5257 Env env(*this, defaultAmendments());
5258 Account const issuer{"issuer"};
5259 Account const holder{"holder"};
5260 Account const other{"other"};
5261 env.fund(XRP(1'000), issuer, holder, other);
5262 MPTTester const mpt(
5263 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
5264 auto const id = mpt.issuanceID();
5265
5267 std::move(env),
5268 holder,
5269 other,
5270 {{"Invariant failed: MPT clawback token is missing"}},
5271 [id](Account const& holder, Account const&, ApplyContext& ac) {
5272 auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
5273 auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
5274 if (!sleToken || !sleIssuance)
5275 return false;
5276 // Keep the issuance consistent after removing the token.
5277 sleIssuance->setFieldU64(sfOutstandingAmount, 0);
5278 ac.view().update(sleIssuance);
5279 ac.view().erase(sleToken);
5280 return true;
5281 },
5282 XRPAmount{},
5283 STTx{
5284 ttCLAWBACK,
5285 [&](STObject& tx) {
5286 tx[sfAccount] = issuer.id();
5287 tx[sfHolder] = holder.id();
5288 tx[sfAmount] = STAmount{MPTIssue{id}, 10};
5289 }},
5291 }
5292
5293 // MPT clawback that changed a different holder's MPToken.
5294 {
5295 Env env(*this, defaultAmendments());
5296 Account const issuer{"issuer"};
5297 Account const holder{"holder"};
5298 Account const other{"other"};
5299 env.fund(XRP(1'000), issuer, holder, other);
5300 MPTTester const mpt(
5301 {.env = env,
5302 .issuer = issuer,
5303 .holders = {holder, other},
5304 .pay = 100,
5305 .maxAmt = 200});
5306 auto const id = mpt.issuanceID();
5307
5309 std::move(env),
5310 holder,
5311 other,
5312 {{"Invariant failed: MPT clawback changed the wrong token"}},
5313 [id](Account const&, Account const& other, ApplyContext& ac) {
5314 auto const sleToken = ac.view().peek(keylet::mptoken(id, other));
5315 auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
5316 if (!sleToken || !sleIssuance)
5317 return false;
5318 sleToken->setFieldU64(sfMPTAmount, 90);
5319 sleIssuance->setFieldU64(sfOutstandingAmount, 190);
5320 ac.view().update(sleToken);
5321 ac.view().update(sleIssuance);
5322 return true;
5323 },
5324 XRPAmount{},
5325 STTx{
5326 ttCLAWBACK,
5327 [&](STObject& tx) {
5328 tx[sfAccount] = issuer.id();
5329 tx[sfHolder] = holder.id();
5330 tx[sfAmount] = STAmount{MPTIssue{id}, 10};
5331 }},
5333 }
5334
5335 // Valid MPToken change but a zero MPT clawback amount.
5336 {
5337 Env env(*this, defaultAmendments());
5338 Account const issuer{"issuer"};
5339 Account const holder{"holder"};
5340 Account const other{"other"};
5341 env.fund(XRP(1'000), issuer, holder, other);
5342 MPTTester const mpt(
5343 {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
5344 auto const id = mpt.issuanceID();
5345
5347 std::move(env),
5348 holder,
5349 other,
5350 {{"Invariant failed: MPT clawback amount is invalid"}},
5351 [id](Account const& holder, Account const&, ApplyContext& ac) {
5352 auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
5353 auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
5354 if (!sleToken || !sleIssuance)
5355 return false;
5356 sleToken->setFieldU64(sfMPTAmount, 90);
5357 sleIssuance->setFieldU64(sfOutstandingAmount, 90);
5358 ac.view().update(sleToken);
5359 ac.view().update(sleIssuance);
5360 return true;
5361 },
5362 XRPAmount{},
5363 STTx{
5364 ttCLAWBACK,
5365 [&](STObject& tx) {
5366 tx[sfAccount] = issuer.id();
5367 tx[sfHolder] = holder.id();
5368 tx[sfAmount] = STAmount{MPTIssue{id}, 0};
5369 }},
5371 }
5372
5373 // More MPTokens created than expected
5375 std::make_pair(ttAMM_WITHDRAW, 2),
5376 std::make_pair(ttAMM_CLAWBACK, 2),
5377 std::make_pair(ttAMM_CREATE, 3),
5378 std::make_pair(ttCHECK_CASH, 2)};
5379 for (auto const& [tx, nTokens] : tests)
5380 {
5382 {{std::string("MPToken created for the MPT issuer")}},
5383 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
5384 auto const sle = ac.view().peek(keylet::account(a1.id()));
5385 if (!sle)
5386 return false;
5387
5388 auto seq = sle->getFieldU32(sfSequence);
5389 for (int i = 0; i < nTokens; ++i)
5390 {
5391 MPTIssue const mpt{makeMptID(seq + i, a1)};
5392 auto sleNew =
5394 ac.view().insert(sleNew);
5395
5396 sleNew = std::make_shared<SLE>(keylet::mptoken(mpt.getMptID(), a2));
5397 ac.view().insert(sleNew);
5398 }
5399
5400 return true;
5401 },
5402 XRPAmount{},
5403 STTx{tx, [](STObject& tx) {}},
5405 }
5406
5407 // More MPTokens deleted than expected
5408 for (auto const& tx : {ttAMM_WITHDRAW, ttAMM_CLAWBACK})
5409 {
5410 MPTID id;
5411 Account const a3("A3");
5413 {{"MPT authorize succeeded but created/deleted bad number of mptokens"}},
5414 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
5415 for (auto const& a : {a1, a2, a3})
5416 {
5417 auto sle = ac.view().peek(keylet::mptoken(id, a));
5418 if (!sle)
5419 return false;
5420 ac.view().erase(sle);
5421 }
5422 return true;
5423 },
5424 XRPAmount{},
5425 STTx{tx, [](STObject& tx) {}},
5427 [&](Account const& a1, Account const& a2, Env& env) {
5428 Account const gw("gw");
5429 env.fund(XRP(1'000), gw, a3);
5430 MPTTester const mpt({.env = env, .issuer = gw, .holders = {a1, a2, a3}});
5431 id = mpt.issuanceID();
5432 return true;
5433 });
5434 }
5435
5436 // sfReferenceHolding can only be set on creation by VaultCreate. A
5437 // non-VaultCreate transaction that creates an MPTokenIssuance with
5438 // sfReferenceHolding present must trip the invariant.
5440 {{"sfReferenceHolding set on a new MPTokenIssuance by a "
5441 "non-VaultCreate transaction"}},
5442 [](Account const& a1, Account const&, ApplyContext& ac) {
5443 auto const sleAcct = ac.view().peek(keylet::account(a1.id()));
5444 if (!sleAcct)
5445 return false;
5446 MPTIssue const mpt{makeMptID(sleAcct->getFieldU32(sfSequence), a1)};
5448 sleNew->setFieldH256(sfReferenceHolding, uint256{1});
5449 ac.view().insert(sleNew);
5450 return true;
5451 },
5452 XRPAmount{},
5453 STTx{ttACCOUNT_SET, [](STObject&) {}});
5454
5455 // sfReferenceHolding is immutable: changing the field on an
5456 // existing MPTokenIssuance must trip the invariant. Set up a real
5457 // vault via preclose (so the share issuance carries
5458 // sfReferenceHolding), then mutate it in precheck to produce a
5459 // before/after pair.
5460 {
5461 uint256 vaultKey;
5463 {{"sfReferenceHolding was modified on an existing "
5464 "MPTokenIssuance"}},
5465 [&](Account const&, Account const&, ApplyContext& ac) {
5466 auto const sleVault = ac.view().peek(keylet::vault(vaultKey));
5467 if (!sleVault)
5468 return false;
5469 auto sleIssuance =
5470 ac.view().peek(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
5471 if (!sleIssuance)
5472 return false;
5473 sleIssuance->setFieldH256(sfReferenceHolding, uint256{2});
5474 ac.view().update(sleIssuance);
5475 return true;
5476 },
5477 XRPAmount{},
5478 STTx{ttACCOUNT_SET, [](STObject&) {}},
5480 [&](Account const& a1, Account const&, Env& env) {
5481 Account const issuer{"issuer"};
5482 env.fund(XRP(10'000), issuer);
5483 env.close();
5484 MPTTester mptt{env, issuer, kMptInitNoFund};
5485 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
5486 PrettyAsset const asset = mptt.issuanceID();
5487 mptt.authorize({.account = a1});
5488 env.close();
5489
5490 Vault const vault{env};
5491 auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
5492 env(tx);
5493 env.close();
5494 vaultKey = keylet.key;
5495 return true;
5496 });
5497 }
5498
5499 // A vault pseudo-account's MPToken cannot be deleted by anything
5500 // other than a VaultDelete transaction. Set up a vault, then have
5501 // an arbitrary tx erase the pseudo's MPToken in precheck.
5502 {
5503 uint256 vaultKey;
5505 {{"vault pseudo-account holding deleted by a "
5506 "non-VaultDelete transaction"}},
5507 [&](Account const&, Account const&, ApplyContext& ac) {
5508 auto const sleVault = ac.view().peek(keylet::vault(vaultKey));
5509 if (!sleVault)
5510 return false;
5511 auto const sleIssuance =
5512 ac.view().peek(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
5513 if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
5514 return false;
5515 auto sleHolding = ac.view().peek(
5516 keylet::unchecked(sleIssuance->getFieldH256(sfReferenceHolding)));
5517 if (!sleHolding)
5518 return false;
5519 ac.view().erase(sleHolding);
5520 return true;
5521 },
5522 XRPAmount{},
5523 STTx{ttACCOUNT_SET, [](STObject&) {}},
5525 [&](Account const& a1, Account const&, Env& env) {
5526 Account const issuer{"issuer"};
5527 env.fund(XRP(10'000), issuer);
5528 env.close();
5529 MPTTester mptt{env, issuer, kMptInitNoFund};
5530 mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
5531 PrettyAsset const asset = mptt.issuanceID();
5532 mptt.authorize({.account = a1});
5533 env.close();
5534
5535 Vault const vault{env};
5536 auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
5537 env(tx);
5538 env.close();
5539 vaultKey = keylet.key;
5540 return true;
5541 });
5542 }
5543
5544 // Invalid transfer
5545 std::array<std::pair<TxType, bool>, 3> const invalidTransferTests = {
5546 std::make_pair(ttAMM_WITHDRAW, false),
5547 std::make_pair(ttPAYMENT, false),
5548 std::make_pair(ttPAYMENT, true)};
5549 for (auto const enabled : {true, false})
5550 {
5551 for (auto const& [tx, crossCurrencyPayment] : invalidTransferTests)
5552 {
5553 for (auto const flag :
5554 {static_cast<std::uint32_t>(lsfMPTLocked),
5555 ~lsfMPTCanTransfer,
5556 ~lsfMPTCanTrade,
5557 0u})
5558 {
5559 MPTID id{};
5560 auto const isSuccess = !enabled || flag == 0 ||
5561 (tx == ttPAYMENT && !crossCurrencyPayment && (flag == ~lsfMPTCanTrade)) ||
5562 (tx == ttAMM_WITHDRAW &&
5563 (flag == ~lsfMPTCanTrade || flag == ~lsfMPTCanTransfer));
5564 std::pair<TER, TER> const error = isSuccess
5568 {{isSuccess ? "" : "invalid MPToken transfer between holders"}},
5569 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
5570 auto update = [&](AccountID const& a, std::uint64_t v) {
5571 auto sle = ac.view().peek(keylet::mptoken(id, a));
5572 if (!sle)
5573 return false;
5574 sle->at(sfMPTAmount) = v;
5575 ac.view().update(sle);
5576 return true;
5577 };
5578 auto issuanceSle = ac.view().peek(keylet::mptokenIssuance(id));
5579 if (!issuanceSle)
5580 return false;
5581 auto const flags = issuanceSle->at(sfFlags);
5582 if (flag == lsfMPTLocked)
5583 {
5584 issuanceSle->at(sfFlags) = flags | lsfMPTLocked;
5585 }
5586 else if (flag != 0u)
5587 {
5588 issuanceSle->at(sfFlags) = flags & flag;
5589 }
5590 issuanceSle->at(sfOutstandingAmount) = 200;
5591 ac.view().update(issuanceSle);
5592 return update(a1, 101) && update(a2, 99);
5593 },
5594 XRPAmount{},
5595 STTx{
5596 tx,
5597 [&](STObject& tx) {
5598 if (crossCurrencyPayment)
5599 {
5600 tx.setFieldAmount(
5601 sfSendMax, STAmount(MPTAmount{100}, MPTIssue{id}));
5602 }
5603 }},
5604 {error.first, error.second},
5605 [&](Account const& a1, Account const& a2, Env& env) {
5606 Account const gw("gw");
5607 env.fund(XRP(1'000), gw);
5608 MPTTester const usd(
5609 {.env = env, .issuer = gw, .holders = {a1, a2}, .pay = 100});
5610 id = usd.issuanceID();
5611 if (!enabled)
5612 {
5613 env.disableFeature(featureMPTokensV2);
5614 }
5615 return true;
5616 });
5617 }
5618 }
5619 }
5620
5621 // Vault-share freeze invariant: isVaultPseudoAccountFrozen descends
5622 // through sfReferenceHolding to test the vault's underlying asset for
5623 // each changed holder.
5624 {
5625 Account const gw{"gw"};
5626 MPTID shareID{};
5627
5628 // Vault setup: a1 and a2 both deposit IOU and hold vault shares.
5629 auto const setupVault = [&](Account const& a1,
5630 Account const& a2,
5632 env.fund(XRP(1'000), gw);
5633 env.trust(gw["IOU"](10'000), a1);
5634 env.trust(gw["IOU"](10'000), a2);
5635 env.close();
5636 env(pay(gw, a1, gw["IOU"](500)));
5637 env(pay(gw, a2, gw["IOU"](500)));
5638 env.close();
5639
5640 Vault const vault{env};
5641 auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]});
5642 env(createTx);
5643 env.close();
5644 env(vault.deposit(
5645 {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](100)}));
5646 env(vault.deposit(
5647 {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](100)}));
5648 env.close();
5649
5650 return {env.le(vaultKeylet)->at(sfShareMPTID), env.le(vaultKeylet)->at(sfAccount)};
5651 };
5652
5653 // Simulate a vault-share transfer: a1 sends 10 shares to a2.
5654 auto const precheck =
5655 [&](Account const& a1, Account const& a2, ApplyContext& ac) -> bool {
5656 auto sle1 = ac.view().peek(keylet::mptoken(shareID, a1.id()));
5657 auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id()));
5658 if (!sle1 || !sle2)
5659 return false;
5660 (*sle1)[sfMPTAmount] -= 10;
5661 (*sle2)[sfMPTAmount] += 10;
5662 ac.view().update(sle1);
5663 ac.view().update(sle2);
5664 return true;
5665 };
5666
5667 // Case: vault pseudo-account's IOU trustline is frozen.
5668 {
5669 auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
5670 auto [sid, vid] = setupVault(a1, a2, env);
5671 shareID = sid;
5672 env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vid}, tfSetFreeze));
5673 env.close();
5674 return true;
5675 };
5676
5678 Env{*this, defaultAmendments()},
5679 {{"invalid MPToken transfer between holders"}},
5680 precheck,
5681 XRPAmount{},
5682 STTx{ttPAYMENT, [](STObject&) {}},
5684 preclose);
5685 }
5686
5687 // Case: receiver's (a2's) IOU trustline is frozen.
5688 {
5689 auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
5690 auto [sid, vid] = setupVault(a1, a2, env);
5691 shareID = sid;
5692 env(trust(gw, gw["IOU"](0), a2, tfSetFreeze));
5693 env.close();
5694 return true;
5695 };
5696
5698 Env{*this, defaultAmendments()},
5699 {{"invalid MPToken transfer between holders"}},
5700 precheck,
5701 XRPAmount{},
5702 STTx{ttPAYMENT, [](STObject&) {}},
5704 preclose);
5705 }
5706 }
5707 }
5708
5709 void
5711 {
5712 testcase << "AMM";
5713 using namespace jtx;
5714
5715 MPTID mptID{};
5716 uint256 ammID{};
5717 AccountID ammAccountID{};
5718 Account const gw{"gw"};
5719 Issue lptIssue{};
5720 PrettyAsset poolAsset{xrpIssue()};
5721
5722 auto deleteAMMAccount = [&](ApplyContext& ac, bool) {
5723 auto sle = ac.view().peek(keylet::account(ammAccountID));
5724 if (!sle)
5725 return false;
5726 ac.view().erase(sle);
5727 return true;
5728 };
5729
5730 auto updateLPTokensBalance = [&](ApplyContext& ac, std::int64_t amount) {
5731 auto sle = ac.view().peek(keylet::amm(ammID));
5732 if (!sle)
5733 return false;
5734 sle->setFieldAmount(sfLPTokenBalance, STAmount{lptIssue, amount});
5735 ac.view().update(sle);
5736 return true;
5737 };
5738 auto updateLPTokensBadAmount = [&](ApplyContext& ac, bool) {
5739 return updateLPTokensBalance(ac, -1);
5740 };
5741 auto updateLPTokensBadBalance = [&](ApplyContext& ac, bool) {
5742 return updateLPTokensBalance(ac, 200'000'000);
5743 };
5744 auto updateAMM = [&](ApplyContext& ac, bool) { return updateLPTokensBalance(ac, 10); };
5745
5746 auto updateAMMPool = [&](ApplyContext& ac, bool isMPT) {
5747 if (isMPT)
5748 {
5749 auto sle = ac.view().peek(keylet::mptoken(mptID, ammAccountID));
5750 if (!sle)
5751 return false;
5752 sle->setFieldU64(sfMPTAmount, 1);
5753 ac.view().update(sle);
5754 return true;
5755 }
5756 auto sle = ac.view().peek(keylet::account(ammAccountID));
5757 if (!sle)
5758 return false;
5759 sle->setFieldAmount(sfBalance, XRP(1));
5760 ac.view().update(sle);
5761 return true;
5762 };
5763
5764 auto test = [&](auto const txType,
5765 auto&& update,
5766 bool isMPT,
5767 TER error = tecINVARIANT_FAILED) {
5769 {{"AMM"}},
5770 [&](Account const&, Account const&, ApplyContext& ac) { return update(ac, isMPT); },
5771 XRPAmount{},
5772 STTx{txType, [&](STObject& tx) {}},
5773 {tecINVARIANT_FAILED, error},
5774 [&](Account const&, Account const&, Env& env) {
5775 env.fund(XRP(1'000), gw);
5776 poolAsset = [&]() -> PrettyAsset {
5777 if (isMPT)
5778 {
5779 MPT const mpt = MPTTester({.env = env, .issuer = gw});
5780 mptID = mpt.issuanceID;
5781 return mpt;
5782 }
5783 return gw["USD"];
5784 }();
5785 AMM const amm(env, gw, XRP(100), poolAsset(100));
5786 ammAccountID = amm.ammAccount();
5787 ammID = amm.ammID();
5788 lptIssue = amm.lptIssue();
5789 return true;
5790 });
5791 };
5792
5793 for (bool const isMPT : {false, true})
5794 {
5795 auto const error = isMPT ? TER(tecINVARIANT_FAILED) : TER(tefINVARIANT_FAILED);
5796 for (auto txType : {ttAMM_CREATE, ttAMM_DEPOSIT, ttAMM_CLAWBACK, ttAMM_WITHDRAW})
5797 {
5798 test(txType, deleteAMMAccount, isMPT, tefINVARIANT_FAILED);
5799 test(txType, updateLPTokensBadAmount, isMPT);
5800 test(txType, updateLPTokensBadBalance, isMPT);
5801 }
5802 for (auto txType : {ttAMM_BID, ttAMM_VOTE})
5803 {
5804 test(txType, updateAMMPool, isMPT, error);
5805 test(txType, updateLPTokensBadAmount, isMPT);
5806 test(txType, updateLPTokensBadBalance, isMPT);
5807 }
5808 for (auto txType : {ttAMM_DELETE, ttCHECK_CASH, ttOFFER_CREATE, ttPAYMENT})
5809 {
5810 test(txType, updateAMM, isMPT);
5811 }
5812 }
5813 }
5814
5815 // Test the invariant overwrite fix for both pre- and post-amendment
5816 // behavior. With the fix enabled, |= accumulates violations across
5817 // entries so a later valid entry cannot clear an earlier violation.
5818 // Without the fix, = assignment means the last-visited entry wins.
5819 void
5821 {
5822 using namespace test::jtx;
5823 bool const fixEnabled = features[fixCleanup3_1_3];
5826
5827 // Insert two trust line SLEs in hash-sorted order, with the "bad"
5828 // entry at the lower-sorting key so it is visited first by
5829 // ApplyStateTable::visit(). The configurer callables receive the
5830 // SLE and the Issue corresponding to that side's keylet currency.
5831 auto const insertOrderedTrustLinePair = [](ApplyContext& ac,
5832 Account const& a1,
5833 Account const& a2,
5834 Account const& a3,
5835 auto const& badConfig,
5836 auto const& goodConfig) {
5837 char const* const c1 = "USD";
5838 char const* const c2 = "EUR";
5839 auto const k1 = keylet::trustLine(a1, a2, a1[c1].currency);
5840 auto const k2 = keylet::trustLine(a1, a3, a1[c2].currency);
5841
5842 bool const k1First = k1.key < k2.key;
5843 auto const& badKey = k1First ? k1 : k2;
5844 auto const& goodKey = k1First ? k2 : k1;
5845 Issue const badIss{k1First ? a1[c1].currency : a1[c2].currency, a1.id()};
5846 Issue const goodIss{k1First ? a1[c2].currency : a1[c1].currency, a1.id()};
5847
5848 auto const sleBad = std::make_shared<SLE>(badKey);
5849 badConfig(*sleBad, badIss);
5850 ac.view().insert(sleBad);
5851
5852 auto const sleGood = std::make_shared<SLE>(goodKey);
5853 goodConfig(*sleGood, goodIss);
5854 ac.view().insert(sleGood);
5855 };
5856
5857 // Regression: bad XRP trust line followed by a valid trust line.
5858 // With the fix, the invariant catches the violation. Without it,
5859 // the valid entry overwrites the flag to false. The keylet
5860 // currencies are non-XRP (the invariant inspects sfLowLimit /
5861 // sfHighLimit issue, not the keylet currency).
5862 testcase << "overwrite: NoXRPTrustLines" + std::string(fixEnabled ? " fix" : "");
5864 makeEnv(features),
5865 fixEnabled ? std::vector<std::string>{{"an XRP trust line was created"}}
5867 [&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
5868 Account const a3{"A3"};
5869 insertOrderedTrustLinePair(
5870 ac,
5871 a1,
5872 a2,
5873 a3,
5874 [](SLE& sle, Issue const& iss) {
5875 // sfLowLimit has xrpIssue, making isXrp = true
5876 sle.setFieldAmount(sfLowLimit, STAmount{xrpIssue(), 0});
5877 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
5878 },
5879 [](SLE& sle, Issue const& iss) {
5880 sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
5881 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
5882 });
5883 return true;
5884 },
5885 XRPAmount{},
5886 STTx{ttACCOUNT_SET, [](STObject&) {}},
5887 fixEnabled ? failTers : passTers);
5888
5889 // Regression: bad deep-freeze trust line followed by a valid one.
5890 testcase << "overwrite: NoDeepFreeze" + std::string(fixEnabled ? " fix" : "");
5892 makeEnv(features),
5893 fixEnabled ? std::vector<std::string>{{"a trust line with deep freeze flag without "
5894 "normal freeze was created"}}
5896 [&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
5897 Account const a3{"A3"};
5898 insertOrderedTrustLinePair(
5899 ac,
5900 a1,
5901 a2,
5902 a3,
5903 [](SLE& sle, Issue const& iss) {
5904 sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
5905 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
5906 sle.setFieldU32(sfFlags, lsfLowDeepFreeze);
5907 },
5908 [](SLE& sle, Issue const& iss) {
5909 sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
5910 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
5911 sle.setFieldU32(sfFlags, 0u);
5912 });
5913 return true;
5914 },
5915 XRPAmount{},
5916 STTx{ttACCOUNT_SET, [](STObject&) {}},
5917 fixEnabled ? failTers : passTers);
5918
5919 // Regression: MPT OutstandingAmount exceeds max, but locked <=
5920 // outstanding. Plain assignment would overwrite bad_ = true.
5921 // With the fix, NoZeroEscrow catches it.
5922 // Without the fix, NoZeroEscrow passes but ValidMPTIssuance
5923 // still fires ("a MPT issuance was created").
5924 testcase << "overwrite: NoZeroEscrow MPT" + std::string(fixEnabled ? " fix" : "");
5926 makeEnv(features),
5927 fixEnabled ? std::vector<std::string>{{"escrow specifies invalid amount"}}
5928 : std::vector<std::string>{{"a MPT issuance was created"}},
5929 [](Account const& a1, Account const&, ApplyContext& ac) {
5930 auto const sle = ac.view().peek(keylet::account(a1.id()));
5931 if (!sle)
5932 return false;
5933
5934 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
5936 // outstanding exceeds kMaxMpTokenAmount -> checkAmount sets bad_
5937 sleNew->setFieldU64(sfOutstandingAmount, kMaxMpTokenAmount + 1);
5938 // locked is valid and <= outstanding -> must NOT clear bad_
5939 sleNew->setFieldU64(sfLockedAmount, 10);
5940 ac.view().insert(sleNew);
5941 return true;
5942 },
5943 XRPAmount{},
5944 STTx{ttACCOUNT_SET, [](STObject&) {}},
5945 failTers);
5946 }
5947
5948 void
5950 {
5951 using namespace jtx;
5952
5953 Account const issuer{"issuer"};
5954 PrettyAsset const vaultAsset = issuer["IOU"];
5955
5956 struct TestCase
5957 {
5958 std::string name;
5959 std::int32_t expectedMinScale;
5961 };
5962
5963 for (auto const mantissaScale : MantissaRange::getAllScales())
5964 {
5965 if (mantissaScale == MantissaRange::MantissaScale::Small)
5966 continue;
5967 NumberMantissaScaleGuard const g{mantissaScale};
5968
5969 auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
5970 return {.delta = n, .scale = scale(n, vaultAsset.raw())};
5971 };
5972
5973 auto const testCases = std::vector<TestCase>{
5974 {
5975 .name = "No values",
5976 .expectedMinScale = 0,
5977 .values = {},
5978 },
5979 {
5980 .name = "Mixed integer and Number values",
5981 .expectedMinScale = -15,
5982 .values = {makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})},
5983 },
5984 {
5985 .name = "Mixed scales",
5986 .expectedMinScale = -17,
5987 .values =
5988 {makeDelta(Number{1, -2}),
5989 makeDelta(Number{5, -3}),
5990 makeDelta(Number{3, -2})},
5991 },
5992 {
5993 .name = "Equal scales",
5994 .expectedMinScale = -16,
5995 .values =
5996 {makeDelta(Number{1, -1}),
5997 makeDelta(Number{5, -1}),
5998 makeDelta(Number{1, -1})},
5999 },
6000 {
6001 .name = "Mixed mantissa sizes",
6002 .expectedMinScale = -12,
6003 .values =
6004 {makeDelta(Number{1}),
6005 makeDelta(Number{1234, -3}),
6006 makeDelta(Number{12345, -6}),
6007 makeDelta(Number{123, 1})},
6008 },
6009 };
6010
6011 for (auto const& tc : testCases)
6012 {
6013 testcase("vault computeCoarsestScale: " + tc.name);
6014
6015 auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
6016
6017 BEAST_EXPECTS(
6018 actualScale == tc.expectedMinScale,
6019 "expected: " + std::to_string(tc.expectedMinScale) +
6020 ", actual: " + std::to_string(actualScale));
6021 for (auto const& num : tc.values)
6022 {
6023 // None of these scales are far enough apart that rounding the
6024 // values would lose information, so check that the rounded
6025 // value matches the original.
6026 auto const actualRounded = roundToAsset(vaultAsset, num.delta, actualScale);
6027 BEAST_EXPECTS(
6028 actualRounded == num.delta,
6029 "number " + to_string(num.delta) + " rounded to scale " +
6030 std::to_string(actualScale) + " is " + to_string(actualRounded));
6031 }
6032 }
6033
6034 auto const testCases2 = std::vector<TestCase>{
6035 {
6036 .name = "False equivalence",
6037 .expectedMinScale = -15,
6038 .values =
6039 {
6040 makeDelta(Number{1234567890123456789, -18}),
6041 makeDelta(Number{12345, -4}),
6042 makeDelta(Number{1}),
6043 },
6044 },
6045 };
6046
6047 // Unlike the first set of test cases, the values in these test could
6048 // look equivalent if using the wrong scale.
6049 for (auto const& tc : testCases2)
6050 {
6051 testcase("vault computeCoarsestScale: " + tc.name);
6052
6053 auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
6054
6055 BEAST_EXPECTS(
6056 actualScale == tc.expectedMinScale,
6057 "expected: " + std::to_string(tc.expectedMinScale) +
6058 ", actual: " + std::to_string(actualScale));
6060 Number firstRounded;
6061 for (auto const& num : tc.values)
6062 {
6063 if (!first)
6064 {
6065 first = num.delta;
6066 firstRounded = roundToAsset(vaultAsset, num.delta, actualScale);
6067 continue;
6068 }
6069 auto const numRounded = roundToAsset(vaultAsset, num.delta, actualScale);
6070 BEAST_EXPECTS(
6071 numRounded != firstRounded,
6072 "at a scale of " + std::to_string(actualScale) + " " +
6073 to_string(num.delta) + " == " + to_string(*first));
6074 }
6075 }
6076 }
6077 }
6078
6079 void
6081 {
6082 using namespace test::jtx;
6083 using namespace std::string_literals;
6084 testcase("Sponsorship");
6085 {
6086 auto const expectMessage =
6087 "SponsoredOwnerCount does not equal SponsoringOwnerCount delta.";
6088
6090 {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
6091 auto const sle = ac.view().peek(keylet::account(a1.id()));
6092 if (!sle)
6093 return false;
6094 sle->setFieldU32(sfSponsoredOwnerCount, 1);
6095 ac.view().update(sle);
6096 return true;
6097 });
6098
6100 {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
6101 auto const sle = ac.view().peek(keylet::account(a1.id()));
6102 if (!sle)
6103 return false;
6104 sle->setFieldU32(sfSponsoringOwnerCount, 1);
6105 ac.view().update(sle);
6106 return true;
6107 });
6108 }
6109
6110 {
6111 auto const expectMessage =
6112 "OwnerCount must be greater than or equal to SponsoredOwnerCount.";
6113
6115 {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
6116 auto const sle = ac.view().peek(keylet::account(a1.id()));
6117 if (!sle)
6118 return false;
6119 sle->setFieldU32(sfOwnerCount, 0);
6120 sle->setFieldU32(sfSponsoredOwnerCount, 1);
6121 ac.view().update(sle);
6122
6123 auto const sle2 = ac.view().peek(keylet::account(a2.id()));
6124 if (!sle2)
6125 return false;
6126 sle2->setFieldU32(sfSponsoringOwnerCount, 1);
6127 ac.view().update(sle2);
6128 return true;
6129 });
6130 }
6131
6132 {
6133 auto const expectMessage =
6134 "SponsoredObjectOwnerCount does not equal SponsoredOwnerCount delta.";
6135 uint256 checkID;
6136
6138 {{expectMessage}},
6139 [&](Account const&, Account const& a2, ApplyContext& ac) {
6140 auto const check = ac.view().peek(keylet::check(checkID));
6141 if (!check)
6142 return false;
6143 check->setAccountID(sfSponsor, a2.id());
6144 ac.view().update(check);
6145 return true;
6146 },
6147 XRPAmount{},
6148 STTx{ttACCOUNT_SET, [](STObject&) {}},
6150 [&checkID](Account const& a1, Account const& a2, Env& env) {
6151 checkID = keylet::check(a1.id(), SeqProxy::rawSequence(env.seq(a1))).key;
6152 env(check::create(a1, a2, XRP(1)));
6153 return true;
6154 });
6155 }
6156
6157 {
6158 auto const expectMessage =
6159 "Invariant failed: Net delta of SponsoringAccountCount does "
6160 "not match net delta of sfSponsor presence.";
6161
6163 {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
6164 auto const sle = ac.view().peek(keylet::account(a1.id()));
6165 if (!sle)
6166 return false;
6167 sle->setFieldU32(sfSponsoringAccountCount, 1);
6168 ac.view().update(sle);
6169 return true;
6170 });
6171
6173 {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
6174 auto const sle = ac.view().peek(keylet::account(a1.id()));
6175 if (!sle)
6176 return false;
6177 sle->setAccountID(sfSponsor, a2.id());
6178 ac.view().update(sle);
6179 return true;
6180 });
6181 }
6182 }
6183
6184 void
6186 {
6187 testcase << "object has pseudo-account";
6188 using namespace jtx;
6189
6190 auto const amendments = defaultAmendments() | fixCleanup3_3_0;
6191
6192 // Vault: object deleted without its pseudo-account
6193 {
6194 Keylet vaultKeylet = keylet::amendments();
6196 Env{*this, amendments},
6197 {{"deleted Vault without deleting its pseudo-account"}},
6198 [&vaultKeylet](Account const&, Account const&, ApplyContext& ac) {
6199 auto sle = ac.view().peek(vaultKeylet);
6200 if (!sle)
6201 return false;
6202 ac.view().erase(sle);
6203 return true;
6204 },
6205 XRPAmount{},
6206 STTx{ttVAULT_DELETE, [](STObject&) {}},
6208 [&vaultKeylet](Account const& a1, Account const&, Env& env) {
6209 Vault const vault{env};
6210 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
6211 env(tx);
6212 vaultKeylet = keylet;
6213 return true;
6214 });
6215 }
6216
6217 // AMM: object deleted without its pseudo-account
6218 {
6219 uint256 ammID{};
6220 Account const gw{"gw"};
6222 Env{*this, amendments},
6223 {{"deleted AMM without deleting its pseudo-account"}},
6224 [&ammID](Account const&, Account const&, ApplyContext& ac) {
6225 auto sle = ac.view().peek(keylet::amm(ammID));
6226 if (!sle)
6227 return false;
6228 ac.view().erase(sle);
6229 return true;
6230 },
6231 XRPAmount{},
6232 STTx{ttAMM_DELETE, [](STObject&) {}},
6234 [&ammID, &gw](Account const&, Account const&, Env& env) {
6235 env.fund(XRP(1'000), gw);
6236 AMM const amm(env, gw, XRP(100), gw["USD"](100));
6237 ammID = amm.ammID();
6238 return true;
6239 });
6240 }
6241
6242 // LoanBroker: object deleted without its pseudo-account
6243 {
6244 Keylet loanBrokerKeylet = keylet::amendments();
6246 Env{*this, amendments},
6247 {{"deleted LoanBroker without deleting its pseudo-account"}},
6248 [&loanBrokerKeylet](Account const&, Account const&, ApplyContext& ac) {
6249 auto sle = ac.view().peek(loanBrokerKeylet);
6250 if (!sle)
6251 return false;
6252 ac.view().erase(sle);
6253 return true;
6254 },
6255 XRPAmount{},
6256 STTx{ttLOAN_BROKER_DELETE, [](STObject&) {}},
6258 [&loanBrokerKeylet, this](Account const& a1, Account const&, Env& env) {
6259 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
6260 loanBrokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
6261 return BEAST_EXPECT(env.le(loanBrokerKeylet));
6262 });
6263 }
6264
6265 // Deleted object missing sfAccount field (defensive check).
6266 // Manually construct the view to place a vault SLE without
6267 // sfAccount into the base ledger, then erase it.
6268 {
6269 Env env{*this, amendments};
6270 Account const a1{"A1"};
6271 Account const a2{"A2"};
6272 env.fund(XRP(1000), a1, a2);
6273 env.close();
6274
6275 OpenView ov{*env.current()};
6276
6277 auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ov.seq()));
6278 auto sleVault = std::make_shared<SLE>(vaultKeylet);
6279 sleVault->makeFieldAbsent(sfAccount);
6280 ov.rawInsert(sleVault);
6281
6282 STTx const tx{ttVAULT_DELETE, [](STObject&) {}};
6284 beast::Journal const jlog{sink};
6285 ApplyContext ac{
6286 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
6287 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
6288
6289 auto sle = ac.view().peek(vaultKeylet);
6290 if (!BEAST_EXPECT(sle))
6291 return;
6292 ac.view().erase(sle);
6293
6294 auto transactor = makeTransactor(ac);
6295 if (!BEAST_EXPECT(transactor))
6296 return;
6297 TER const result = transactor->checkInvariants(tesSUCCESS, XRPAmount{});
6298 BEAST_EXPECT(result == tecINVARIANT_FAILED);
6299 BEAST_EXPECT(sink.messages().str().contains("is missing pseudo-account field"));
6300 }
6301 }
6302
6303 void
6305 {
6306 using namespace test::jtx;
6307 testcase << "ValidConfidentialMPToken";
6308
6309 MPTID mptID;
6310
6311 // Generate an MPT with privacy, issue 100 tokens to A2.
6312 // Perform a confidential conversion to populate encrypted state.
6313 auto const precloseConfidential =
6314 [&mptID](Account const& a1, Account const& a2, Env& env) -> bool {
6315 MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
6316 mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
6317 mptID = mpt.issuanceID();
6318
6319 mpt.authorize({.account = a2});
6320 mpt.pay(a1, a2, 100);
6321
6322 mpt.generateKeyPair(a1);
6323 mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
6324
6325 mpt.generateKeyPair(a2);
6326 mpt.convert({
6327 .account = a2,
6328 .amt = 100,
6329 .holderPubKey = mpt.getPubKey(a2),
6330 });
6331 return true;
6332 };
6333
6334 // badDelete
6336 {"MPToken deleted with encrypted fields while COA > 0"},
6337 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
6338 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
6339 if (!sleToken)
6340 return false;
6341 // Force an erase of the object while the COA remains 100
6342 ac.view().erase(sleToken);
6343 return true;
6344 },
6345 XRPAmount{},
6346 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
6348 precloseConfidential);
6349
6350 // badConsistency
6352 {"MPToken encrypted field existence inconsistency"},
6353 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
6354 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
6355 if (!sleToken)
6356 return false;
6357 // Remove one of the required encrypted fields to create a mismatch
6358 sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
6359 ac.view().update(sleToken);
6360 return true;
6361 },
6362 XRPAmount{},
6363 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
6365 precloseConfidential);
6366
6368 {"MPToken encrypted field existence inconsistency"},
6369 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
6370 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
6371 if (!sleToken)
6372 return false;
6373 sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
6374 sleToken->makeFieldAbsent(sfConfidentialBalanceInbox);
6375 sleToken->makeFieldAbsent(sfConfidentialBalanceSpending);
6376 sleToken->setFieldVL(sfAuditorEncryptedBalance, Blob{0x00});
6377 ac.view().update(sleToken);
6378 return true;
6379 },
6380 XRPAmount{},
6381 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
6383 precloseConfidential);
6384
6385 // requiresPrivacyFlag
6386 auto const precloseNoPrivacy = [&mptID](
6387 Account const& a1, Account const& a2, Env& env) -> bool {
6388 MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
6389 // completely omitted the tfMPTCanHoldConfidentialBalance flag here.
6390 mpt.create({.flags = tfMPTCanTransfer});
6391 mptID = mpt.issuanceID();
6392 mpt.authorize({.account = a2});
6393 mpt.pay(a1, a2, 100);
6394 return true;
6395 };
6396
6398 {"MPToken has encrypted fields but Issuance does not have "
6399 "lsfMPTCanHoldConfidentialBalance "
6400 "set"},
6401 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
6402 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
6403 if (!sleToken)
6404 return false;
6405 // Inject all three encrypted fields consistently (inbox+spending+issuer must be
6406 // in sync or badConsistency fires first and masks requiresPrivacyFlag).
6407 sleToken->setFieldVL(sfConfidentialBalanceInbox, Blob{0x00});
6408 sleToken->setFieldVL(sfConfidentialBalanceSpending, Blob{0x00});
6409 sleToken->setFieldVL(sfIssuerEncryptedBalance, Blob{0x00});
6410 ac.view().update(sleToken);
6411 return true;
6412 },
6413 XRPAmount{},
6414 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
6416 precloseNoPrivacy);
6417
6418 // badCOA
6420 {"Confidential outstanding amount exceeds total outstanding amount"},
6421 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
6422 auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
6423 if (!sleIssuance)
6424 return false;
6425 // Total outstanding is natively 100; bloat the COA over 100
6426 sleIssuance->setFieldU64(sfConfidentialOutstandingAmount, 200);
6427 ac.view().update(sleIssuance);
6428 return true;
6429 },
6430 XRPAmount{},
6431 STTx{ttMPTOKEN_ISSUANCE_SET, [](STObject&) {}},
6433 precloseConfidential);
6434
6435 // Conservation Violation
6437 {"Token conservation violation for MPT"},
6438 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
6439 auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
6440 if (!sleIssuance)
6441 return false;
6442
6443 sleIssuance->setFieldU64(
6444 sfConfidentialOutstandingAmount,
6445 sleIssuance->getFieldU64(sfConfidentialOutstandingAmount) - 10);
6446 ac.view().update(sleIssuance);
6447
6448 return true;
6449 },
6450 XRPAmount{},
6451 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
6453 precloseConfidential);
6454
6455 // Send/MergeInbox must not change OutstandingAmount (coaDelta == 0)
6457 {"Invariant failed: OutstandingAmount changed "
6458 "by confidential transaction that should not "
6459 "modify it for MPT"},
6460 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
6461 auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
6462 if (!sleIssuance)
6463 return false;
6464 sleIssuance->setFieldU64(
6465 sfOutstandingAmount, sleIssuance->getFieldU64(sfOutstandingAmount) + 1);
6466 ac.view().update(sleIssuance);
6467 return true;
6468 },
6469 XRPAmount{},
6470 STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
6472 precloseConfidential);
6473
6474 // Send/MergeInbox and zero-COA-delta confidential transactions must not
6475 // change public holder MPTAmount.
6477 {"Invariant failed: MPTAmount changed by confidential "
6478 "transaction that should not modify this field."},
6479 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
6480 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
6481 if (!sleToken)
6482 return false;
6483 sleToken->setFieldU64(sfMPTAmount, sleToken->getFieldU64(sfMPTAmount) + 1);
6484 ac.view().update(sleToken);
6485 return true;
6486 },
6487 XRPAmount{},
6488 STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
6490 precloseConfidential);
6491
6492 // badVersion
6494 {"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending "
6495 "changed"},
6496 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
6497 Blob const kChangedConfidentialSpending = {0xBA, 0xDD};
6498 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
6499 if (!sleToken)
6500 return false;
6501 sleToken->setFieldVL(sfConfidentialBalanceSpending, kChangedConfidentialSpending);
6502
6503 // DO NOT update sfConfidentialBalanceVersion
6504 ac.view().update(sleToken);
6505 return true;
6506 },
6507 XRPAmount{},
6508 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
6510 precloseConfidential);
6511
6512 // Skipping Deleted MPTs (Issuance deleted)
6513 auto const precloseOrphan = [&mptID](
6514 Account const& a1, Account const& a2, Env& env) -> bool {
6515 MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
6516 mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
6517 mptID = mpt.issuanceID();
6518 mpt.authorize({.account = a2});
6519
6520 // Generate privacy keys and convert 0 amount so Bob has the encrypted fields
6521 mpt.generateKeyPair(a1);
6522 mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
6523 mpt.generateKeyPair(a2);
6524 mpt.convert({
6525 .account = a2,
6526 .amt = 0,
6527 .holderPubKey = mpt.getPubKey(a2),
6528 });
6529
6530 // Immediately destroy the issuance. A2's empty, encrypted token object lives on.
6531 mpt.destroy();
6532 return true;
6533 };
6534
6536 {},
6537 [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
6538 auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
6539 if (!sleToken)
6540 return false;
6541 // Safely able to erase the deleted token.
6542 ac.view().erase(sleToken);
6543 return true;
6544 },
6545 XRPAmount{},
6546 STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
6548 precloseOrphan);
6549 }
6550
6551public:
6552 void
6588};
6589
6591
6592} // namespace xrpl::test
A generic endpoint for log messages.
Definition Journal.h:44
A testsuite class.
Definition suite.h:52
bool expect(Condition const &shouldBeTrue)
Evaluate a test condition.
Definition suite.h:235
LogOs< char > log
Logging output stream.
Definition suite.h:150
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
State information when applying a tx.
ApplyView & view()
Writeable view to a ledger, for applying a transaction.
Definition ApplyView.h:134
virtual SLE::pointer peek(Keylet const &k)=0
Prepare to modify the SLE associated with key.
virtual void insert(SLE::ref sle)=0
Insert a new state SLE.
virtual void erase(SLE::ref sle)=0
Remove a peeked SLE.
std::optional< std::uint64_t > dirInsert(Keylet const &directory, uint256 const &key, std::function< void(SLE::ref)> const &describe)
Insert an entry to a directory.
Definition ApplyView.h:366
virtual void update(SLE::ref sle)=0
Indicate changes to a peeked SLE.
Specifies an order book.
Definition Book.h:28
RAII class to set and restore the current transaction rules.
Definition Rules.h:113
SOTemplate const * findSOTemplateBySField(SField const &sField) const
static InnerObjectFormats const & getInstance()
A currency issued by an account.
Definition Issue.h:18
constexpr MPTID const & getMptID() const
Definition MPTIssue.h:43
std::chrono::time_point< NetClock > time_point
Definition chrono.h:48
std::chrono::duration< rep, period > duration
Definition chrono.h:47
Sets the new scale and restores the old scale when it leaves scope.
Definition Number.h:963
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
void rawInsert(SLE::ref sle) override
Unconditionally insert a state item.
Definition OpenView.cpp:237
void rawErase(SLE::ref sle) override
Delete an existing state item.
Definition OpenView.cpp:231
Rules const & rules() const override
Returns the tx processing rules.
Definition OpenView.cpp:148
bool exists(Keylet const &k) const override
Determine if a state item exists.
Definition OpenView.cpp:154
virtual Rules const & rules() const =0
Returns the tx processing rules.
LedgerIndex seq() const
Returns the sequence number of the base ledger.
Definition ReadView.h:115
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:180
Identifies fields.
Definition SField.h:132
Defines the fields and their attributes within a STObject.
Definition SOTemplate.h:105
static std::uint64_t const kURateOne
Definition STAmount.h:78
void negate()
Definition STAmount.h:586
AccountID const & getIssuer() const
Definition STAmount.h:516
iterator begin()
Definition STArray.h:224
void pushBack(STObject const &object)
Definition STArray.h:212
std::shared_ptr< STLedgerEntry > pointer
void setFieldU32(SField const &field, std::uint32_t)
Definition STObject.cpp:743
void setFieldAmount(SField const &field, STAmount const &)
Definition STObject.cpp:803
static STObject makeInnerObject(SField const &name)
Definition STObject.cpp:79
void setAccountID(SField const &field, AccountID const &)
Definition STObject.cpp:785
void pushBack(uint256 const &v)
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
An immutable linear range of bytes.
Definition Slice.h:28
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
static std::int32_t computeCoarsestScale(std::vector< DeltaInfo > const &numbers)
void run() override
Runs the suite.
void testInvariantOverwrite(FeatureBitset features)
void doInvariantCheck(test::jtx::Env &&env, std::vector< std::string > const &expectLogs, Precheck const &precheck, XRPAmount fee=XRPAmount{}, STTx tx=STTx{ttACCOUNT_SET, [](STObject &) {}}, std::initializer_list< TER > ters={tecINVARIANT_FAILED, tefINVARIANT_FAILED}, Preclose const &preclose={}, TxAccount setTxAccount=TxAccount::None, std::source_location const &loc=std::source_location::current())
std::function< bool(test::jtx::Account const &a, test::jtx::Account const &b, test::jtx::Env &env)> Preclose
void doInvariantCheck(test::jtx::Env &&env, test::jtx::Account const &a1, test::jtx::Account const &a2, std::vector< std::string > const &expectLogs, Precheck const &precheck, XRPAmount fee=XRPAmount{}, STTx tx=STTx{ttACCOUNT_SET, [](STObject &) {}}, std::initializer_list< TER > ters={tecINVARIANT_FAILED, tefINVARIANT_FAILED}, std::source_location const &loc=std::source_location::current())
test::jtx::Env makeEnv(FeatureBitset features)
Keylet createLoanBroker(jtx::Account const &a, jtx::Env &env, jtx::PrettyAsset const &asset)
void testPermissionedDEX(FeatureBitset features)
void doInvariantCheck(std::vector< std::string > const &expectLogs, Precheck const &precheck, XRPAmount fee=XRPAmount{}, STTx tx=STTx{ttACCOUNT_SET, [](STObject &) {}}, std::initializer_list< TER > ters={tecINVARIANT_FAILED, tefINVARIANT_FAILED}, Preclose const &preclose={}, TxAccount setTxAccount=TxAccount::None, std::source_location const &loc=std::source_location::current())
static SLE::pointer createPermissionedDomain(ApplyContext &ac, test::jtx::Account const &a1, test::jtx::Account const &a2, std::uint32_t numCreds=2, std::uint32_t seq=10)
TxAccount
Run a specific test case to put the ledger into a state that will be detected by an invariant.
static std::pair< std::uint32_t, uint256 > createPermissionedDomainEnv(test::jtx::Env &env, test::jtx::Account const &a1, test::jtx::Account const &a2, std::uint32_t numCreds=2)
void testPermissionedDomainInvariants(FeatureBitset features)
std::function< bool(test::jtx::Account const &a, test::jtx::Account const &b, ApplyContext &ac)> Precheck
void testAMMDeleteInvariants(FeatureBitset features)
static FeatureBitset defaultAmendments()
std::stringstream const & messages() const
Convenience class to test AMM functionality.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
std::string const & human() const
Returns the human readable public key.
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
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
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:354
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:538
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.
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:447
void create(MPTCreate const &arg=MPTCreate{})
Definition mpt.cpp:241
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:353
MPTID const & issuanceID() const
Definition mpt.h:641
Converts to MPT Issue or STAmount.
T current(T... args)
T file_name(T... args)
T get(T... args)
T invoke(T... args)
T iter_swap(T... args)
T make_pair(T... args)
T make_shared(T... args)
constexpr Zero kZero
Definition Zero.h:30
std::uint64_t insertKey(ApplyView &view, SLE::ref node, std::uint64_t page, bool preserveOrder, STVector256 &indexes, uint256 const &key)
Definition ApplyView.cpp:70
std::optional< std::uint64_t > insertPage(ApplyView &view, std::uint64_t page, SLE::pointer node, std::uint64_t nextPage, SLE::ref next, uint256 const &key, Keylet const &directory, std::function< void(SLE::ref)> const &describe)
std::uint64_t createRoot(ApplyView &view, Keylet const &directory, uint256 const &key, std::function< void(SLE::ref)> const &describe)
Helper functions for managing low-level directory operations.
Definition ApplyView.cpp:30
Keylet computation functions.
Definition Indexes.h:40
Keylet escrow(AccountID const &src, SeqProxy const &seq) noexcept
An escrow entry.
Definition Indexes.cpp:388
Keylet quality(Keylet const &k, std::uint64_t const q) noexcept
The initial directory page for a specific quality.
Definition Indexes.cpp:282
Keylet offer(AccountID const &id, SeqProxy const &seq) noexcept
An offer from an account.
Definition Indexes.cpp:276
Keylet unchecked(uint256 const &key) noexcept
Any ledger entry.
Definition Indexes.cpp:367
Keylet loan(uint256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:573
Keylet book(Book const &b)
The beginning of an order book.
Definition Indexes.cpp:247
Keylet const & amendments() noexcept
The index of the amendment table.
Definition Indexes.cpp:226
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:373
Keylet check(AccountID const &id, SeqProxy const &seq) noexcept
A Check.
Definition Indexes.cpp:338
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:441
Keylet nftokenPageMin(AccountID const &owner)
NFT page keylets.
Definition Indexes.cpp:400
Keylet page(uint256 const &root, std::uint64_t const index=0) noexcept
A page in a directory.
Definition Indexes.cpp:379
Keylet nftokenPage(Keylet const &k, uint256 const &token)
Definition Indexes.cpp:416
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:561
Keylet permissionedDomain(AccountID const &account, SeqProxy const &seq) noexcept
Definition Indexes.cpp:579
Keylet nftokenPageMax(AccountID const &owner)
A keylet for the owner's last possible NFT page.
Definition Indexes.cpp:408
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:543
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:567
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:537
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:253
Check operations.
Definition check.h:18
json::Value create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
json::Value set(AccountID const &account, uint256 const &vaultId, uint32_t flags)
json::Value coverDeposit(AccountID const &account, uint256 const &brokerID, STAmount const &amount, uint32_t flags)
std::vector< Credential > Credentials
json::Value setTx(AccountID const &account, Credentials const &credentials, std::optional< uint256 > domain)
uint256 getNewDomain(std::shared_ptr< STObject const > const &meta)
json::Value mint(jtx::Account const &account, std::uint32_t nfTokenTaxon)
Mint an NFToken.
Definition token.cpp:23
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
FeatureBitset testableAmendments()
Definition Env.h:92
std::unique_ptr< Config > envconfig()
creates and initializes a default configuration for jtx::Env
Definition envconfig.h:37
static IncrementT const kIncrement
Definition tags.h:31
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.
static MPTInit const kMptInitNoFund
Definition mpt.h:172
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
STTx createTx(bool disabling, LedgerIndex seq, PublicKey const &txKey)
Create ttUNL_MODIFY Tx.
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
std::vector< SField const * > const & getPseudoAccountFields()
Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if set.
AccountID pseudoAccountAddress(ReadView const &view, uint256 const &pseudoOwnerKey)
Generate a pseudo-account address from a pseudo owner key.
TxType
Transaction type identifiers.
Definition TxFormats.h:45
void increaseOwnerCount(ApplyView &view, SLE::ref accountSle, SLE::ref sponsorSle, std::uint32_t count, beast::Journal j)
Increase owner-count fields when the caller supplies the sponsor.
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:794
@ tefINVARIANT_FAILED
Definition TER.h:175
constexpr std::uint32_t kMaxInvestmentPeriod
Definition Protocol.h:359
BaseUInt< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition UintTypes.h:42
TER trustDelete(ApplyView &view, SLE::ref sleRippleState, AccountID const &uLowAccountID, AccountID const &uHighAccountID, beast::Journal j)
std::string transToken(TER code)
Definition TER.cpp:251
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
STLedgerEntry SLE
TER deleteAMMAccount(Sandbox &view, Asset const &asset, Asset const &asset2, beast::Journal j)
Delete trustlines to AMM.
std::unique_ptr< Transactor > makeTransactor(ApplyContext &ctx)
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
Definition UintTypes.h:54
std::function< void(SLE::ref)> describeOwnerDir(AccountID const &account)
Returns a function that sets the owner on a directory SLE.
MPTID makeMptID(std::uint32_t const sequence, AccountID const &account)
Definition Indexes.cpp:184
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:735
@ TapNone
Definition ApplyView.h:28
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
bool isTesSuccess(TER x) noexcept
Definition TER.h:676
constexpr std::uint8_t kVaultStrategyFirstComeFirstServe
Vault withdrawal policies.
Definition Protocol.h:307
TERSubset< CanCvtToTER > TER
Definition TER.h:647
constexpr std::size_t kMaxPermissionedDomainCredentialsArraySize
The maximum number of credentials can be passed in array for permissioned domain.
Definition Protocol.h:286
LedgerEntryType
Identifiers for on-ledger objects.
@ tecINCOMPLETE
Definition TER.h:338
@ tecINVARIANT_FAILED
Definition TER.h:316
std::vector< unsigned char > Blob
Storage for linear binary data.
Definition Blob.h:11
Currency const & badCurrency()
We deliberately disallow the currency that looks like "XRP" because too many people were using it ins...
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:296
constexpr XRPAmount kInitialXrp
Configure the native currency.
std::array< KeyletDesc< AccountID const & >, 6 > const kDirectAccountKeylets
Definition Indexes.cpp:39
constexpr std::uint32_t kMinInvestmentPeriod
Bounds on the length of a closed-ended vault's Investment phase (RedemptionDate - SubscriptionDate).
Definition Protocol.h:356
BaseUInt< 256 > uint256
Definition base_uint.h:580
@ tesSUCCESS
Definition TER.h:245
T str(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
uint256 key
Definition Keylet.h:21
LedgerEntryType type
Definition Keylet.h:22
static std::set< MantissaScale > const & getAllScales()
Definition Number.h:178
T to_string(T... args)