xrpld
Loading...
Searching...
No Matches
InvariantsMisc_test.cpp
1#include <test/app/invariants/InvariantsBase.h>
2#include <test/jtx/AMM.h>
3#include <test/jtx/Account.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/pay.h>
8#include <test/jtx/token.h>
9#include <test/jtx/trust.h>
10#include <test/jtx/vault.h>
11#include <test/unit_test/SuiteJournal.h>
12
13#include <xrpl/basics/Number.h>
14#include <xrpl/basics/base_uint.h>
15#include <xrpl/basics/chrono.h>
16#include <xrpl/beast/unit_test/suite.h>
17#include <xrpl/beast/utility/Journal.h>
18#include <xrpl/beast/utility/Zero.h>
19#include <xrpl/ledger/ApplyView.h>
20#include <xrpl/ledger/OpenView.h>
21#include <xrpl/ledger/ReadView.h>
22#include <xrpl/ledger/helpers/AccountRootHelpers.h>
23#include <xrpl/ledger/helpers/RippleStateHelpers.h>
24#include <xrpl/protocol/AccountID.h>
25#include <xrpl/protocol/Feature.h>
26#include <xrpl/protocol/Indexes.h>
27#include <xrpl/protocol/Issue.h>
28#include <xrpl/protocol/Keylet.h>
29#include <xrpl/protocol/LedgerFormats.h>
30#include <xrpl/protocol/MPTIssue.h>
31#include <xrpl/protocol/Protocol.h>
32#include <xrpl/protocol/Rules.h>
33#include <xrpl/protocol/SField.h>
34#include <xrpl/protocol/STAmount.h>
35#include <xrpl/protocol/STLedgerEntry.h>
36#include <xrpl/protocol/STObject.h>
37#include <xrpl/protocol/STTx.h>
38#include <xrpl/protocol/SeqProxy.h>
39#include <xrpl/protocol/SystemParameters.h>
40#include <xrpl/protocol/TER.h>
41#include <xrpl/protocol/TxFormats.h>
42#include <xrpl/protocol/XRPAmount.h>
43#include <xrpl/tx/ApplyContext.h>
44#include <xrpl/tx/Transactor.h>
45#include <xrpl/tx/applySteps.h>
46#include <xrpl/tx/invariants/InvariantRunner.h>
47
48#include <array>
49#include <cstdint>
50#include <functional>
51#include <initializer_list>
52#include <memory>
53#include <optional>
54#include <stdexcept>
55#include <string>
56#include <utility>
57#include <vector>
58
59namespace xrpl::test {
60
62{
64
65 void
67 {
68 using namespace test::jtx;
69 testcase << "XRP created";
71 {{"XRP net change was positive: 500"}},
72 [](Account const& a1, Account const&, ApplyContext& ac) {
73 // put a single account in the view and "manufacture" some XRP
74 auto const sle = ac.view().peek(keylet::account(a1.id()));
75 if (!sle)
76 return false;
77 auto amt = sle->getFieldAmount(sfBalance);
78 sle->setFieldAmount(sfBalance, amt + STAmount{500});
79 ac.view().update(sle);
80 return true;
81 });
82 }
83
84 void
86 {
87 using namespace test::jtx;
88 testcase << "account root removed";
89
90 // An account was deleted, but not by an AccountDelete transaction.
92 {{"an account root was deleted"}},
93 [](Account const& a1, Account const&, ApplyContext& ac) {
94 // remove an account from the view
95 auto sle = ac.view().peek(keylet::account(a1.id()));
96 if (!sle)
97 return false;
98 // Clear the balance so the "account deletion left behind a
99 // non-zero balance" check doesn't trip earlier than the desired
100 // check.
101 sle->at(sfBalance) = beast::kZero;
102 ac.view().erase(sle);
103 return true;
104 });
105
106 // Successful AccountDelete transaction that didn't delete an account.
107 //
108 // Note that this is a case where a second invocation of the invariant
109 // checker returns a tecINVARIANT_FAILED, not a tefINVARIANT_FAILED.
110 // After a discussion with the team, we believe that's okay.
112 {{"account deletion succeeded without deleting an account"}},
113 [](Account const&, Account const&, ApplyContext& ac) { return true; },
114 XRPAmount{},
115 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}},
117
118 // Successful AccountDelete that deleted more than one account.
120 {{"account deletion succeeded but deleted multiple accounts"}},
121 [](Account const& a1, Account const& a2, ApplyContext& ac) {
122 // remove two accounts from the view
123 auto sleA1 = ac.view().peek(keylet::account(a1.id()));
124 auto sleA2 = ac.view().peek(keylet::account(a2.id()));
125 if (!sleA1 || !sleA2)
126 return false;
127 // Clear the balance so the "account deletion left behind a
128 // non-zero balance" check doesn't trip earlier than the desired
129 // check.
130 sleA1->at(sfBalance) = beast::kZero;
131 sleA2->at(sfBalance) = beast::kZero;
132 ac.view().erase(sleA1);
133 ac.view().erase(sleA2);
134 return true;
135 },
136 XRPAmount{},
137 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
138 }
139
140 void
142 {
143 using namespace test::jtx;
144 testcase << "account root deletion left artifact";
145
147 {{"account deletion left behind a non-zero balance"}},
148 // NOLINTNEXTLINE(readability-identifier-naming)
149 [&](Account const& A1, Account const& A2, ApplyContext& ac) {
150 // A1 has a balance. Delete A1
151 auto const a1 = A1.id();
152 auto const sleA1 = ac.view().peek(keylet::account(a1));
153 if (!sleA1)
154 return false;
155 if (!BEAST_EXPECT(*sleA1->at(sfBalance) != beast::kZero))
156 return false;
157
158 ac.view().erase(sleA1);
159
160 return true;
161 },
162 XRPAmount{},
163 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
164
166 {{"account deletion left behind a non-zero owner count"}},
167 // NOLINTNEXTLINE(readability-identifier-naming)
168 [&](Account const& A1, Account const& A2, ApplyContext& ac) {
169 // Increment A1's owner count, then delete A1
170 auto const a1 = A1.id();
171 auto const sleA1 = ac.view().peek(keylet::account(a1));
172 if (!sleA1)
173 return false;
174 // Clear the balance so the "account deletion left behind a
175 // non-zero balance" check doesn't trip earlier than the desired
176 // check.
177 sleA1->at(sfBalance) = beast::kZero;
178 BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0);
179 increaseOwnerCount(ac.view(), sleA1, {}, 1, ac.journal);
180
181 ac.view().erase(sleA1);
182
183 return true;
184 },
185 XRPAmount{},
186 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
187
189 {{"account deletion left behind a sponsorship field"}},
190 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
191 auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
192 if (!sleA1)
193 return false;
194 sleA1->at(sfBalance) = beast::kZero;
195 sleA1->setFieldU32(sfSponsoredOwnerCount, 1);
196
197 ac.view().erase(sleA1);
198
199 return true;
200 },
201 XRPAmount{},
202 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
203
205 {{"account deletion left behind a sponsorship field"}},
206 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
207 auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
208 if (!sleA1)
209 return false;
210 sleA1->at(sfBalance) = beast::kZero;
211 sleA1->setFieldU32(sfSponsoringOwnerCount, 1);
212
213 ac.view().erase(sleA1);
214
215 return true;
216 },
217 XRPAmount{},
218 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
219
221 {{"account deletion left behind a sponsorship field"}},
222 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
223 auto const a1Id = a1.id();
224 auto const sleA1 = ac.view().peek(keylet::account(a1Id));
225 if (!sleA1)
226 return false;
227 sleA1->at(sfBalance) = beast::kZero;
228 sleA1->setFieldU32(sfSponsoringAccountCount, 1);
229
230 ac.view().erase(sleA1);
231
232 return true;
233 },
234 XRPAmount{},
235 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
236
238 {{"account deletion left behind a sponsorship field"}},
239 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
240 auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
241 if (!sleA1)
242 return false;
243 sleA1->at(sfBalance) = beast::kZero;
244 sleA1->setAccountID(sfSponsor, a2.id());
245
246 ac.view().erase(sleA1);
247
248 return true;
249 },
250 XRPAmount{},
251 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
252
254 Env{*this, FeatureBitset{featureSponsor}},
255 {{"account deletion left behind a sponsorship field"}},
256 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
257 auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
258 if (!sleA1)
259 return false;
260 sleA1->at(sfBalance) = beast::kZero;
261 sleA1->setAccountID(sfSponsor, a2.id());
262
263 ac.view().erase(sleA1);
264
265 return true;
266 },
267 XRPAmount{},
268 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
269
270 for (auto const& [keyletfunc, type, includeInTests] : kDirectAccountKeylets)
271 {
272 if (!includeInTests)
273 continue;
274
275 using namespace std::string_literals;
276
278 {{"account deletion left behind a "s + type.cStr() + " object"}},
279 // NOLINTNEXTLINE(readability-identifier-naming)
280 [&](Account const& A1, Account const& A2, ApplyContext& ac) {
281 // Add an object to the ledger for account A1, then delete
282 // A1
283 auto const a1 = A1.id();
284 auto sleA1 = ac.view().peek(keylet::account(a1));
285 if (!sleA1)
286 return false;
287
288 auto const key = std::invoke(keyletfunc, a1);
289 auto const newSLE = std::make_shared<SLE>(key);
290 ac.view().insert(newSLE);
291 // Clear the balance so the "account deletion left behind a
292 // non-zero balance" check doesn't trip earlier than the
293 // desired check.
294 sleA1->at(sfBalance) = beast::kZero;
295 ac.view().erase(sleA1);
296
297 return true;
298 },
299 XRPAmount{},
300 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
301 }
302
303 // NFT special case
305 {{"account deletion left behind a NFTokenPage object"}},
306 [&](Account const& a1, Account const&, ApplyContext& ac) {
307 // remove an account from the view
308 auto sle = ac.view().peek(keylet::account(a1.id()));
309 if (!sle)
310 return false;
311 // Clear the balance so the "account deletion left behind a
312 // non-zero balance" check doesn't trip earlier than the desired
313 // check.
314 sle->at(sfBalance) = beast::kZero;
315 sle->at(sfOwnerCount) = 0;
316 ac.view().erase(sle);
317 return true;
318 },
319 XRPAmount{},
320 STTx{ttACCOUNT_DELETE, [](STObject& tx) {}},
322 [&](Account const& a1, Account const&, Env& env) {
323 // Preclose callback to mint the NFT which will be deleted in
324 // the Precheck callback above.
325 env(token::mint(a1));
326
327 return true;
328 });
329
330 // AMM special cases
331 AccountID ammAcctID;
332 UInt256 ammKey;
333 Issue ammIssue;
335 {{"account deletion left behind a DirectoryNode object"}},
336 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
337 // Delete the AMM account without cleaning up the directory or
338 // deleting the AMM object
339 auto sle = ac.view().peek(keylet::account(ammAcctID));
340 if (!sle)
341 return false;
342
343 BEAST_EXPECT(sle->at(~sfAMMID));
344 BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
345
346 // Clear the balance so the "account deletion left behind a
347 // non-zero balance" check doesn't trip earlier than the desired
348 // check.
349 sle->at(sfBalance) = beast::kZero;
350 sle->at(sfOwnerCount) = 0;
351 ac.view().erase(sle);
352
353 return true;
354 },
355 XRPAmount{},
356 STTx{ttAMM_WITHDRAW, [](STObject& tx) {}},
358 [&](Account const& a1, Account const& a2, Env& env) {
359 // Preclose callback to create the AMM which will be partially
360 // deleted in the Precheck callback above.
361 AMM const amm(env, a1, XRP(100), a1["USD"](50));
362 ammAcctID = amm.ammAccount();
363 ammKey = amm.ammID();
364 ammIssue = amm.lptIssue();
365 return true;
366 });
368 {{"account deletion left behind a AMM object"}},
369 [&](Account const& a1, Account const& a2, ApplyContext& ac) {
370 // Delete all the AMM's trust lines, remove the AMM from the AMM
371 // account's directory (this deletes the directory), and delete
372 // the AMM account. Do not delete the AMM object.
373 auto sle = ac.view().peek(keylet::account(ammAcctID));
374 if (!sle)
375 return false;
376
377 BEAST_EXPECT(sle->at(~sfAMMID));
378 BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
379
380 for (auto const& trustKeylet :
381 {keylet::trustLine(ammAcctID, a1["USD"]), keylet::trustLine(a1, ammIssue)})
382 {
383 auto const line = ac.view().peek(trustKeylet);
384 if (!line)
385 {
386 return false;
387 }
388
389 STAmount const lowLimit = line->at(sfLowLimit);
390 STAmount const highLimit = line->at(sfHighLimit);
391 BEAST_EXPECT(
393 ac.view(),
394 line,
395 lowLimit.getIssuer(),
396 highLimit.getIssuer(),
397 ac.journal) == tesSUCCESS);
398 }
399
400 auto const ammSle = ac.view().peek(keylet::amm(ammKey));
401 if (!BEAST_EXPECT(ammSle))
402 return false;
403 auto const ownerDirKeylet = keylet::ownerDir(ammAcctID);
404
405 BEAST_EXPECT(
406 ac.view().dirRemove(ownerDirKeylet, ammSle->at(sfOwnerNode), ammKey, false));
407 BEAST_EXPECT(
408 !ac.view().exists(ownerDirKeylet) || ac.view().emptyDirDelete(ownerDirKeylet));
409
410 // Clear the balance so the "account deletion left behind a
411 // non-zero balance" check doesn't trip earlier than the desired
412 // check.
413 sle->at(sfBalance) = beast::kZero;
414 sle->at(sfOwnerCount) = 0;
415 ac.view().erase(sle);
416
417 return true;
418 },
419 XRPAmount{},
420 STTx{ttAMM_WITHDRAW, [](STObject& tx) {}},
422 [&](Account const& a1, Account const& a2, Env& env) {
423 // Preclose callback to create the AMM which will be partially
424 // deleted in the Precheck callback above.
425 AMM const amm(env, a1, XRP(100), a1["USD"](50));
426 ammAcctID = amm.ammAccount();
427 ammKey = amm.ammID();
428 ammIssue = amm.lptIssue();
429 return true;
430 });
431 }
432
433 void
435 {
436 using namespace test::jtx;
437 testcase << "ledger entry types don't match";
439 {{"ledger entry type mismatch"}, {"XRP net change of -1000000000 doesn't match fee 0"}},
440 [](Account const& a1, Account const&, ApplyContext& ac) {
441 // replace an entry in the table with an SLE of a different type
442 auto const sle = ac.view().peek(keylet::account(a1.id()));
443 if (!sle)
444 return false;
445 auto const sleNew = std::make_shared<SLE>(ltTICKET, sle->key());
446 ac.rawView().rawReplace(sleNew);
447 return true;
448 });
449
451 {{"invalid ledger entry type added"}},
452 [](Account const& a1, Account const&, ApplyContext& ac) {
453 // add an entry in the table with an SLE of an invalid type
454 auto const sle = ac.view().peek(keylet::account(a1.id()));
455 if (!sle)
456 return false;
457
458 // make a dummy escrow ledger entry, then change the type to an
459 // unsupported value so that the valid type invariant check
460 // will fail.
461 auto const sleNew = std::make_shared<SLE>(
462 keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
463
464 // We don't use ltNICKNAME directly since it's marked deprecated
465 // to prevent accidental use elsewhere.
466 sleNew->type_ = static_cast<LedgerEntryType>('n');
467 ac.view().insert(sleNew);
468 return true;
469 });
470 }
471
472 void
474 {
475 using namespace test::jtx;
476 testcase << "XRP balance checks";
477
479 {{"Cannot return non-native STAmount as XRPAmount"}},
480 [](Account const& a1, Account const& a2, ApplyContext& ac) {
481 // non-native balance
482 auto const sle = ac.view().peek(keylet::account(a1.id()));
483 if (!sle)
484 return false;
485 STAmount const nonNative(a2["USD"](51));
486 sle->setFieldAmount(sfBalance, nonNative);
487 ac.view().update(sle);
488 return true;
489 });
490
492 {{"incorrect account XRP balance"}, {"XRP net change was positive: 99999999000000001"}},
493 [this](Account const& a1, Account const&, ApplyContext& ac) {
494 // balance exceeds genesis amount
495 auto const sle = ac.view().peek(keylet::account(a1.id()));
496 if (!sle)
497 return false;
498 // Use `drops(1)` to bypass a call to STAmount::canonicalize
499 // with an invalid value
500 sle->setFieldAmount(sfBalance, kInitialXrp + drops(1));
501 BEAST_EXPECT(!sle->getFieldAmount(sfBalance).negative());
502 ac.view().update(sle);
503 return true;
504 });
505
507 {{"incorrect account XRP balance"},
508 {"XRP net change of -1000000001 doesn't match fee 0"}},
509 [this](Account const& a1, Account const&, ApplyContext& ac) {
510 // balance is negative
511 auto const sle = ac.view().peek(keylet::account(a1.id()));
512 if (!sle)
513 return false;
514 sle->setFieldAmount(sfBalance, STAmount{1, true});
515 BEAST_EXPECT(sle->getFieldAmount(sfBalance).negative());
516 ac.view().update(sle);
517 return true;
518 });
519 }
520
521 void
523 {
524 using namespace test::jtx;
525 using namespace std::string_literals;
526 testcase << "Transaction fee checks";
527
529 {{"fee paid was negative: -1"}, {"XRP net change of 0 doesn't match fee -1"}},
530 [](Account const&, Account const&, ApplyContext&) { return true; },
531 XRPAmount{-1});
532
534 {{"fee paid exceeds system limit: "s + to_string(kInitialXrp)},
535 {"XRP net change of 0 doesn't match fee "s + to_string(kInitialXrp)}},
536 [](Account const&, Account const&, ApplyContext&) { return true; },
538
540 {{"fee paid is 20 exceeds fee specified in transaction."},
541 {"XRP net change of 0 doesn't match fee 20"}},
542 [](Account const&, Account const&, ApplyContext&) { return true; },
543 XRPAmount{20},
544 STTx{ttACCOUNT_SET, [](STObject& tx) { tx.setFieldAmount(sfFee, XRPAmount{10}); }});
545 }
546
547 void
549 {
550 using namespace test::jtx;
551 testcase << "no bad offers";
552
554 {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
555 // offer with negative takerpays
556 auto const sle = ac.view().peek(keylet::account(a1.id()));
557 if (!sle)
558 return false;
559 auto sleNew = std::make_shared<SLE>(
560 keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
561 sleNew->setAccountID(sfAccount, a1.id());
562 sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
563 sleNew->setFieldAmount(sfTakerPays, XRP(-1));
564 ac.view().insert(sleNew);
565 return true;
566 });
567
569 {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
570 // offer with negative takergets
571 auto const sle = ac.view().peek(keylet::account(a1.id()));
572 if (!sle)
573 return false;
574 auto sleNew = std::make_shared<SLE>(
575 keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
576 sleNew->setAccountID(sfAccount, a1.id());
577 sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
578 sleNew->setFieldAmount(sfTakerPays, a1["USD"](10));
579 sleNew->setFieldAmount(sfTakerGets, XRP(-1));
580 ac.view().insert(sleNew);
581 return true;
582 });
583
585 {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
586 // offer XRP to XRP
587 auto const sle = ac.view().peek(keylet::account(a1.id()));
588 if (!sle)
589 return false;
590 auto sleNew = std::make_shared<SLE>(
591 keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
592 sleNew->setAccountID(sfAccount, a1.id());
593 sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
594 sleNew->setFieldAmount(sfTakerPays, XRP(10));
595 sleNew->setFieldAmount(sfTakerGets, XRP(11));
596 ac.view().insert(sleNew);
597 return true;
598 });
599 }
600
601 void
603 {
604 using namespace test::jtx;
605 testcase << "valid new account root";
606
608 {{"account root created illegally"}},
609 [](Account const&, Account const&, ApplyContext& ac) {
610 // Insert a new account root created by a non-payment into
611 // the view.
612 Account const a3{"A3"};
613 Keylet const acctKeylet = keylet::account(a3);
614 auto const sleNew = std::make_shared<SLE>(acctKeylet);
615 ac.view().insert(sleNew);
616 return true;
617 });
618
620 {{"multiple accounts created in a single transaction"}},
621 [](Account const&, Account const&, ApplyContext& ac) {
622 // Insert two new account roots into the view.
623 {
624 Account const a3{"A3"};
625 Keylet const acctKeylet = keylet::account(a3);
626 auto const sleA3 = std::make_shared<SLE>(acctKeylet);
627 ac.view().insert(sleA3);
628 }
629 {
630 Account const a4{"A4"};
631 Keylet const acctKeylet = keylet::account(a4);
632 auto const sleA4 = std::make_shared<SLE>(acctKeylet);
633 ac.view().insert(sleA4);
634 }
635 return true;
636 });
637
639 {{"account created with wrong starting sequence number"}},
640 [](Account const&, Account const&, ApplyContext& ac) {
641 // Insert a new account root with the wrong starting sequence.
642 Account const a3{"A3"};
643 Keylet const acctKeylet = keylet::account(a3);
644 auto const sleNew = std::make_shared<SLE>(acctKeylet);
645 sleNew->setFieldU32(sfSequence, ac.view().seq() + 1);
646 ac.view().insert(sleNew);
647 return true;
648 },
649 XRPAmount{},
650 STTx{ttPAYMENT, [](STObject& tx) {}});
651
653 {{"pseudo-account created by a wrong transaction type"}},
654 [](Account const&, Account const&, ApplyContext& ac) {
655 Account const a3{"A3"};
656 Keylet const acctKeylet = keylet::account(a3);
657 auto const sleNew = std::make_shared<SLE>(acctKeylet);
658 sleNew->setFieldU32(sfSequence, 0);
659 sleNew->setFieldH256(sfAMMID, UInt256(1));
660 sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple);
661 ac.view().insert(sleNew);
662 return true;
663 },
664 XRPAmount{},
665 STTx{ttPAYMENT, [](STObject& tx) {}});
666
668 {{"account created with wrong starting sequence number"}},
669 [](Account const&, Account const&, ApplyContext& ac) {
670 Account const a3{"A3"};
671 Keylet const acctKeylet = keylet::account(a3);
672 auto const sleNew = std::make_shared<SLE>(acctKeylet);
673 sleNew->setFieldU32(sfSequence, ac.view().seq());
674 sleNew->setFieldH256(sfAMMID, UInt256(1));
675 sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
676 ac.view().insert(sleNew);
677 return true;
678 },
679 XRPAmount{},
680 STTx{ttAMM_CREATE, [](STObject& tx) {}});
681
683 {{"pseudo-account created with wrong flags"}},
684 [](Account const&, Account const&, ApplyContext& ac) {
685 Account const a3{"A3"};
686 Keylet const acctKeylet = keylet::account(a3);
687 auto const sleNew = std::make_shared<SLE>(acctKeylet);
688 sleNew->setFieldU32(sfSequence, 0);
689 sleNew->setFieldH256(sfAMMID, UInt256(1));
690 sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple);
691 ac.view().insert(sleNew);
692 return true;
693 },
694 XRPAmount{},
695 STTx{ttVAULT_CREATE, [](STObject& tx) {}});
696
698 {{"pseudo-account created with wrong flags"}},
699 [](Account const&, Account const&, ApplyContext& ac) {
700 Account const a3{"A3"};
701 Keylet const acctKeylet = keylet::account(a3);
702 auto const sleNew = std::make_shared<SLE>(acctKeylet);
703 sleNew->setFieldU32(sfSequence, 0);
704 sleNew->setFieldH256(sfAMMID, UInt256(1));
705 sleNew->setFieldU32(
706 sfFlags,
707 lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth | lsfRequireDestTag);
708 ac.view().insert(sleNew);
709 return true;
710 },
711 XRPAmount{},
712 STTx{ttAMM_CREATE, [](STObject& tx) {}});
713 }
714
715 void
717 {
718 testcase("no modified unmodifiable fields");
719 using namespace jtx;
720
721 // Initialize with a placeholder value because there's no default ctor
722 Keylet loanBrokerKeylet = keylet::amendments();
723 Preclose const createLoanBroker = [&, this](Account const& a, Account const& b, Env& env) {
724 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
725
726 loanBrokerKeylet = this->createLoanBroker(a, env, xrpAsset);
727 return BEAST_EXPECT(env.le(loanBrokerKeylet));
728 };
729
730 {
731 auto const mods = std::to_array<std::function<void(SLE::pointer&)>>({
732 [](SLE::pointer& sle) { sle->at(sfSequence) += 1; },
733 [](SLE::pointer& sle) { sle->at(sfOwnerNode) += 1; },
734 [](SLE::pointer& sle) { sle->at(sfVaultNode) += 1; },
735 [](SLE::pointer& sle) { sle->at(sfVaultID) = UInt256(1u); },
736 [](SLE::pointer& sle) { sle->at(sfAccount) = sle->at(sfOwner); },
737 [](SLE::pointer& sle) { sle->at(sfOwner) = sle->at(sfAccount); },
738 [](SLE::pointer& sle) { sle->at(sfManagementFeeRate) += 1; },
739 [](SLE::pointer& sle) { sle->at(sfCoverRateMinimum) += 1; },
740 [](SLE::pointer& sle) { sle->at(sfCoverRateLiquidation) += 1; },
741 [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
742 [](SLE::pointer& sle) { sle->at(sfLedgerIndex) = sle->at(sfVaultID).value(); },
743 });
744
745 for (auto const& mod : mods)
746 {
748 {{"changed an unchangeable field"}},
749 [&](Account const& a1, Account const&, ApplyContext& ac) {
750 auto sle = ac.view().peek(loanBrokerKeylet);
751 if (!sle)
752 return false;
753 mod(sle);
754 ac.view().update(sle);
755 return true;
756 },
757 XRPAmount{},
758 STTx{ttACCOUNT_SET, [](STObject& tx) {}},
761 }
762 }
763
764 // Loan flag immutability lives in NoModifiedUnmodifiableFields's
765 // ltLOAN case: lsfLoanOverpayment must never toggle in either
766 // direction, and lsfLoanDefault (gated on featureLendingProtocolV1_1)
767 // may only transition from unset to set. Each case needs a loan that
768 // already exists in the base ledger, so that the apply-view modification
769 // is seen as a before/after change rather than an insertion.
770 {
771 struct Case
772 {
773 std::uint32_t before;
775 std::string expected;
776 };
777 auto const cases = std::to_array<Case>({
778 {.before = lsfLoanOverpayment,
779 .after = 0,
780 .expected = "lsfLoanOverpayment flag toggled on immutable ledger entry"},
781 {.before = 0,
782 .after = lsfLoanOverpayment,
783 .expected = "lsfLoanOverpayment flag toggled on immutable ledger entry"},
784 {.before = lsfLoanDefault,
785 .after = 0,
786 .expected = "lsfLoanDefault flag cleared on immutable ledger entry"},
787 });
788
789 for (auto const& c : cases)
790 {
791 Env env{*this, all_};
792 Account const a1{"A1"};
793 env.fund(XRP(1000), a1);
794 env.close();
795
796 OpenView ov{*env.current()};
797
798 auto const brokerKeylet =
800 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
801 {
802 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a1.id());
803 sleLoan->at(sfPrincipalOutstanding) = Number(100);
804 sleLoan->at(sfTotalValueOutstanding) = Number(150);
805 sleLoan->setFieldU32(sfPaymentRemaining, 1);
806 sleLoan->setFieldU32(sfFlags, c.before);
807 ov.rawInsert(sleLoan);
808 }
809
810 STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
812 beast::Journal const jlog{sink};
813 ApplyContext ac{
814 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
815 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
816
817 auto sleLoan = ac.view().peek(loanKeylet);
818 if (!BEAST_EXPECT(sleLoan))
819 continue;
820 sleLoan->setFieldU32(sfFlags, c.after);
821 ac.view().update(sleLoan);
822
823 auto transactor = makeTransactor(ac);
824 if (!BEAST_EXPECT(transactor))
825 continue;
826 TER const result = transactor->checkInvariants(
828 BEAST_EXPECT(result == tecINVARIANT_FAILED);
829 BEAST_EXPECT(sink.messages().str().contains(c.expected));
830 }
831 }
832
833 // Pre-featureLendingProtocolV1_1 sibling of the lsfLoanOverpayment
834 // cases above: the same set-once immutability was originally enforced
835 // by ValidLoan::finalize, so with V1_1 disabled toggling the flag
836 // must trip that legacy check instead. lsfLoanDefault immutability
837 // did not exist pre-V1_1 and is not tested here.
838 {
839 auto const cases = std::to_array<std::pair<std::uint32_t, std::uint32_t>>({
840 {lsfLoanOverpayment, 0},
841 {0, lsfLoanOverpayment},
842 });
843
844 for (auto const& [before, after] : cases)
845 {
846 Env env{*this, all_ - featureLendingProtocolV1_1};
847 Account const a1{"A1"};
848 env.fund(XRP(1000), a1);
849 env.close();
850
851 OpenView ov{*env.current()};
852
853 auto const brokerKeylet =
855 auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
856 {
857 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a1.id());
858 sleLoan->at(sfPrincipalOutstanding) = Number(100);
859 sleLoan->at(sfTotalValueOutstanding) = Number(150);
860 sleLoan->setFieldU32(sfPaymentRemaining, 1);
861 sleLoan->setFieldU32(sfFlags, before);
862 ov.rawInsert(sleLoan);
863 }
864
865 STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
867 beast::Journal const jlog{sink};
868 ApplyContext ac{
869 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
870 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
871
872 auto sleLoan = ac.view().peek(loanKeylet);
873 if (!BEAST_EXPECT(sleLoan))
874 continue;
875 sleLoan->setFieldU32(sfFlags, after);
876 ac.view().update(sleLoan);
877
878 auto transactor = makeTransactor(ac);
879 if (!BEAST_EXPECT(transactor))
880 continue;
881 TER const result = transactor->checkInvariants(
883 BEAST_EXPECT(result == tecINVARIANT_FAILED);
884 BEAST_EXPECT(sink.messages().str().contains("Loan Overpayment flag changed"));
885 }
886 }
887
888 // Under featureLendingProtocolV1_1, ValidLoan::finalize requires
889 // interest due (total value minus principal and management fee) to be
890 // non-negative after each value is rounded to sfLoanScale. Test zero,
891 // each way to produce a one-unit deficit, and a two-unit deficit. At
892 // scale 0, an XRP-backed broker rejects any deficit, while an
893 // IOU-backed one permits one unit of rounding tolerance.
894 {
895 struct Case
896 {
897 Number totalValue;
898 Number principal;
899 Number managementFee;
900 bool expectFireIntegral;
901 bool expectFireTolerant;
902 };
903 // The first case sits exactly at the boundary, the middle three
904 // perturb one component so that interest due is -1, which is within
905 // the tolerance, and the last overshoots it at -2.
906 auto const cases = std::to_array<Case>({
907 {.totalValue = Number(100),
908 .principal = Number(100),
909 .managementFee = Number(0),
910 .expectFireIntegral = false,
911 .expectFireTolerant = false},
912 {.totalValue = Number(99),
913 .principal = Number(100),
914 .managementFee = Number(0),
915 .expectFireIntegral = true,
916 .expectFireTolerant = false},
917 {.totalValue = Number(100),
918 .principal = Number(101),
919 .managementFee = Number(0),
920 .expectFireIntegral = true,
921 .expectFireTolerant = false},
922 {.totalValue = Number(100),
923 .principal = Number(100),
924 .managementFee = Number(1),
925 .expectFireIntegral = true,
926 .expectFireTolerant = false},
927 {.totalValue = Number(98),
928 .principal = Number(100),
929 .managementFee = Number(0),
930 .expectFireIntegral = true,
931 .expectFireTolerant = true},
932 });
933
934 for (bool const integralAsset : {true, false})
935 {
936 for (auto const& c : cases)
937 {
938 Env env{*this, all_};
939 Account const a1{"A1"};
940 Account const issuer{"issuer"};
941 env.fund(XRP(1000), a1, issuer);
942 env.close();
943
944 // The check reads the broker's vault asset to decide
945 // whether the rounding tolerance applies, so both
946 // branches need a real broker over the relevant asset.
947 auto const asset = [&]() -> PrettyAsset {
948 if (integralAsset)
949 return PrettyAsset{xrpIssue(), 1'000'000};
950 PrettyAsset const iouAsset = issuer["IOU"];
951 env(trust(a1, iouAsset(1000)));
952 env(pay(issuer, a1, iouAsset(1000)));
953 env.close();
954 return iouAsset;
955 }();
956
957 auto const brokerKeylet = this->createLoanBroker(a1, env, asset);
958 if (!BEAST_EXPECT(env.le(brokerKeylet)))
959 continue;
960 env.close();
961
962 OpenView ov{*env.current()};
963
964 auto const loanKeylet =
965 keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
966 // Seed a loan whose interest due sits at the boundary. The
967 // apply-view update below moves it.
968 {
969 auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a1.id());
970 sleLoan->at(sfPrincipalOutstanding) = Number(100);
971 sleLoan->at(sfTotalValueOutstanding) = Number(100);
972 sleLoan->at(sfManagementFeeOutstanding) = Number(0);
973 sleLoan->at(sfLoanScale) = 0;
974 sleLoan->setFieldU32(sfPaymentRemaining, 1);
975 ov.rawInsert(sleLoan);
976 }
977
978 STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
980 beast::Journal const jlog{sink};
981 ApplyContext ac{
982 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
983 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
984
985 auto sleLoan = ac.view().peek(loanKeylet);
986 if (!BEAST_EXPECT(sleLoan))
987 continue;
988 sleLoan->at(sfTotalValueOutstanding) = c.totalValue;
989 sleLoan->at(sfPrincipalOutstanding) = c.principal;
990 sleLoan->at(sfManagementFeeOutstanding) = c.managementFee;
991 ac.view().update(sleLoan);
992
993 auto transactor = makeTransactor(ac);
994 if (!BEAST_EXPECT(transactor))
995 continue;
996 TER const result = transactor->checkInvariants(
998 auto const messages = sink.messages().str();
999 if (integralAsset ? c.expectFireIntegral : c.expectFireTolerant)
1000 {
1001 BEAST_EXPECT(result == tecINVARIANT_FAILED);
1002 BEAST_EXPECT(messages.contains("Loan interest due is negative"));
1003 }
1004 else
1005 {
1006 // Other invariants may still fire on this raw-inserted
1007 // loan, so only assert the specific message is absent.
1008 BEAST_EXPECT(!messages.contains("Loan interest due is negative"));
1009 }
1010 }
1011 }
1012 }
1013
1014 // VaultKind, SubscriptionDate and RedemptionDate are immutable once set at creation.
1015 // Enforced by NoModifiedUnmodifiableFields on ltVAULT via kFieldChanged.
1016 Keylet closedEndedVaultKeylet = keylet::amendments();
1017 Preclose const createClosedEndedVault = [&, this](
1018 Account const& a, Account const&, Env& env) {
1019 auto const sub = env.now().time_since_epoch().count() + 60;
1020 auto const red = sub + kMinInvestmentPeriod + 1'000'000;
1021 Vault const vault{env};
1022 auto [tx, keylet] = vault.create(
1023 {.owner = a,
1024 .asset = xrpIssue(),
1025 .vaultKind = std::to_underlying(VaultKind::ClosedEnded),
1026 .subscriptionDate = sub,
1027 .redemptionDate = red});
1028 env(tx);
1029 closedEndedVaultKeylet = keylet;
1030 return BEAST_EXPECT(env.le(closedEndedVaultKeylet));
1031 };
1032
1033 {
1034 // Each mutation must keep the vault otherwise valid so that only the immutability check
1035 // fires. Shifting both dates by the same offset preserves the gap; bumping sfVaultKind
1036 // stays within the recognised range.
1037 auto const mods = std::to_array<std::function<void(SLE::pointer&)>>({
1038 [](SLE::pointer& sle) { sle->at(sfVaultKind) += 1; },
1039 [](SLE::pointer& sle) { sle->at(sfSubscriptionDate) += 1; },
1040 [](SLE::pointer& sle) { sle->at(sfRedemptionDate) += 1; },
1041 });
1042
1043 for (auto const& mod : mods)
1044 {
1046 {{"changed an unchangeable field"}},
1047 [&](Account const&, Account const&, ApplyContext& ac) {
1048 auto sle = ac.view().peek(closedEndedVaultKeylet);
1049 if (!sle)
1050 return false;
1051 mod(sle);
1052 ac.view().update(sle);
1053 return true;
1054 },
1055 XRPAmount{},
1056 STTx{ttACCOUNT_SET, [](STObject&) {}},
1058 createClosedEndedVault);
1059 }
1060 }
1061
1062 {
1063 auto const mods = std::to_array<std::function<void(SLE::pointer&)>>({
1064 [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
1065 [](SLE::pointer& sle) { sle->at(sfLedgerIndex) = UInt256(1u); },
1066 });
1067
1068 for (auto const& mod : mods)
1069 {
1071 {{"changed an unchangeable field"}},
1072 [&](Account const& a1, Account const&, ApplyContext& ac) {
1073 auto sle = ac.view().peek(keylet::account(a1.id()));
1074 if (!sle)
1075 return false;
1076 mod(sle);
1077 ac.view().update(sle);
1078 return true;
1079 });
1080 }
1081 }
1082 }
1083
1084 void
1086 {
1087 using namespace test::jtx;
1088 bool const fixEnabled = features[fixCleanup3_1_3];
1091
1092 // Insert two trust line SLEs in hash-sorted order, with the "bad"
1093 // entry at the lower-sorting key so it is visited first by
1094 // ApplyStateTable::visit(). The configurer callables receive the
1095 // SLE and the Issue corresponding to that side's keylet currency.
1096 auto const insertOrderedTrustLinePair = [](ApplyContext& ac,
1097 Account const& a1,
1098 Account const& a2,
1099 Account const& a3,
1100 auto const& badConfig,
1101 auto const& goodConfig) {
1102 char const* const c1 = "USD";
1103 char const* const c2 = "EUR";
1104 auto const k1 = keylet::trustLine(a1, a2, a1[c1].currency);
1105 auto const k2 = keylet::trustLine(a1, a3, a1[c2].currency);
1106
1107 bool const k1First = k1.key < k2.key;
1108 auto const& badKey = k1First ? k1 : k2;
1109 auto const& goodKey = k1First ? k2 : k1;
1110 Issue const badIss{k1First ? a1[c1].currency : a1[c2].currency, a1.id()};
1111 Issue const goodIss{k1First ? a1[c2].currency : a1[c1].currency, a1.id()};
1112
1113 auto const sleBad = std::make_shared<SLE>(badKey);
1114 badConfig(*sleBad, badIss);
1115 ac.view().insert(sleBad);
1116
1117 auto const sleGood = std::make_shared<SLE>(goodKey);
1118 goodConfig(*sleGood, goodIss);
1119 ac.view().insert(sleGood);
1120 };
1121
1122 // Regression: bad XRP trust line followed by a valid trust line.
1123 // With the fix, the invariant catches the violation. Without it,
1124 // the valid entry overwrites the flag to false. The keylet
1125 // currencies are non-XRP (the invariant inspects sfLowLimit /
1126 // sfHighLimit issue, not the keylet currency).
1127 testcase << "overwrite: NoXRPTrustLines" + std::string(fixEnabled ? " fix" : "");
1129 makeEnv(features),
1130 fixEnabled ? std::vector<std::string>{{"an XRP trust line was created"}}
1132 [&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
1133 Account const a3{"A3"};
1134 insertOrderedTrustLinePair(
1135 ac,
1136 a1,
1137 a2,
1138 a3,
1139 [](SLE& sle, Issue const& iss) {
1140 // sfLowLimit has xrpIssue, making isXrp = true
1141 sle.setFieldAmount(sfLowLimit, STAmount{xrpIssue(), 0});
1142 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
1143 },
1144 [](SLE& sle, Issue const& iss) {
1145 sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
1146 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
1147 });
1148 return true;
1149 },
1150 XRPAmount{},
1151 STTx{ttACCOUNT_SET, [](STObject&) {}},
1152 fixEnabled ? failTers : passTers);
1153
1154 // Regression: bad deep-freeze trust line followed by a valid one.
1155 testcase << "overwrite: NoDeepFreeze" + std::string(fixEnabled ? " fix" : "");
1157 makeEnv(features),
1158 fixEnabled ? std::vector<std::string>{{"a trust line with deep freeze flag without "
1159 "normal freeze was created"}}
1161 [&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
1162 Account const a3{"A3"};
1163 insertOrderedTrustLinePair(
1164 ac,
1165 a1,
1166 a2,
1167 a3,
1168 [](SLE& sle, Issue const& iss) {
1169 sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
1170 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
1171 sle.setFieldU32(sfFlags, lsfLowDeepFreeze);
1172 },
1173 [](SLE& sle, Issue const& iss) {
1174 sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
1175 sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
1176 sle.setFieldU32(sfFlags, 0u);
1177 });
1178 return true;
1179 },
1180 XRPAmount{},
1181 STTx{ttACCOUNT_SET, [](STObject&) {}},
1182 fixEnabled ? failTers : passTers);
1183
1184 // Regression: MPT OutstandingAmount exceeds max, but locked <=
1185 // outstanding. Plain assignment would overwrite bad_ = true.
1186 // With the fix, NoZeroEscrow catches it.
1187 // Without the fix, NoZeroEscrow passes but ValidMPTIssuance
1188 // still fires ("a MPT issuance was created").
1189 testcase << "overwrite: NoZeroEscrow MPT" + std::string(fixEnabled ? " fix" : "");
1191 makeEnv(features),
1192 fixEnabled ? std::vector<std::string>{{"escrow specifies invalid amount"}}
1193 : std::vector<std::string>{{"a MPT issuance was created"}},
1194 [](Account const& a1, Account const&, ApplyContext& ac) {
1195 auto const sle = ac.view().peek(keylet::account(a1.id()));
1196 if (!sle)
1197 return false;
1198
1199 MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
1201 // outstanding exceeds kMaxMpTokenAmount -> checkAmount sets bad_
1202 sleNew->setFieldU64(sfOutstandingAmount, kMaxMpTokenAmount + 1);
1203 // locked is valid and <= outstanding -> must NOT clear bad_
1204 sleNew->setFieldU64(sfLockedAmount, 10);
1205 ac.view().insert(sleNew);
1206 return true;
1207 },
1208 XRPAmount{},
1209 STTx{ttACCOUNT_SET, [](STObject&) {}},
1210 failTers);
1211 }
1212
1213 void
1215 {
1216 using namespace test::jtx;
1217 using namespace std::string_literals;
1218 testcase("Sponsorship");
1219 {
1220 auto const expectMessage =
1221 "SponsoredOwnerCount does not equal SponsoringOwnerCount delta.";
1222
1224 {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1225 auto const sle = ac.view().peek(keylet::account(a1.id()));
1226 if (!sle)
1227 return false;
1228 sle->setFieldU32(sfSponsoredOwnerCount, 1);
1229 ac.view().update(sle);
1230 return true;
1231 });
1232
1234 {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1235 auto const sle = ac.view().peek(keylet::account(a1.id()));
1236 if (!sle)
1237 return false;
1238 sle->setFieldU32(sfSponsoringOwnerCount, 1);
1239 ac.view().update(sle);
1240 return true;
1241 });
1242 }
1243
1244 {
1245 auto const expectMessage =
1246 "OwnerCount must be greater than or equal to SponsoredOwnerCount.";
1247
1249 {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1250 auto const sle = ac.view().peek(keylet::account(a1.id()));
1251 if (!sle)
1252 return false;
1253 sle->setFieldU32(sfOwnerCount, 0);
1254 sle->setFieldU32(sfSponsoredOwnerCount, 1);
1255 ac.view().update(sle);
1256
1257 auto const sle2 = ac.view().peek(keylet::account(a2.id()));
1258 if (!sle2)
1259 return false;
1260 sle2->setFieldU32(sfSponsoringOwnerCount, 1);
1261 ac.view().update(sle2);
1262 return true;
1263 });
1264 }
1265
1266 {
1267 auto const expectMessage =
1268 "SponsoredObjectOwnerCount does not equal SponsoredOwnerCount delta.";
1269 UInt256 checkID;
1270
1272 {{expectMessage}},
1273 [&](Account const&, Account const& a2, ApplyContext& ac) {
1274 auto const check = ac.view().peek(keylet::check(checkID));
1275 if (!check)
1276 return false;
1277 check->setAccountID(sfSponsor, a2.id());
1278 ac.view().update(check);
1279 return true;
1280 },
1281 XRPAmount{},
1282 STTx{ttACCOUNT_SET, [](STObject&) {}},
1284 [&checkID](Account const& a1, Account const& a2, Env& env) {
1285 checkID = keylet::check(a1.id(), SeqProxy::rawSequence(env.seq(a1))).key;
1286 env(check::create(a1, a2, XRP(1)));
1287 return true;
1288 });
1289 }
1290
1291 {
1292 auto const expectMessage =
1293 "Invariant failed: Net delta of SponsoringAccountCount does "
1294 "not match net delta of sfSponsor presence.";
1295
1297 {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1298 auto const sle = ac.view().peek(keylet::account(a1.id()));
1299 if (!sle)
1300 return false;
1301 sle->setFieldU32(sfSponsoringAccountCount, 1);
1302 ac.view().update(sle);
1303 return true;
1304 });
1305
1307 {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
1308 auto const sle = ac.view().peek(keylet::account(a1.id()));
1309 if (!sle)
1310 return false;
1311 sle->setAccountID(sfSponsor, a2.id());
1312 ac.view().update(sle);
1313 return true;
1314 });
1315 }
1316 }
1317
1318 void
1320 {
1321 testcase << "object has pseudo-account";
1322 using namespace jtx;
1323
1324 auto const amendments = all_ | fixCleanup3_3_0;
1325
1326 // Vault: object deleted without its pseudo-account
1327 {
1328 Keylet vaultKeylet = keylet::amendments();
1330 Env{*this, amendments},
1331 {{"deleted Vault without deleting its pseudo-account"}},
1332 [&vaultKeylet](Account const&, Account const&, ApplyContext& ac) {
1333 auto sle = ac.view().peek(vaultKeylet);
1334 if (!sle)
1335 return false;
1336 ac.view().erase(sle);
1337 return true;
1338 },
1339 XRPAmount{},
1340 STTx{ttVAULT_DELETE, [](STObject&) {}},
1342 [&vaultKeylet](Account const& a1, Account const&, Env& env) {
1343 Vault const vault{env};
1344 auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
1345 env(tx);
1346 vaultKeylet = keylet;
1347 return true;
1348 });
1349 }
1350
1351 // AMM: object deleted without its pseudo-account
1352 {
1353 UInt256 ammID{};
1354 Account const gw{"gw"};
1356 Env{*this, amendments},
1357 {{"deleted AMM without deleting its pseudo-account"}},
1358 [&ammID](Account const&, Account const&, ApplyContext& ac) {
1359 auto sle = ac.view().peek(keylet::amm(ammID));
1360 if (!sle)
1361 return false;
1362 ac.view().erase(sle);
1363 return true;
1364 },
1365 XRPAmount{},
1366 STTx{ttAMM_DELETE, [](STObject&) {}},
1368 [&ammID, &gw](Account const&, Account const&, Env& env) {
1369 env.fund(XRP(1'000), gw);
1370 AMM const amm(env, gw, XRP(100), gw["USD"](100));
1371 ammID = amm.ammID();
1372 return true;
1373 });
1374 }
1375
1376 // LoanBroker: object deleted without its pseudo-account
1377 {
1378 Keylet loanBrokerKeylet = keylet::amendments();
1380 Env{*this, amendments},
1381 {{"deleted LoanBroker without deleting its pseudo-account"}},
1382 [&loanBrokerKeylet](Account const&, Account const&, ApplyContext& ac) {
1383 auto sle = ac.view().peek(loanBrokerKeylet);
1384 if (!sle)
1385 return false;
1386 ac.view().erase(sle);
1387 return true;
1388 },
1389 XRPAmount{},
1390 STTx{ttLOAN_BROKER_DELETE, [](STObject&) {}},
1392 [&loanBrokerKeylet, this](Account const& a1, Account const&, Env& env) {
1393 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
1394 loanBrokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
1395 return BEAST_EXPECT(env.le(loanBrokerKeylet));
1396 });
1397 }
1398
1399 // Deleted object missing sfAccount field (defensive check).
1400 // Manually construct the view to place a vault SLE without
1401 // sfAccount into the base ledger, then erase it.
1402 {
1403 Env env{*this, amendments};
1404 Account const a1{"A1"};
1405 Account const a2{"A2"};
1406 env.fund(XRP(1000), a1, a2);
1407 env.close();
1408
1409 OpenView ov{*env.current()};
1410
1411 auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ov.seq()));
1412 auto sleVault = std::make_shared<SLE>(vaultKeylet);
1413 sleVault->makeFieldAbsent(sfAccount);
1414 ov.rawInsert(sleVault);
1415
1416 STTx const tx{ttVAULT_DELETE, [](STObject&) {}};
1418 beast::Journal const jlog{sink};
1419 ApplyContext ac{
1420 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
1421 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
1422
1423 auto sle = ac.view().peek(vaultKeylet);
1424 if (!BEAST_EXPECT(sle))
1425 return;
1426 ac.view().erase(sle);
1427
1428 auto transactor = makeTransactor(ac);
1429 if (!BEAST_EXPECT(transactor))
1430 return;
1431 TER const result = transactor->checkInvariants(
1433 BEAST_EXPECT(result == tecINVARIANT_FAILED);
1434 BEAST_EXPECT(sink.messages().str().contains("is missing pseudo-account field"));
1435 }
1436 }
1437
1438 void
1440 {
1441 testcase << "txCheck exception";
1442 using namespace jtx;
1443
1444 // A TxInvariantCheck that throws from the requested hook, so we can
1445 // exercise checkInvariantsHelper's catch block via the
1446 // transaction-specific layer (as opposed to the protocol layer,
1447 // which testObjectHasPseudoAccount's last case already covers via a
1448 // real Transactor's finalizeInvariants).
1449 enum class ThrowFrom { VisitEntry, Finalize };
1450
1451 struct ThrowingTxInvariantCheck : TxInvariantCheck
1452 {
1453 ThrowFrom const throwFrom;
1454
1455 explicit ThrowingTxInvariantCheck(ThrowFrom throwFrom) : throwFrom(throwFrom)
1456 {
1457 }
1458
1459 void
1460 visitEntry(bool, SLE::ConstRef, SLE::ConstRef) override
1461 {
1462 if (throwFrom == ThrowFrom::VisitEntry)
1463 throw std::runtime_error("test-injected visitEntry exception");
1464 }
1465
1466 [[nodiscard]] bool
1467 finalize(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) override
1468 {
1469 if (throwFrom == ThrowFrom::Finalize)
1470 throw std::runtime_error("test-injected finalize exception");
1471 return true;
1472 }
1473 };
1474
1475 for (auto const throwFrom : {ThrowFrom::VisitEntry, ThrowFrom::Finalize})
1476 {
1477 Env env{*this};
1478 Account const alice{"alice"};
1479 env.fund(XRP(1000), alice);
1480 env.close();
1481
1482 OpenView ov{*env.current()};
1483 STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
1485 beast::Journal const jlog{sink};
1486 ApplyContext ac{
1487 env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
1488 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
1489
1490 // visitEntry only runs for entries the transaction touched, so
1491 // make a modification for the traversal to report.
1492 auto sle = ac.view().peek(keylet::account(alice.id()));
1493 if (!BEAST_EXPECT(sle))
1494 return;
1495 sle->at(sfSequence) = sle->at(sfSequence) + 1;
1496 ac.view().update(sle);
1497
1498 ThrowingTxInvariantCheck throwing{throwFrom};
1499 TER terActual = tesSUCCESS;
1500 for (TER const& terExpect : {TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED)})
1501 {
1502 terActual = checkInvariants(ac, terActual, XRPAmount{}, throwing);
1503 BEAST_EXPECT(terExpect == terActual);
1504 BEAST_EXPECT(sink.messages().str().contains(
1505 "Transaction caused an exception during invariant checks"));
1506 }
1507 }
1508 }
1509
1510 void
1512 {
1513 testcase << "txCheck finalize returns false";
1514 using namespace jtx;
1515
1516 // A TxInvariantCheck whose finalize returns false, so we can exercise
1517 // the "Transaction has failed one or more transaction invariants"
1518 // log path in checkInvariantsHelper independently of any real
1519 // transactor. This is the transaction-layer analogue of the
1520 // protocol-layer coverage in testObjectHasPseudoAccount / others.
1521 struct FailingTxInvariantCheck : TxInvariantCheck
1522 {
1523 void
1524 visitEntry(bool, SLE::ConstRef, SLE::ConstRef) override
1525 {
1526 }
1527
1528 [[nodiscard]] bool
1529 finalize(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) override
1530 {
1531 return false;
1532 }
1533 };
1534
1535 Env env{*this};
1536 Account const alice{"alice"};
1537 env.fund(XRP(1000), alice);
1538 env.close();
1539
1540 OpenView ov{*env.current()};
1541 STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
1543 beast::Journal const jlog{sink};
1544 ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
1545 CurrentTransactionRulesGuard const rulesGuard(ov.rules());
1546
1547 FailingTxInvariantCheck failing;
1548 TER terActual = tesSUCCESS;
1549 for (TER const& terExpect : {TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED)})
1550 {
1551 terActual = checkInvariants(ac, terActual, XRPAmount{}, failing);
1552 BEAST_EXPECT(terExpect == terActual);
1553 BEAST_EXPECT(sink.messages().str().contains(
1554 "Transaction has failed one or more transaction invariants"));
1555 // The protocol-layer log must not appear: only the tx-layer
1556 // finalize failed here.
1557 BEAST_EXPECT(!sink.messages().str().contains(
1558 "Transaction has failed one or more global invariants"));
1559 }
1560 }
1561
1562 void
1581};
1582
1583BEAST_DEFINE_TESTSUITE(InvariantsMisc, app, xrpl);
1584
1585} // namespace xrpl::test
A generic endpoint for log messages.
Definition Journal.h:44
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
State information when applying a tx.
ApplyView & view()
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.
virtual void update(SLE::Ref sle)=0
Indicate changes to a peeked SLE.
RAII class to set and restore the current transaction rules.
Definition Rules.h:113
A currency issued by an account.
Definition Issue.h:18
constexpr MPTID const & getMptID() const
Definition MPTIssue.h:43
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:238
Rules const & rules() const override
Returns the tx processing rules.
Definition OpenView.cpp:149
A view into a ledger.
Definition ReadView.h:41
LedgerIndex seq() const
Returns the sequence number of the base ledger.
Definition ReadView.h:115
AccountID const & getIssuer() const
Definition STAmount.h:516
std::shared_ptr< STLedgerEntry > pointer
std::shared_ptr< STLedgerEntry const > const & ConstRef
void setFieldU32(SField const &field, std::uint32_t)
Definition STObject.cpp:743
void setFieldAmount(SField const &field, STAmount const &)
Definition STObject.cpp:803
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
Runtime interface for a transaction-specific invariant check.
Keylet createLoanBroker(jtx::Account const &a, jtx::Env &env, jtx::PrettyAsset const &asset)
test::jtx::Env makeEnv(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(), TER initialResult=tesSUCCESS)
Run a specific test case to put the ledger into a state that will be detected by an invariant.
static SLE::pointer makeLoanSle(UInt256 const &loanBrokerID, std::uint32_t loanSeq, AccountID const &borrower)
std::function< bool(test::jtx::Account const &a, test::jtx::Account const &b, test::jtx::Env &env)> Preclose
void testInvariantOverwrite(FeatureBitset features)
void run() override
Runs the suite.
std::stringstream const & messages() const
Convenience class to test AMM functionality.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
T invoke(T... args)
T make_shared(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet computation functions.
Definition Indexes.h:40
Keylet escrow(AccountID const &src, SeqProxy const &seq) noexcept
An escrow entry.
Definition Indexes.cpp:418
Keylet offer(AccountID const &id, SeqProxy const &seq) noexcept
An offer from an account.
Definition Indexes.cpp:298
Keylet const & amendments() noexcept
The index of the amendment table.
Definition Indexes.cpp:248
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:403
Keylet check(AccountID const &id, SeqProxy const &seq) noexcept
A Check.
Definition Indexes.cpp:360
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:471
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:591
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:597
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:603
Keylet mptokenIssuance(MPTID const &issuanceID) noexcept
Definition Indexes.cpp:567
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:275
Check operations.
Definition check.h:18
json::Value create(A const &account, A const &dest, STAmount const &sendMax)
Create a check.
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
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.
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
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
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
@ tefINVARIANT_FAILED
Definition TER.h:178
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.
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
STLedgerEntry SLE
TER checkInvariants(ApplyContext &ctx, TER result, XRPAmount fee, std::optional< std::reference_wrapper< TxInvariantCheck > > txCheck)
Run all protocol invariant checks plus the transaction-specific check in a single pass over the modif...
BaseUInt< 256 > UInt256
Definition base_uint.h:580
std::unique_ptr< Transactor > makeTransactor(ApplyContext &ctx)
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:644
MPTID makeMptID(std::uint32_t const sequence, AccountID const &account)
Definition Indexes.cpp:206
@ TapNone
Definition ApplyView.h:28
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
TERSubset< CanCvtToTER > TER
Definition TER.h:654
LedgerEntryType
Identifiers for on-ledger objects.
@ tecINVARIANT_FAILED
Definition TER.h:321
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:297
constexpr XRPAmount kInitialXrp
Configure the native currency.
TER trustDelete(ApplyView &view, SLE::Ref sleRippleState, AccountID const &uLowAccountID, AccountID const &uHighAccountID, beast::Journal j)
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:369
@ tesSUCCESS
Definition TER.h:250
T str(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
UInt256 key
Definition Keylet.h:21