xrpld
Loading...
Searching...
No Matches
InvariantCheck.cpp
1#include <xrpl/tx/invariants/InvariantCheck.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/base_uint.h>
5#include <xrpl/beast/utility/Journal.h>
6#include <xrpl/beast/utility/Zero.h>
7#include <xrpl/beast/utility/instrumentation.h>
8#include <xrpl/ledger/ReadView.h>
9#include <xrpl/ledger/helpers/AccountRootHelpers.h>
10#include <xrpl/ledger/helpers/TokenHelpers.h>
11#include <xrpl/protocol/AccountID.h>
12#include <xrpl/protocol/Feature.h>
13#include <xrpl/protocol/Indexes.h>
14#include <xrpl/protocol/Issue.h>
15#include <xrpl/protocol/Keylet.h>
16#include <xrpl/protocol/LedgerFormats.h>
17#include <xrpl/protocol/MPTIssue.h>
18#include <xrpl/protocol/Protocol.h>
19#include <xrpl/protocol/Rules.h>
20#include <xrpl/protocol/SField.h>
21#include <xrpl/protocol/STAmount.h>
22#include <xrpl/protocol/STLedgerEntry.h>
23#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
24#include <xrpl/protocol/STTx.h>
25#include <xrpl/protocol/SystemParameters.h>
26#include <xrpl/protocol/TER.h>
27#include <xrpl/protocol/TxFormats.h>
28#include <xrpl/protocol/UintTypes.h>
29#include <xrpl/protocol/XRPAmount.h>
30#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
31
32#include <algorithm>
33#include <cstdint>
34#include <functional>
35#include <memory>
36#include <optional>
37#include <sstream>
38#include <string>
39#include <vector>
40
41namespace xrpl {
42
43#pragma push_macro("TRANSACTION")
44#undef TRANSACTION
45
46#define TRANSACTION(tag, value, name, delegable, amendment, privileges, ...) \
47 case tag: { \
48 return (privileges) & priv; \
49 }
50
51bool
52hasPrivilege(STTx const& tx, Privilege priv)
53{
54 switch (tx.getTxnType())
55 {
56#include <xrpl/protocol/detail/transactions.macro>
57
58 // Deprecated types
59 default:
60 return false;
61 }
62};
63
64#undef TRANSACTION
65#pragma pop_macro("TRANSACTION")
66
67// Returns the human-readable name of a ledger entry's type, falling back to
68// the numeric type if the format is somehow unknown.
69static std::string
71{
72 auto const item = LedgerFormats::getInstance().findByType(sle.getType());
73
74 if (item == nullptr)
75 {
76 // LCOV_EXCL_START
77 UNREACHABLE("xrpl::ledgerEntryTypeName : ledger entry has no known ledger format");
78 return std::to_string(sle.getType());
79 // LCOV_EXCL_STOP
80 }
81 return item->getName();
82}
83
84void
89
90bool
92 STTx const& tx,
93 TER const,
94 XRPAmount const fee,
95 ReadView const&,
96 beast::Journal const& j)
97{
98 // We should never charge a negative fee
99 if (fee.drops() < 0)
100 {
101 JLOG(j.fatal()) << "Invariant failed: fee paid was negative: " << fee.drops();
102 return false;
103 }
104
105 // We should never charge a fee that's greater than or equal to the
106 // entire XRP supply.
107 if (fee >= kInitialXrp)
108 {
109 JLOG(j.fatal()) << "Invariant failed: fee paid exceeds system limit: " << fee.drops();
110 return false;
111 }
112
113 // We should never charge more for a transaction than the transaction
114 // authorizes. It's possible to charge less in some circumstances.
115 if (fee > tx.getFieldAmount(sfFee).xrp())
116 {
117 JLOG(j.fatal()) << "Invariant failed: fee paid is " << fee.drops()
118 << " exceeds fee specified in transaction.";
119 return false;
120 }
121
122 return true;
123}
124
125//------------------------------------------------------------------------------
126
127void
129{
130 /* We go through all modified ledger entries, looking only at account roots,
131 * escrow payments, and payment channels. We remove from the total any
132 * previous XRP values and add to the total any new XRP values. The net
133 * balance of a payment channel is computed from two fields (amount and
134 * balance) and deletions are ignored for paychan and escrow because the
135 * amount fields have not been adjusted for those in the case of deletion.
136 */
137 if (before)
138 {
139 switch (before->getType())
140 {
141 case ltACCOUNT_ROOT:
142 drops_ -= (*before)[sfBalance].xrp().drops();
143 break;
144 case ltPAYCHAN:
145 drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
146 break;
147 case ltESCROW:
148 if (isXRP((*before)[sfAmount]))
149 drops_ -= (*before)[sfAmount].xrp().drops();
150 break;
151 case ltSPONSORSHIP:
152 if (before->isFieldPresent(sfFeeAmount))
153 {
154 XRPL_ASSERT(
155 isXRP((*before)[sfFeeAmount]),
156 "XRPNotCreated::visitEntry : Sponsorship.FeeAmount is XRP");
157 drops_ -= (*before)[sfFeeAmount].xrp().drops();
158 }
159 break;
160 default:
161 break;
162 }
163 }
164
165 if (!after)
166 {
167 // LCOV_EXCL_START
168 UNREACHABLE("xrpl::XRPNotCreated::visitEntry : after can't be null");
169 return;
170 // LCOV_EXCL_STOP
171 }
172 switch (after->getType())
173 {
174 case ltACCOUNT_ROOT:
175 drops_ += (*after)[sfBalance].xrp().drops();
176 break;
177 case ltPAYCHAN:
178 if (!isDelete)
179 drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops();
180 break;
181 case ltESCROW:
182 if (!isDelete && isXRP((*after)[sfAmount]))
183 drops_ += (*after)[sfAmount].xrp().drops();
184 break;
185 case ltSPONSORSHIP:
186 if (!isDelete && after->isFieldPresent(sfFeeAmount))
187 {
188 XRPL_ASSERT(
189 isXRP((*after)[sfFeeAmount]),
190 "XRPNotCreated::visitEntry : Sponsorship.FeeAmount is XRP");
191 drops_ += (*after)[sfFeeAmount].xrp().drops();
192 }
193 break;
194 default:
195 break;
196 }
197}
198
199bool
201 STTx const& tx,
202 TER const,
203 XRPAmount const fee,
204 ReadView const&,
205 beast::Journal const& j) const
206{
207 // The net change should never be positive, as this would mean that the
208 // transaction created XRP out of thin air. That's not possible.
209 if (drops_ > 0)
210 {
211 JLOG(j.fatal()) << "Invariant failed: XRP net change was positive: " << drops_;
212 return false;
213 }
214
215 // The negative of the net change should be equal to actual fee charged.
216 if (-drops_ != fee.drops())
217 {
218 JLOG(j.fatal()) << "Invariant failed: XRP net change of " << drops_ << " doesn't match fee "
219 << fee.drops();
220 return false;
221 }
222
223 return true;
224}
225
226//------------------------------------------------------------------------------
227
228void
230{
231 auto isBad = [](STAmount const& balance) {
232 if (!balance.native())
233 return true;
234
235 auto const drops = balance.xrp();
236
237 // Can't have more than the number of drops instantiated
238 // in the genesis ledger.
239 if (drops > kInitialXrp)
240 return true;
241
242 // Can't have a negative balance (0 is OK)
243 if (drops < XRPAmount{0})
244 return true;
245
246 return false;
247 };
248
249 if (before && before->getType() == ltACCOUNT_ROOT)
250 bad_ |= isBad((*before)[sfBalance]);
251
252 if (after && after->getType() == ltACCOUNT_ROOT)
253 bad_ |= isBad((*after)[sfBalance]);
254}
255
256bool
258 STTx const&,
259 TER const,
260 XRPAmount const,
261 ReadView const&,
262 beast::Journal const& j) const
263{
264 if (bad_)
265 {
266 JLOG(j.fatal()) << "Invariant failed: incorrect account XRP balance";
267 return false;
268 }
269
270 return true;
271}
272
273//------------------------------------------------------------------------------
274
275void
277{
278 auto isBad = [](STAmount const& pays, STAmount const& gets) {
279 // An offer should never be negative
280 if (pays < beast::kZero)
281 return true;
282
283 if (gets < beast::kZero)
284 return true;
285
286 // Can't have an XRP to XRP offer:
287 return pays.native() && gets.native();
288 };
289
290 if (before && before->getType() == ltOFFER)
291 bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]);
292
293 if (after && after->getType() == ltOFFER)
294 bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]);
295}
296
297bool
299 STTx const&,
300 TER const,
301 XRPAmount const,
302 ReadView const&,
303 beast::Journal const& j) const
304{
305 if (bad_)
306 {
307 JLOG(j.fatal()) << "Invariant failed: offer with a bad amount";
308 return false;
309 }
310
311 return true;
312}
313
314//------------------------------------------------------------------------------
315
316void
318{
319 auto isBad = [](STAmount const& amount) {
320 // XRP case
321 if (amount.native())
322 {
323 if (amount.xrp() <= XRPAmount{0})
324 return true;
325
326 if (amount.xrp() >= kInitialXrp)
327 return true;
328 }
329 else
330 {
331 return amount.asset().visit(
332 [&](Issue const& issue) {
333 // IOU case
334 if (amount <= beast::kZero)
335 return true;
336
337 if (badCurrency() == issue.currency)
338 return true;
339
340 return false;
341 }
342
343 // MPT case
344 ,
345 [&](MPTIssue const&) {
346 if (amount <= beast::kZero)
347 return true;
348
349 if (amount.mpt() > MPTAmount{kMaxMpTokenAmount})
350 return true; // LCOV_EXCL_LINE
351
352 return false;
353 });
354 }
355 return false;
356 };
357
358 if (before && before->getType() == ltESCROW)
359 bad_ |= isBad((*before)[sfAmount]);
360
361 if (after && after->getType() == ltESCROW)
362 bad_ |= isBad((*after)[sfAmount]);
363
364 auto checkAmount = [this](std::int64_t amount) {
365 if (amount > kMaxMpTokenAmount || amount < 0)
366 bad_ |= true;
367 };
368
369 bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
370
371 if (after && after->getType() == ltMPTOKEN_ISSUANCE)
372 {
373 auto const outstanding = (*after)[sfOutstandingAmount];
374 checkAmount(outstanding);
375 if (auto const locked = (*after)[~sfLockedAmount])
376 {
377 checkAmount(*locked);
378 bool const isBad = outstanding < *locked;
379 if (overwriteFixEnabled)
380 {
381 bad_ |= isBad;
382 }
383 else
384 {
385 bad_ = isBad;
386 }
387 }
388 }
389
390 if (after && after->getType() == ltMPTOKEN)
391 {
392 auto const mptAmount = (*after)[sfMPTAmount];
393 checkAmount(mptAmount);
394 if (auto const locked = (*after)[~sfLockedAmount])
395 {
396 checkAmount(*locked);
397 }
398 }
399}
400
401bool
403 STTx const& txn,
404 TER const,
405 XRPAmount const,
406 ReadView const&,
407 beast::Journal const& j) const
408{
409 if (bad_)
410 {
411 JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount";
412 return false;
413 }
414
415 return true;
416}
417
418//------------------------------------------------------------------------------
419
420void
422{
423 if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
425}
426
427bool
429 STTx const& tx,
430 TER const result,
431 XRPAmount const,
432 ReadView const&,
433 beast::Journal const& j) const
434{
435 // AMM account root can be deleted as the result of AMM withdraw/delete
436 // transaction when the total AMM LP Tokens balance goes to 0.
437 // A successful AccountDelete or AMMDelete MUST delete exactly
438 // one account root.
439 if (hasPrivilege(tx, MustDeleteAcct) && isTesSuccess(result))
440 {
441 if (accountsDeleted_ == 1)
442 return true;
443
444 if (accountsDeleted_ == 0)
445 {
446 JLOG(j.fatal()) << "Invariant failed: account deletion "
447 "succeeded without deleting an account";
448 }
449 else
450 {
451 JLOG(j.fatal()) << "Invariant failed: account deletion "
452 "succeeded but deleted multiple accounts!";
453 }
454 return false;
455 }
456
457 // A successful AMMWithdraw/AMMClawback MAY delete one account root
458 // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw
459 // deletes the AMM account, accountsDeleted_ is set if it is deleted.
460 if (hasPrivilege(tx, MayDeleteAcct) && isTesSuccess(result) && accountsDeleted_ == 1)
461 return true;
462
463 if (accountsDeleted_ == 0)
464 return true;
465
466 JLOG(j.fatal()) << "Invariant failed: an account root was deleted";
467 return false;
468}
469
470//------------------------------------------------------------------------------
471
472void
474{
475 if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
476 accountsDeleted_.emplace_back(before, after);
477}
478
479bool
481 STTx const& tx,
482 TER const result,
483 XRPAmount const,
484 ReadView const& view,
485 beast::Journal const& j)
486{
487 // Always check for objects in the ledger, but to prevent differing
488 // transaction processing results, however unlikely, only fail if the
489 // feature is enabled. Enabled, or not, though, a fatal-level message will
490 // be logged
491 [[maybe_unused]] bool const enforce = view.rules().enabled(fixCleanup3_2_0) ||
492 view.rules().enabled(featureSponsor) || view.rules().enabled(featureSingleAssetVault) ||
493 view.rules().enabled(featureLendingProtocol);
494
495 auto const objectExists = [&view, enforce, &j](auto const& keylet) {
496 (void)enforce;
497 if (auto const sle = view.read(keylet))
498 {
499 // Finding the object is bad
500 JLOG(j.fatal()) << "Invariant failed: account deletion left behind a "
501 << ledgerEntryTypeName(*sle) << " object";
502 // The comment above starting with "assert(enforce)" explains this
503 // assert.
504 XRPL_ASSERT(
505 enforce,
506 "xrpl::AccountRootsDeletedClean::finalize::objectExists : "
507 "account deletion left no objects behind");
508 return true;
509 }
510 return false;
511 };
512
513 for (auto const& [before, after] : accountsDeleted_)
514 {
515 auto const accountID = before->getAccountID(sfAccount);
516 // An account should not be deleted with a balance
517 if (after->at(sfBalance) != beast::kZero)
518 {
519 JLOG(j.fatal()) << "Invariant failed: account deletion left "
520 "behind a non-zero balance";
521 XRPL_ASSERT(
522 enforce,
523 "xrpl::AccountRootsDeletedClean::finalize : "
524 "deleted account has zero balance");
525 if (enforce)
526 return false;
527 }
528 // An account should not be deleted with a non-zero owner count
529 if (after->at(sfOwnerCount) != 0)
530 {
531 JLOG(j.fatal()) << "Invariant failed: account deletion left "
532 "behind a non-zero owner count";
533 XRPL_ASSERT(
534 enforce,
535 "xrpl::AccountRootsDeletedClean::finalize : "
536 "deleted account has zero owner count");
537 if (enforce)
538 return false;
539 }
540 // An account should not be deleted with sponsorship fields
541 if (after->isFieldPresent(sfSponsoredOwnerCount) ||
542 after->isFieldPresent(sfSponsoringOwnerCount) ||
543 after->isFieldPresent(sfSponsoringAccountCount) || after->isFieldPresent(sfSponsor))
544 {
545 JLOG(j.fatal()) << "Invariant failed: account deletion left "
546 "behind a sponsorship field";
547 XRPL_ASSERT(
548 enforce,
549 "xrpl::AccountRootsDeletedClean::finalize : "
550 "deleted account has no sponsorship fields");
551 if (enforce)
552 return false;
553 }
554 // Simple types
555 for (auto const& [keyletfunc, _1, _2] : kDirectAccountKeylets)
556 {
557 // TODO: use '_' for both unused variables above once we are in C++26
558 if (objectExists(std::invoke(keyletfunc, accountID)) && enforce)
559 return false;
560 }
561
562 {
563 // NFT pages. nftpage_min and nftpage_max were already explicitly
564 // checked above as entries in directAccountKeylets. This uses
565 // view.succ() to check for any NFT pages in between the two
566 // endpoints.
567 Keylet const first = keylet::nftokenPageMin(accountID);
568 Keylet const last = keylet::nftokenPageMax(accountID);
569
570 std::optional<uint256> key = view.succ(first.key, last.key.next());
571
572 // current page
573 if (key && objectExists(Keylet{ltNFTOKEN_PAGE, *key}) && enforce)
574 return false;
575 }
576
577 // If the account is a pseudo account, then the linked object must
578 // also be deleted. e.g. AMM, Vault, etc.
579 for (auto const& field : getPseudoAccountFields())
580 {
581 if (before->isFieldPresent(*field))
582 {
583 auto const key = before->getFieldH256(*field);
584 if (objectExists(keylet::unchecked(key)) && enforce)
585 return false;
586 }
587 }
588 }
589
590 return true;
591}
592
593//------------------------------------------------------------------------------
594
595void
597{
598 if (before && after && before->getType() != after->getType())
599 typeMismatch_ = true;
600
601 if (after)
602 {
603#pragma push_macro("LEDGER_ENTRY")
604#undef LEDGER_ENTRY
605
606#define LEDGER_ENTRY(tag, ...) case tag:
607
608 switch (after->getType())
609 {
610#include <xrpl/protocol/detail/ledger_entries.macro>
611
612 break;
613 default:
614 invalidTypeAdded_ = true;
615 break;
616 }
617
618#undef LEDGER_ENTRY
619#pragma pop_macro("LEDGER_ENTRY")
620 }
621}
622
623bool
625 STTx const&,
626 TER const,
627 XRPAmount const,
628 ReadView const&,
629 beast::Journal const& j) const
630{
631 if ((!typeMismatch_) && (!invalidTypeAdded_))
632 return true;
633
634 if (typeMismatch_)
635 {
636 JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch";
637 }
638
640 {
641 JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added";
642 }
643
644 return false;
645}
646
647//------------------------------------------------------------------------------
648
649void
651{
652 bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
653
654 if (after && after->getType() == ltRIPPLE_STATE)
655 {
656 // checking the issue directly here instead of
657 // relying on .native() just in case native somehow
658 // were systematically incorrect
659 bool const isXrp = after->getFieldAmount(sfLowLimit).asset() == xrpIssue() ||
660 after->getFieldAmount(sfHighLimit).asset() == xrpIssue();
661 if (overwriteFixEnabled)
662 {
663 xrpTrustLine_ |= isXrp;
664 }
665 else
666 {
667 xrpTrustLine_ = isXrp;
668 }
669 }
670}
671
672bool
674 STTx const&,
675 TER const,
676 XRPAmount const,
677 ReadView const&,
678 beast::Journal const& j) const
679{
680 if (!xrpTrustLine_)
681 return true;
682
683 JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created";
684 return false;
685}
686
687//------------------------------------------------------------------------------
688
689void
691{
692 if (after && after->getType() == ltRIPPLE_STATE)
693 {
694 bool const overwriteFixEnabled = isFeatureEnabled(fixCleanup3_1_3, true);
695
696 bool const lowFreeze = after->isFlag(lsfLowFreeze);
697 bool const lowDeepFreeze = after->isFlag(lsfLowDeepFreeze);
698
699 bool const highFreeze = after->isFlag(lsfHighFreeze);
700 bool const highDeepFreeze = after->isFlag(lsfHighDeepFreeze);
701
702 bool const bad = (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze);
703 if (overwriteFixEnabled)
704 {
706 }
707 else
708 {
710 }
711 }
712}
713
714bool
716 STTx const&,
717 TER const,
718 XRPAmount const,
719 ReadView const&,
720 beast::Journal const& j) const
721{
723 return true;
724
725 JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag "
726 "without normal freeze was created";
727 return false;
728}
729
730//------------------------------------------------------------------------------
731
732void
734{
735 if (!before && after->getType() == ltACCOUNT_ROOT)
736 {
738 accountSeq_ = (*after)[sfSequence];
740 flags_ = after->getFlags();
741 }
742}
743
744bool
746 STTx const& tx,
747 TER const result,
748 XRPAmount const,
749 ReadView const& view,
750 beast::Journal const& j) const
751{
752 if (accountsCreated_ == 0)
753 return true;
754
755 if (accountsCreated_ > 1)
756 {
757 JLOG(j.fatal()) << "Invariant failed: multiple accounts "
758 "created in a single transaction";
759 return false;
760 }
761
762 // From this point on we know exactly one account was created.
764 {
765 bool const pseudoAccount =
767 (view.rules().enabled(featureSingleAssetVault) ||
768 view.rules().enabled(featureLendingProtocol)));
769
770 if (pseudoAccount && !hasPrivilege(tx, CreatePseudoAcct))
771 {
772 JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a "
773 "wrong transaction type";
774 return false;
775 }
776
777 std::uint32_t const startingSeq = pseudoAccount ? 0 : view.seq();
778
779 if (accountSeq_ != startingSeq)
780 {
781 JLOG(j.fatal()) << "Invariant failed: account created with "
782 "wrong starting sequence number";
783 return false;
784 }
785
786 if (pseudoAccount)
787 {
788 std::uint32_t const expected = (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
789 if (flags_ != expected)
790 {
791 JLOG(j.fatal()) << "Invariant failed: pseudo-account created with "
792 "wrong flags";
793 return false;
794 }
795 }
796
797 return true;
798 }
799
800 JLOG(j.fatal()) << "Invariant failed: account root created illegally";
801 return false;
802} // namespace xrpl
803
804//------------------------------------------------------------------------------
805
808 SLE::const_pointer const& sle,
809 AccountID const& holder,
810 AccountID const& issuer,
811 Currency const& currency)
812{
813 if (!sle)
814 return STAmount{Issue{currency, issuer}};
815
816 if (sle->getType() != ltRIPPLE_STATE ||
817 sle->key() != keylet::trustLine(holder, issuer, currency).key)
818 {
819 return std::nullopt;
820 }
821
822 STAmount balance = sle->getFieldAmount(sfBalance);
823 if (holder > issuer)
824 balance.negate();
825 balance.get<Issue>().account = issuer;
826 return balance;
827}
828
829void
831{
832 if (before && before->getType() == ltRIPPLE_STATE)
833 {
835 iou_.before = before;
836 }
837
838 if (!isDelete && after && after->getType() == ltRIPPLE_STATE)
839 iou_.after = after;
840
841 if (before && before->getType() == ltMPTOKEN)
842 {
844 mpt_.before = before;
845 }
846
847 if (!isDelete && after && after->getType() == ltMPTOKEN)
848 mpt_.after = after;
849}
850
851bool
853 STTx const& tx,
854 TER const result,
855 XRPAmount const,
856 ReadView const& view,
857 beast::Journal const& j) const
858{
859 if (tx.getTxnType() != ttCLAWBACK)
860 return true;
861
862 if (isTesSuccess(result))
863 {
864 if (trustlinesChanged_ > 1)
865 {
866 JLOG(j.fatal()) << "Invariant failed: more than one trustline changed.";
867 return false;
868 }
869
870 if (mptokensChanged_ > 1)
871 {
872 JLOG(j.fatal()) << "Invariant failed: more than one mptokens changed.";
873 return false;
874 }
875
876 bool const mptV2Enabled = view.rules().enabled(featureMPTokensV2);
877 if (trustlinesChanged_ != 0 && mptokensChanged_ != 0)
878 {
879 JLOG(j.fatal()) << "Invariant failed: trustline and MPToken both changed.";
880 if (mptV2Enabled)
881 return false;
882 }
883
884 if (trustlinesChanged_ == 1 || (mptV2Enabled && mptokensChanged_ == 1))
885 {
886 STAmount const& amount = tx.getFieldAmount(sfAmount);
887
888 return amount.asset().visit(
889 [&](Issue const& issue) {
890 AccountID const issuer = tx.getAccountID(sfAccount);
891 AccountID const& holder = amount.getIssuer();
892 STAmount const holderBalance = accountHolds(
893 view, holder, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
894
895 if (holderBalance.signum() < 0)
896 {
897 JLOG(j.fatal()) << "Invariant failed: trustline or MPT balance is negative";
898 return false;
899 }
900
901 if (!iou_.before)
902 {
903 JLOG(j.fatal())
904 << "Invariant failed: trustline clawback changed the wrong line";
905 return !mptV2Enabled;
906 }
907
908 auto const beforeBalance = clawbackTrustLineBalanceInHolderTerms(
909 iou_.before, holder, issuer, issue.currency);
910 auto const afterBalance = clawbackTrustLineBalanceInHolderTerms(
911 iou_.after, holder, issuer, issue.currency);
912 if (!beforeBalance || !afterBalance)
913 {
914 JLOG(j.fatal())
915 << "Invariant failed: trustline clawback changed the wrong line";
916 return !mptV2Enabled;
917 }
918
919 STAmount clawAmount = amount;
920 clawAmount.get<Issue>().account = issuer;
921 if (clawAmount <= beast::kZero)
922 {
923 JLOG(j.fatal()) << "Invariant failed: trustline clawback amount is invalid";
924 return !mptV2Enabled;
925 }
926
927 if (*afterBalance > *beforeBalance ||
928 (*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount))
929 {
930 JLOG(j.fatal())
931 << "Invariant failed: trustline clawback balance change is invalid";
932 return !mptV2Enabled;
933 }
934
935 return true;
936 },
937 [&](MPTIssue const& issue) {
938 auto const holder = tx[~sfHolder];
939 if (!holder)
940 {
941 JLOG(j.fatal()) << "Invariant failed: MPT clawback missing holder";
942 return !mptV2Enabled;
943 }
944
945 if (!mpt_.before || !mpt_.after)
946 {
947 JLOG(j.fatal()) << "Invariant failed: MPT clawback token is missing";
948 return !mptV2Enabled;
949 }
950
951 if (mpt_.before->getAccountID(sfAccount) != *holder ||
952 mpt_.after->getAccountID(sfAccount) != *holder ||
953 (*mpt_.before)[sfMPTokenIssuanceID] != issue.getMptID() ||
954 (*mpt_.after)[sfMPTokenIssuanceID] != issue.getMptID())
955 {
956 JLOG(j.fatal()) << "Invariant failed: MPT clawback changed the wrong token";
957 return !mptV2Enabled;
958 }
959
960 auto const before = mpt_.before->getFieldU64(sfMPTAmount);
961 auto const after = mpt_.after->getFieldU64(sfMPTAmount);
962 if (amount.negative() || amount.mantissa() == 0)
963 {
964 JLOG(j.fatal()) << "Invariant failed: MPT clawback amount is invalid";
965 return !mptV2Enabled;
966 }
967 auto const clawAmount = amount.mantissa();
968
969 // MPT balances are unsigned, so validate the raw holder
970 // debit instead of routing through accountHolds().
971 if (after > before || (before - after) != std::min(before, clawAmount))
972 {
973 JLOG(j.fatal())
974 << "Invariant failed: MPT clawback balance change is invalid";
975 return !mptV2Enabled;
976 }
977
978 return true;
979 });
980 }
981 }
982 else
983 {
984 if (trustlinesChanged_ != 0)
985 {
986 JLOG(j.fatal()) << "Invariant failed: some trustlines were changed "
987 "despite failure of the transaction.";
988 return false;
989 }
990
991 if (mptokensChanged_ != 0)
992 {
993 JLOG(j.fatal()) << "Invariant failed: some mptokens were changed "
994 "despite failure of the transaction.";
995 return false;
996 }
997 }
998
999 return true;
1000}
1001
1002//------------------------------------------------------------------------------
1003
1004void
1006{
1007 if (isDelete)
1008 {
1009 // Deletion is ignored
1010 return;
1011 }
1012
1013 if (after && after->getType() == ltACCOUNT_ROOT)
1014 {
1015 bool const isPseudo = [&]() {
1016 // isPseudoAccount checks that any of the pseudo-account fields are
1017 // set.
1019 return true;
1020 // Not all pseudo-accounts have a zero sequence, but all accounts
1021 // with a zero sequence had better be pseudo-accounts.
1022 if (after->at(sfSequence) == 0)
1023 return true;
1024
1025 return false;
1026 }();
1027 if (isPseudo)
1028 {
1029 // Pseudo accounts must have the following properties:
1030 // 1. Exactly one of the pseudo-account fields is set.
1031 // 2. The sequence number is not changed.
1032 // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth
1033 // flags are set.
1034 // 4. The RegularKey is not set.
1035 // 5. The SponsoredOwnerCount, SponsoringOwnerCount, SponsoringAccountCount, Sponsor
1036 // fields are not set.
1037 {
1039
1040 auto const numFields = std::ranges::count_if(
1041 fields,
1042 [&after](SField const* sf) -> bool { return after->isFieldPresent(*sf); });
1043 if (numFields != 1)
1044 {
1045 std::stringstream error;
1046 error << "pseudo-account has " << numFields << " pseudo-account fields set";
1047 errors_.emplace_back(error.str());
1048 }
1049 }
1050 if (before && before->at(sfSequence) != after->at(sfSequence))
1051 {
1052 errors_.emplace_back("pseudo-account sequence changed");
1053 }
1054 if (!after->isFlag(lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth))
1055 {
1056 errors_.emplace_back("pseudo-account flags are not set");
1057 }
1058 if (after->isFieldPresent(sfRegularKey))
1059 {
1060 errors_.emplace_back("pseudo-account has a regular key");
1061 }
1062 if (after->isFieldPresent(sfSponsoredOwnerCount) ||
1063 after->isFieldPresent(sfSponsoringOwnerCount) || after->isFieldPresent(sfSponsor) ||
1064 after->isFieldPresent(sfSponsoringAccountCount))
1065 {
1066 errors_.emplace_back("pseudo-account has a sponsorship field");
1067 }
1068 }
1069 }
1070}
1071
1072bool
1074 STTx const& tx,
1075 TER const,
1076 XRPAmount const,
1077 ReadView const& view,
1078 beast::Journal const& j)
1079{
1080 bool const enforce = view.rules().enabled(featureSingleAssetVault);
1081 XRPL_ASSERT(
1082 errors_.empty() || enforce,
1083 "xrpl::ValidPseudoAccounts::finalize : no bad "
1084 "changes or enforce invariant");
1085 if (!errors_.empty())
1086 {
1087 for (auto const& error : errors_)
1088 {
1089 JLOG(j.fatal()) << "Invariant failed: " << error;
1090 }
1091 if (enforce)
1092 return false;
1093 }
1094 return true;
1095}
1096
1097//------------------------------------------------------------------------------
1098
1099void
1101{
1102 if (isDelete || !before)
1103 {
1104 // Creation and deletion are ignored
1105 return;
1106 }
1107
1108 changedEntries_.emplace(before, after);
1109}
1110
1111bool
1113 STTx const& tx,
1114 TER const,
1115 XRPAmount const,
1116 ReadView const& view,
1117 beast::Journal const& j)
1118{
1119 static auto const kFieldChanged = [](auto const& before, auto const& after, auto const& field) {
1120 bool const beforeField = before->isFieldPresent(field);
1121 bool const afterField = after->isFieldPresent(field);
1122 return beforeField != afterField || (afterField && before->at(field) != after->at(field));
1123 };
1124 for (auto const& slePair : changedEntries_)
1125 {
1126 auto const& before = slePair.first;
1127 auto const& after = slePair.second;
1128 auto const type = after->getType();
1129 // featureLendingProtocol gates enforcement, not detection: changes are
1130 // always logged, but the transaction is only failed once the amendment
1131 // is enabled. Type-specific field lists may add their own gates (see
1132 // ltVAULT).
1133 bool const enforce = view.rules().enabled(featureLendingProtocol);
1134 bool bad = kFieldChanged(before, after, sfLedgerEntryType) ||
1135 kFieldChanged(before, after, sfLedgerIndex);
1136 switch (type)
1137 {
1138 case ltLOAN_BROKER:
1139 bad = bad || kFieldChanged(before, after, sfSequence) ||
1140 kFieldChanged(before, after, sfOwnerNode) ||
1141 kFieldChanged(before, after, sfVaultNode) ||
1142 kFieldChanged(before, after, sfVaultID) ||
1143 kFieldChanged(before, after, sfAccount) ||
1144 kFieldChanged(before, after, sfOwner) ||
1145 kFieldChanged(before, after, sfManagementFeeRate) ||
1146 kFieldChanged(before, after, sfCoverRateMinimum) ||
1147 kFieldChanged(before, after, sfCoverRateLiquidation);
1148 break;
1149 case ltLOAN:
1150 bad = bad || kFieldChanged(before, after, sfSequence) ||
1151 kFieldChanged(before, after, sfOwnerNode) ||
1152 kFieldChanged(before, after, sfLoanBrokerNode) ||
1153 kFieldChanged(before, after, sfLoanBrokerID) ||
1154 kFieldChanged(before, after, sfBorrower) ||
1155 kFieldChanged(before, after, sfLoanOriginationFee) ||
1156 kFieldChanged(before, after, sfLoanServiceFee) ||
1157 kFieldChanged(before, after, sfLatePaymentFee) ||
1158 kFieldChanged(before, after, sfClosePaymentFee) ||
1159 kFieldChanged(before, after, sfOverpaymentFee) ||
1160 kFieldChanged(before, after, sfInterestRate) ||
1161 kFieldChanged(before, after, sfLateInterestRate) ||
1162 kFieldChanged(before, after, sfCloseInterestRate) ||
1163 kFieldChanged(before, after, sfOverpaymentInterestRate) ||
1164 kFieldChanged(before, after, sfStartDate) ||
1165 kFieldChanged(before, after, sfPaymentInterval) ||
1166 kFieldChanged(before, after, sfGracePeriod) ||
1167 kFieldChanged(before, after, sfLoanScale);
1168 break;
1169 case ltVAULT:
1170 /*
1171 * sfAccount, sfAsset and sfShareMPTID are already
1172 * captured by VaultInvariant. The additional fields
1173 * below are introduced by featureLendingProtocolV1_1
1174 * and only exist on V1_1 vaults.
1175 */
1176 if (view.rules().enabled(featureLendingProtocolV1_1))
1177 {
1178 bad = bad || kFieldChanged(before, after, sfVaultKind) ||
1179 kFieldChanged(before, after, sfSubscriptionDate) ||
1180 kFieldChanged(before, after, sfRedemptionDate) ||
1181 kFieldChanged(before, after, sfSequence) ||
1182 kFieldChanged(before, after, sfOwnerNode) ||
1183 kFieldChanged(before, after, sfOwner) ||
1184 kFieldChanged(before, after, sfWithdrawalPolicy) ||
1185 kFieldChanged(before, after, sfScale) ||
1186 kFieldChanged(before, after, sfLEVersion);
1187 }
1188 break;
1189 default:
1190 break;
1191 }
1192 XRPL_ASSERT(
1193 !bad || enforce,
1194 "xrpl::NoModifiedUnmodifiableFields::finalize : no bad "
1195 "changes or enforce invariant");
1196 if (bad)
1197 {
1198 JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for "
1199 << tx.getTransactionID();
1200 if (enforce)
1201 return false;
1202 }
1203 }
1204 return true;
1205}
1206
1207void
1209 bool isDelete,
1212{
1213 if (!isDelete && after)
1214 afterEntries_.push_back(after);
1215}
1216
1217bool
1219 STTx const&,
1220 TER const,
1221 XRPAmount const,
1222 ReadView const& view,
1223 beast::Journal const& j) const
1224{
1225 bool const badLedgerEntry = std::ranges::any_of(
1226 afterEntries_, [&](auto const& sle) { return hasInvalidAmount(*sle, j); });
1227
1228 if (badLedgerEntry)
1229 {
1230 JLOG(j.fatal())
1231 << "Invariant failed: ledger entry contains non-canonical MPT or XRP amount";
1232 return !view.rules().enabled(fixCleanup3_2_0);
1233 }
1234
1235 return true;
1236}
1237
1238void
1240{
1241 if (!isDelete)
1242 return;
1243
1244 // Before should never be null when isDelete = true
1245 if (!before)
1246 {
1247 // LCOV_EXCL_START
1248 UNREACHABLE(
1249 "xrpl::ObjectHasPseudoAccount::visitEntry : deleted ledger entry missing before state");
1250 return;
1251 // LCOV_EXCL_STOP
1252 }
1253
1254 switch (before->getType())
1255 {
1256 case ltAMM:
1257 case ltVAULT:
1258 case ltLOAN_BROKER:
1259 deletedObjSles_.push_back(before);
1260 break;
1261 default:
1262 return;
1263 }
1264}
1265
1266[[nodiscard]] bool
1268 STTx const&,
1269 TER const,
1270 XRPAmount const,
1271 ReadView const& view,
1272 beast::Journal const& j) const
1273{
1274 if (!view.rules().enabled(fixCleanup3_3_0))
1275 return true;
1276
1277 if (deletedObjSles_.empty())
1278 return true;
1279
1280 bool failed = false;
1281 for (auto const& sle : deletedObjSles_)
1282 {
1283 if (!sle->isFieldPresent(sfAccount))
1284 {
1285 JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle)
1286 << " is missing pseudo-account field";
1287 failed = true;
1288 continue;
1289 }
1290
1291 // The pseudo-account must NOT exist on the ledger after the object is deleted.
1292 if (view.exists(keylet::account(sle->getAccountID(sfAccount))))
1293 {
1294 JLOG(j.fatal()) << "Invariant failed: deleted " << ledgerEntryTypeName(*sle)
1295 << " without deleting its pseudo-account";
1296 failed = true;
1297 }
1298 }
1299
1300 return !failed;
1301}
1302
1303} // namespace xrpl
T any_of(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
Stream fatal() const
Definition Journal.h:368
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
std::vector< std::pair< SLE::const_pointer, SLE::const_pointer > > accountsDeleted_
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
constexpr auto visit(Visitors &&... visitors) const -> decltype(auto)
Definition Asset.h:117
BaseUInt next() const
Definition base_uint.h:477
A currency issued by an account.
Definition Issue.h:18
Currency currency
Definition Issue.h:20
Item const * findByType(KeyType type) const
Retrieve a format based on its type.
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
static LedgerFormats const & getInstance()
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
std::set< std::pair< SLE::const_pointer, SLE::const_pointer > > changedEntries_
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 &) const
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
std::vector< SLE::const_pointer > deletedObjSles_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
A view into a ledger.
Definition ReadView.h:41
virtual Rules const & rules() const =0
Returns the tx processing rules.
virtual bool exists(Keylet const &k) const =0
Determine if a state item exists.
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
LedgerIndex seq() const
Returns the sequence number of the base ledger.
Definition ReadView.h:115
virtual std::optional< key_type > succ(key_type const &key, std::optional< key_type > const &last=std::nullopt) const =0
Return the key of the next state item.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:180
Identifies fields.
Definition SField.h:132
constexpr TIss const & get() const
void negate()
Definition STAmount.h:586
std::uint64_t mantissa() const noexcept
Definition STAmount.h:490
int signum() const noexcept
Definition STAmount.h:522
bool negative() const noexcept
Definition STAmount.h:484
bool native() const noexcept
Definition STAmount.h:471
Asset const & asset() const
Definition STAmount.h:496
AccountID const & getIssuer() const
Definition STAmount.h:516
XRPAmount xrp() const
Definition STAmount.cpp:271
LedgerEntryType getType() const
std::shared_ptr< STLedgerEntry const > const & const_ref
std::shared_ptr< STLedgerEntry const > const_pointer
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:643
STAmount const & getFieldAmount(SField const &field) const
Definition STObject.cpp:657
TxType getTxnType() const
Definition STTx.h:226
uint256 getTransactionID() const
Definition STTx.h:238
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
static 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 &) const
std::vector< SLE::const_pointer > afterEntries_
std::uint32_t mptokensChanged_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
std::uint32_t trustlinesChanged_
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
std::vector< std::string > errors_
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
constexpr value_type drops() const
Returns the number of drops.
Definition XRPAmount.h:170
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
void visitEntry(bool, SLE::const_ref, SLE::const_ref)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &) const
T count_if(T... args)
T invoke(T... args)
T min(T... args)
constexpr Zero kZero
Definition Zero.h:30
Keylet computation functions.
Definition Indexes.h:40
Keylet unchecked(uint256 const &key) noexcept
Any ledger entry.
Definition Indexes.cpp:367
Keylet nftokenPageMin(AccountID const &owner)
NFT page keylets.
Definition Indexes.cpp:400
Keylet nftokenPageMax(AccountID const &owner)
A keylet for the owner's last possible NFT page.
Definition Indexes.cpp:408
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
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
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
std::vector< SField const * > const & getPseudoAccountFields()
Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if set.
bool isFeatureEnabled(uint256 const &feature, bool resultIfNoRules)
Check whether a feature is enabled in the current ledger rules.
Definition Rules.cpp:197
bool isXRP(AccountID const &c)
Definition AccountID.h:84
BaseUInt< 160, detail::CurrencyTag > Currency
Currency is a hash representing a specific currency.
Definition UintTypes.h:42
STLedgerEntry SLE
bool hasPrivilege(STTx const &tx, Privilege priv)
static std::optional< STAmount > clawbackTrustLineBalanceInHolderTerms(SLE::const_pointer const &sle, AccountID const &holder, AccountID const &issuer, Currency const &currency)
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:572
static std::string ledgerEntryTypeName(SLE const &sle)
bool hasInvalidAmount(STBase const &field, beast::Journal j)
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
TERSubset< CanCvtToTER > TER
Definition TER.h:647
bool isPseudoAccount(SLE::const_pointer sleAcct, std::set< SField const * > const &pseudoFieldFilter={})
Returns true if and only if sleAcct is a pseudo-account or specific pseudo-accounts in pseudoFieldFil...
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
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j, SpendableHandling includeFullBalance=SpendableHandling::SimpleBalance)
T str(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
uint256 key
Definition Keylet.h:21
T to_string(T... args)