rippled
Loading...
Searching...
No Matches
InvariantCheck.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2012-2016 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <xrpld/app/misc/AMMHelpers.h>
21#include <xrpld/app/misc/AMMUtils.h>
22#include <xrpld/app/misc/CredentialHelpers.h>
23#include <xrpld/app/tx/detail/InvariantCheck.h>
24#include <xrpld/app/tx/detail/NFTokenUtils.h>
25#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
26#include <xrpld/ledger/ReadView.h>
27#include <xrpld/ledger/View.h>
28
29#include <xrpl/basics/Log.h>
30#include <xrpl/protocol/Feature.h>
31#include <xrpl/protocol/FeeUnits.h>
32#include <xrpl/protocol/STArray.h>
33#include <xrpl/protocol/SystemParameters.h>
34#include <xrpl/protocol/TxFormats.h>
35#include <xrpl/protocol/nftPageMask.h>
36
37namespace ripple {
38
39void
41 bool,
44{
45 // nothing to do
46}
47
48bool
50 STTx const& tx,
51 TER const,
52 XRPAmount const fee,
53 ReadView const&,
54 beast::Journal const& j)
55{
56 // We should never charge a negative fee
57 if (fee.drops() < 0)
58 {
59 JLOG(j.fatal()) << "Invariant failed: fee paid was negative: "
60 << fee.drops();
61 return false;
62 }
63
64 // We should never charge a fee that's greater than or equal to the
65 // entire XRP supply.
66 if (fee >= INITIAL_XRP)
67 {
68 JLOG(j.fatal()) << "Invariant failed: fee paid exceeds system limit: "
69 << fee.drops();
70 return false;
71 }
72
73 // We should never charge more for a transaction than the transaction
74 // authorizes. It's possible to charge less in some circumstances.
75 if (fee > tx.getFieldAmount(sfFee).xrp())
76 {
77 JLOG(j.fatal()) << "Invariant failed: fee paid is " << fee.drops()
78 << " exceeds fee specified in transaction.";
79 return false;
80 }
81
82 return true;
83}
84
85//------------------------------------------------------------------------------
86
87void
89 bool isDelete,
90 std::shared_ptr<SLE const> const& before,
92{
93 /* We go through all modified ledger entries, looking only at account roots,
94 * escrow payments, and payment channels. We remove from the total any
95 * previous XRP values and add to the total any new XRP values. The net
96 * balance of a payment channel is computed from two fields (amount and
97 * balance) and deletions are ignored for paychan and escrow because the
98 * amount fields have not been adjusted for those in the case of deletion.
99 */
100 if (before)
101 {
102 switch (before->getType())
103 {
104 case ltACCOUNT_ROOT:
105 drops_ -= (*before)[sfBalance].xrp().drops();
106 break;
107 case ltPAYCHAN:
108 drops_ -=
109 ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
110 break;
111 case ltESCROW:
112 if (isXRP((*before)[sfAmount]))
113 drops_ -= (*before)[sfAmount].xrp().drops();
114 break;
115 default:
116 break;
117 }
118 }
119
120 if (after)
121 {
122 switch (after->getType())
123 {
124 case ltACCOUNT_ROOT:
125 drops_ += (*after)[sfBalance].xrp().drops();
126 break;
127 case ltPAYCHAN:
128 if (!isDelete)
129 drops_ += ((*after)[sfAmount] - (*after)[sfBalance])
130 .xrp()
131 .drops();
132 break;
133 case ltESCROW:
134 if (!isDelete && isXRP((*after)[sfAmount]))
135 drops_ += (*after)[sfAmount].xrp().drops();
136 break;
137 default:
138 break;
139 }
140 }
141}
142
143bool
145 STTx const& tx,
146 TER const,
147 XRPAmount const fee,
148 ReadView const&,
149 beast::Journal const& j)
150{
151 // The net change should never be positive, as this would mean that the
152 // transaction created XRP out of thin air. That's not possible.
153 if (drops_ > 0)
154 {
155 JLOG(j.fatal()) << "Invariant failed: XRP net change was positive: "
156 << drops_;
157 return false;
158 }
159
160 // The negative of the net change should be equal to actual fee charged.
161 if (-drops_ != fee.drops())
162 {
163 JLOG(j.fatal()) << "Invariant failed: XRP net change of " << drops_
164 << " doesn't match fee " << fee.drops();
165 return false;
166 }
167
168 return true;
169}
170
171//------------------------------------------------------------------------------
172
173void
175 bool,
176 std::shared_ptr<SLE const> const& before,
178{
179 auto isBad = [](STAmount const& balance) {
180 if (!balance.native())
181 return true;
182
183 auto const drops = balance.xrp();
184
185 // Can't have more than the number of drops instantiated
186 // in the genesis ledger.
187 if (drops > INITIAL_XRP)
188 return true;
189
190 // Can't have a negative balance (0 is OK)
191 if (drops < XRPAmount{0})
192 return true;
193
194 return false;
195 };
196
197 if (before && before->getType() == ltACCOUNT_ROOT)
198 bad_ |= isBad((*before)[sfBalance]);
199
200 if (after && after->getType() == ltACCOUNT_ROOT)
201 bad_ |= isBad((*after)[sfBalance]);
202}
203
204bool
206 STTx const&,
207 TER const,
208 XRPAmount const,
209 ReadView const&,
210 beast::Journal const& j)
211{
212 if (bad_)
213 {
214 JLOG(j.fatal()) << "Invariant failed: incorrect account XRP balance";
215 return false;
216 }
217
218 return true;
219}
220
221//------------------------------------------------------------------------------
222
223void
225 bool isDelete,
226 std::shared_ptr<SLE const> const& before,
228{
229 auto isBad = [](STAmount const& pays, STAmount const& gets) {
230 // An offer should never be negative
231 if (pays < beast::zero)
232 return true;
233
234 if (gets < beast::zero)
235 return true;
236
237 // Can't have an XRP to XRP offer:
238 return pays.native() && gets.native();
239 };
240
241 if (before && before->getType() == ltOFFER)
242 bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]);
243
244 if (after && after->getType() == ltOFFER)
245 bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]);
246}
247
248bool
250 STTx const&,
251 TER const,
252 XRPAmount const,
253 ReadView const&,
254 beast::Journal const& j)
255{
256 if (bad_)
257 {
258 JLOG(j.fatal()) << "Invariant failed: offer with a bad amount";
259 return false;
260 }
261
262 return true;
263}
264
265//------------------------------------------------------------------------------
266
267void
269 bool isDelete,
270 std::shared_ptr<SLE const> const& before,
272{
273 auto isBad = [](STAmount const& amount) {
274 // XRP case
275 if (amount.native())
276 {
277 if (amount.xrp() <= XRPAmount{0})
278 return true;
279
280 if (amount.xrp() >= INITIAL_XRP)
281 return true;
282 }
283 else
284 {
285 // IOU case
286 if (amount.holds<Issue>())
287 {
288 if (amount <= beast::zero)
289 return true;
290
291 if (badCurrency() == amount.getCurrency())
292 return true;
293 }
294
295 // MPT case
296 if (amount.holds<MPTIssue>())
297 {
298 if (amount <= beast::zero)
299 return true;
300
301 if (amount.mpt() > MPTAmount{maxMPTokenAmount})
302 return true; // LCOV_EXCL_LINE
303 }
304 }
305 return false;
306 };
307
308 if (before && before->getType() == ltESCROW)
309 bad_ |= isBad((*before)[sfAmount]);
310
311 if (after && after->getType() == ltESCROW)
312 bad_ |= isBad((*after)[sfAmount]);
313
314 auto checkAmount = [this](std::int64_t amount) {
315 if (amount > maxMPTokenAmount || amount < 0)
316 bad_ = true;
317 };
318
319 if (after && after->getType() == ltMPTOKEN_ISSUANCE)
320 {
321 auto const outstanding = (*after)[sfOutstandingAmount];
322 checkAmount(outstanding);
323 if (auto const locked = (*after)[~sfLockedAmount])
324 {
325 checkAmount(*locked);
326 bad_ = outstanding < *locked;
327 }
328 }
329
330 if (after && after->getType() == ltMPTOKEN)
331 {
332 auto const mptAmount = (*after)[sfMPTAmount];
333 checkAmount(mptAmount);
334 if (auto const locked = (*after)[~sfLockedAmount])
335 {
336 checkAmount(*locked);
337 }
338 }
339}
340
341bool
343 STTx const& txn,
344 TER const,
345 XRPAmount const,
346 ReadView const& rv,
347 beast::Journal const& j)
348{
349 if (bad_)
350 {
351 JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount";
352 return false;
353 }
354
355 return true;
356}
357
358//------------------------------------------------------------------------------
359
360void
362 bool isDelete,
363 std::shared_ptr<SLE const> const& before,
365{
366 if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
368}
369
370bool
372 STTx const& tx,
373 TER const result,
374 XRPAmount const,
375 ReadView const&,
376 beast::Journal const& j)
377{
378 // AMM account root can be deleted as the result of AMM withdraw/delete
379 // transaction when the total AMM LP Tokens balance goes to 0.
380 // A successful AccountDelete or AMMDelete MUST delete exactly
381 // one account root.
382 if ((tx.getTxnType() == ttACCOUNT_DELETE ||
383 tx.getTxnType() == ttAMM_DELETE ||
384 tx.getTxnType() == ttVAULT_DELETE) &&
385 result == tesSUCCESS)
386 {
387 if (accountsDeleted_ == 1)
388 return true;
389
390 if (accountsDeleted_ == 0)
391 JLOG(j.fatal()) << "Invariant failed: account deletion "
392 "succeeded without deleting an account";
393 else
394 JLOG(j.fatal()) << "Invariant failed: account deletion "
395 "succeeded but deleted multiple accounts!";
396 return false;
397 }
398
399 // A successful AMMWithdraw/AMMClawback MAY delete one account root
400 // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw
401 // deletes the AMM account, accountsDeleted_ is set if it is deleted.
402 if ((tx.getTxnType() == ttAMM_WITHDRAW ||
403 tx.getTxnType() == ttAMM_CLAWBACK) &&
404 result == tesSUCCESS && accountsDeleted_ == 1)
405 return true;
406
407 if (accountsDeleted_ == 0)
408 return true;
409
410 JLOG(j.fatal()) << "Invariant failed: an account root was deleted";
411 return false;
412}
413
414//------------------------------------------------------------------------------
415
416void
418 bool isDelete,
419 std::shared_ptr<SLE const> const& before,
421{
422 if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
423 accountsDeleted_.emplace_back(before);
424}
425
426bool
428 STTx const& tx,
429 TER const result,
430 XRPAmount const,
431 ReadView const& view,
432 beast::Journal const& j)
433{
434 // Always check for objects in the ledger, but to prevent differing
435 // transaction processing results, however unlikely, only fail if the
436 // feature is enabled. Enabled, or not, though, a fatal-level message will
437 // be logged
438 [[maybe_unused]] bool const enforce =
439 view.rules().enabled(featureInvariantsV1_1);
440
441 auto const objectExists = [&view, enforce, &j](auto const& keylet) {
442 (void)enforce;
443 if (auto const sle = view.read(keylet))
444 {
445 // Finding the object is bad
446 auto const typeName = [&sle]() {
447 auto item =
448 LedgerFormats::getInstance().findByType(sle->getType());
449
450 if (item != nullptr)
451 return item->getName();
452 return std::to_string(sle->getType());
453 }();
454
455 JLOG(j.fatal())
456 << "Invariant failed: account deletion left behind a "
457 << typeName << " object";
458 XRPL_ASSERT(
459 enforce,
460 "ripple::AccountRootsDeletedClean::finalize::objectExists : "
461 "account deletion left no objects behind");
462 return true;
463 }
464 return false;
465 };
466
467 for (auto const& accountSLE : accountsDeleted_)
468 {
469 auto const accountID = accountSLE->getAccountID(sfAccount);
470 // Simple types
471 for (auto const& [keyletfunc, _, __] : directAccountKeylets)
472 {
473 if (objectExists(std::invoke(keyletfunc, accountID)) && enforce)
474 return false;
475 }
476
477 {
478 // NFT pages. ntfpage_min and nftpage_max were already explicitly
479 // checked above as entries in directAccountKeylets. This uses
480 // view.succ() to check for any NFT pages in between the two
481 // endpoints.
482 Keylet const first = keylet::nftpage_min(accountID);
483 Keylet const last = keylet::nftpage_max(accountID);
484
485 std::optional<uint256> key = view.succ(first.key, last.key.next());
486
487 // current page
488 if (key && objectExists(Keylet{ltNFTOKEN_PAGE, *key}) && enforce)
489 return false;
490 }
491
492 // Keys directly stored in the AccountRoot object
493 if (auto const ammKey = accountSLE->at(~sfAMMID))
494 {
495 if (objectExists(keylet::amm(*ammKey)) && enforce)
496 return false;
497 }
498 }
499
500 return true;
501}
502
503//------------------------------------------------------------------------------
504
505void
507 bool,
508 std::shared_ptr<SLE const> const& before,
510{
511 if (before && after && before->getType() != after->getType())
512 typeMismatch_ = true;
513
514 if (after)
515 {
516 switch (after->getType())
517 {
518 case ltACCOUNT_ROOT:
519 case ltDELEGATE:
520 case ltDIR_NODE:
521 case ltRIPPLE_STATE:
522 case ltTICKET:
523 case ltSIGNER_LIST:
524 case ltOFFER:
525 case ltLEDGER_HASHES:
526 case ltAMENDMENTS:
527 case ltFEE_SETTINGS:
528 case ltESCROW:
529 case ltPAYCHAN:
530 case ltCHECK:
531 case ltDEPOSIT_PREAUTH:
532 case ltNEGATIVE_UNL:
533 case ltNFTOKEN_PAGE:
534 case ltNFTOKEN_OFFER:
535 case ltAMM:
536 case ltBRIDGE:
537 case ltXCHAIN_OWNED_CLAIM_ID:
538 case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
539 case ltDID:
540 case ltORACLE:
541 case ltMPTOKEN_ISSUANCE:
542 case ltMPTOKEN:
543 case ltCREDENTIAL:
544 case ltPERMISSIONED_DOMAIN:
545 case ltVAULT:
546 break;
547 default:
548 invalidTypeAdded_ = true;
549 break;
550 }
551 }
552}
553
554bool
556 STTx const&,
557 TER const,
558 XRPAmount const,
559 ReadView const&,
560 beast::Journal const& j)
561{
562 if ((!typeMismatch_) && (!invalidTypeAdded_))
563 return true;
564
565 if (typeMismatch_)
566 {
567 JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch";
568 }
569
571 {
572 JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added";
573 }
574
575 return false;
576}
577
578//------------------------------------------------------------------------------
579
580void
582 bool,
585{
586 if (after && after->getType() == ltRIPPLE_STATE)
587 {
588 // checking the issue directly here instead of
589 // relying on .native() just in case native somehow
590 // were systematically incorrect
592 after->getFieldAmount(sfLowLimit).issue() == xrpIssue() ||
593 after->getFieldAmount(sfHighLimit).issue() == xrpIssue();
594 }
595}
596
597bool
599 STTx const&,
600 TER const,
601 XRPAmount const,
602 ReadView const&,
603 beast::Journal const& j)
604{
605 if (!xrpTrustLine_)
606 return true;
607
608 JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created";
609 return false;
610}
611
612//------------------------------------------------------------------------------
613
614void
616 bool,
619{
620 if (after && after->getType() == ltRIPPLE_STATE)
621 {
622 std::uint32_t const uFlags = after->getFieldU32(sfFlags);
623 bool const lowFreeze = uFlags & lsfLowFreeze;
624 bool const lowDeepFreeze = uFlags & lsfLowDeepFreeze;
625
626 bool const highFreeze = uFlags & lsfHighFreeze;
627 bool const highDeepFreeze = uFlags & lsfHighDeepFreeze;
628
630 (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze);
631 }
632}
633
634bool
636 STTx const&,
637 TER const,
638 XRPAmount const,
639 ReadView const&,
640 beast::Journal const& j)
641{
643 return true;
644
645 JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag "
646 "without normal freeze was created";
647 return false;
648}
649
650//------------------------------------------------------------------------------
651
652void
654 bool isDelete,
655 std::shared_ptr<SLE const> const& before,
657{
658 /*
659 * A trust line freeze state alone doesn't determine if a transfer is
660 * frozen. The transfer must be examined "end-to-end" because both sides of
661 * the transfer may have different freeze states and freeze impact depends
662 * on the transfer direction. This is why first we need to track the
663 * transfers using IssuerChanges senders/receivers.
664 *
665 * Only in validateIssuerChanges, after we collected all changes can we
666 * determine if the transfer is valid.
667 */
668 if (!isValidEntry(before, after))
669 {
670 return;
671 }
672
673 auto const balanceChange = calculateBalanceChange(before, after, isDelete);
674 if (balanceChange.signum() == 0)
675 {
676 return;
677 }
678
679 recordBalanceChanges(after, balanceChange);
680}
681
682bool
684 STTx const& tx,
685 TER const ter,
686 XRPAmount const fee,
687 ReadView const& view,
688 beast::Journal const& j)
689{
690 /*
691 * We check this invariant regardless of deep freeze amendment status,
692 * allowing for detection and logging of potential issues even when the
693 * amendment is disabled.
694 *
695 * If an exploit that allows moving frozen assets is discovered,
696 * we can alert operators who monitor fatal messages and trigger assert in
697 * debug builds for an early warning.
698 *
699 * In an unlikely event that an exploit is found, this early detection
700 * enables encouraging the UNL to expedite deep freeze amendment activation
701 * or deploy hotfixes via new amendments. In case of a new amendment, we'd
702 * only have to change this line setting 'enforce' variable.
703 * enforce = view.rules().enabled(featureDeepFreeze) ||
704 * view.rules().enabled(fixFreezeExploit);
705 */
706 [[maybe_unused]] bool const enforce =
707 view.rules().enabled(featureDeepFreeze);
708
709 for (auto const& [issue, changes] : balanceChanges_)
710 {
711 auto const issuerSle = findIssuer(issue.account, view);
712 // It should be impossible for the issuer to not be found, but check
713 // just in case so rippled doesn't crash in release.
714 if (!issuerSle)
715 {
716 XRPL_ASSERT(
717 enforce,
718 "ripple::TransfersNotFrozen::finalize : enforce "
719 "invariant.");
720 if (enforce)
721 {
722 return false;
723 }
724 continue;
725 }
726
727 if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce))
728 {
729 return false;
730 }
731 }
732
733 return true;
734}
735
736bool
738 std::shared_ptr<SLE const> const& before,
740{
741 // `after` can never be null, even if the trust line is deleted.
742 XRPL_ASSERT(
743 after, "ripple::TransfersNotFrozen::isValidEntry : valid after.");
744 if (!after)
745 {
746 return false;
747 }
748
749 if (after->getType() == ltACCOUNT_ROOT)
750 {
751 possibleIssuers_.emplace(after->at(sfAccount), after);
752 return false;
753 }
754
755 /* While LedgerEntryTypesMatch invariant also checks types, all invariants
756 * are processed regardless of previous failures.
757 *
758 * This type check is still necessary here because it prevents potential
759 * issues in subsequent processing.
760 */
761 return after->getType() == ltRIPPLE_STATE &&
762 (!before || before->getType() == ltRIPPLE_STATE);
763}
764
767 std::shared_ptr<SLE const> const& before,
769 bool isDelete)
770{
771 auto const getBalance = [](auto const& line, auto const& other, bool zero) {
772 STAmount amt =
773 line ? line->at(sfBalance) : other->at(sfBalance).zeroed();
774 return zero ? amt.zeroed() : amt;
775 };
776
777 /* Trust lines can be created dynamically by other transactions such as
778 * Payment and OfferCreate that cross offers. Such trust line won't be
779 * created frozen, but the sender might be, so the starting balance must be
780 * treated as zero.
781 */
782 auto const balanceBefore = getBalance(before, after, false);
783
784 /* Same as above, trust lines can be dynamically deleted, and for frozen
785 * trust lines, payments not involving the issuer must be blocked. This is
786 * achieved by treating the final balance as zero when isDelete=true to
787 * ensure frozen line restrictions are enforced even during deletion.
788 */
789 auto const balanceAfter = getBalance(after, before, isDelete);
790
791 return balanceAfter - balanceBefore;
792}
793
794void
796{
797 XRPL_ASSERT(
798 change.balanceChangeSign,
799 "ripple::TransfersNotFrozen::recordBalance : valid trustline "
800 "balance sign.");
801 auto& changes = balanceChanges_[issue];
802 if (change.balanceChangeSign < 0)
803 changes.senders.emplace_back(std::move(change));
804 else
805 changes.receivers.emplace_back(std::move(change));
806}
807
808void
811 STAmount const& balanceChange)
812{
813 auto const balanceChangeSign = balanceChange.signum();
814 auto const currency = after->at(sfBalance).getCurrency();
815
816 // Change from low account's perspective, which is trust line default
818 {currency, after->at(sfHighLimit).getIssuer()},
819 {after, balanceChangeSign});
820
821 // Change from high account's perspective, which reverses the sign.
823 {currency, after->at(sfLowLimit).getIssuer()},
824 {after, -balanceChangeSign});
825}
826
829{
830 if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end())
831 {
832 return it->second;
833 }
834
835 return view.read(keylet::account(issuerID));
836}
837
838bool
840 std::shared_ptr<SLE const> const& issuer,
841 IssuerChanges const& changes,
842 STTx const& tx,
843 beast::Journal const& j,
844 bool enforce)
845{
846 if (!issuer)
847 {
848 return false;
849 }
850
851 bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze);
852 if (changes.receivers.empty() || changes.senders.empty())
853 {
854 /* If there are no receivers, then the holder(s) are returning
855 * their tokens to the issuer. Likewise, if there are no
856 * senders, then the issuer is issuing tokens to the holder(s).
857 * This is allowed regardless of the issuer's freeze flags. (The
858 * holder may have contradicting freeze flags, but that will be
859 * checked when the holder is treated as issuer.)
860 */
861 return true;
862 }
863
864 for (auto const& actors : {changes.senders, changes.receivers})
865 {
866 for (auto const& change : actors)
867 {
868 bool const high = change.line->at(sfLowLimit).getIssuer() ==
869 issuer->at(sfAccount);
870
872 change, high, tx, j, enforce, globalFreeze))
873 {
874 return false;
875 }
876 }
877 }
878 return true;
879}
880
881bool
883 BalanceChange const& change,
884 bool high,
885 STTx const& tx,
886 beast::Journal const& j,
887 bool enforce,
888 bool globalFreeze)
889{
890 bool const freeze = change.balanceChangeSign < 0 &&
891 change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze);
892 bool const deepFreeze =
893 change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze);
894 bool const frozen = globalFreeze || deepFreeze || freeze;
895
896 bool const isAMMLine = change.line->isFlag(lsfAMMNode);
897
898 if (!frozen)
899 {
900 return true;
901 }
902
903 // AMMClawbacks are allowed to override some freeze rules
904 if ((!isAMMLine || globalFreeze) && tx.getTxnType() == ttAMM_CLAWBACK)
905 {
906 JLOG(j.debug()) << "Invariant check allowing funds to be moved "
907 << (change.balanceChangeSign > 0 ? "to" : "from")
908 << " a frozen trustline for AMMClawback "
909 << tx.getTransactionID();
910 return true;
911 }
912
913 JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for "
914 << tx.getTransactionID();
915 XRPL_ASSERT(
916 enforce,
917 "ripple::TransfersNotFrozen::validateFrozenState : enforce "
918 "invariant.");
919
920 if (enforce)
921 {
922 return false;
923 }
924
925 return true;
926}
927
928//------------------------------------------------------------------------------
929
930void
932 bool,
933 std::shared_ptr<SLE const> const& before,
935{
936 if (!before && after->getType() == ltACCOUNT_ROOT)
937 {
939 accountSeq_ = (*after)[sfSequence];
941 flags_ = after->getFlags();
942 }
943}
944
945bool
947 STTx const& tx,
948 TER const result,
949 XRPAmount const,
950 ReadView const& view,
951 beast::Journal const& j)
952{
953 if (accountsCreated_ == 0)
954 return true;
955
956 if (accountsCreated_ > 1)
957 {
958 JLOG(j.fatal()) << "Invariant failed: multiple accounts "
959 "created in a single transaction";
960 return false;
961 }
962
963 // From this point on we know exactly one account was created.
964 if ((tx.getTxnType() == ttPAYMENT || tx.getTxnType() == ttAMM_CREATE ||
965 tx.getTxnType() == ttVAULT_CREATE ||
966 tx.getTxnType() == ttXCHAIN_ADD_CLAIM_ATTESTATION ||
967 tx.getTxnType() == ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION) &&
968 result == tesSUCCESS)
969 {
970 bool const pseudoAccount =
971 (pseudoAccount_ && view.rules().enabled(featureSingleAssetVault));
972
973 if (pseudoAccount && tx.getTxnType() != ttAMM_CREATE &&
974 tx.getTxnType() != ttVAULT_CREATE)
975 {
976 JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a "
977 "wrong transaction type";
978 return false;
979 }
980
981 std::uint32_t const startingSeq = //
982 pseudoAccount //
983 ? 0 //
984 : view.rules().enabled(featureDeletableAccounts) //
985 ? view.seq() //
986 : 1;
987
988 if (accountSeq_ != startingSeq)
989 {
990 JLOG(j.fatal()) << "Invariant failed: account created with "
991 "wrong starting sequence number";
992 return false;
993 }
994
995 if (pseudoAccount)
996 {
997 std::uint32_t const expected =
999 if (flags_ != expected)
1000 {
1001 JLOG(j.fatal())
1002 << "Invariant failed: pseudo-account created with "
1003 "wrong flags";
1004 return false;
1005 }
1006 }
1007
1008 return true;
1009 }
1010
1011 JLOG(j.fatal()) << "Invariant failed: account root created illegally";
1012 return false;
1013}
1014
1015//------------------------------------------------------------------------------
1016
1017void
1019 bool isDelete,
1020 std::shared_ptr<SLE const> const& before,
1022{
1023 static constexpr uint256 const& pageBits = nft::pageMask;
1024 static constexpr uint256 const accountBits = ~pageBits;
1025
1026 if ((before && before->getType() != ltNFTOKEN_PAGE) ||
1027 (after && after->getType() != ltNFTOKEN_PAGE))
1028 return;
1029
1030 auto check = [this, isDelete](std::shared_ptr<SLE const> const& sle) {
1031 uint256 const account = sle->key() & accountBits;
1032 uint256 const hiLimit = sle->key() & pageBits;
1033 std::optional<uint256> const prev = (*sle)[~sfPreviousPageMin];
1034
1035 // Make sure that any page links...
1036 // 1. Are properly associated with the owning account and
1037 // 2. The page is correctly ordered between links.
1038 if (prev)
1039 {
1040 if (account != (*prev & accountBits))
1041 badLink_ = true;
1042
1043 if (hiLimit <= (*prev & pageBits))
1044 badLink_ = true;
1045 }
1046
1047 if (auto const next = (*sle)[~sfNextPageMin])
1048 {
1049 if (account != (*next & accountBits))
1050 badLink_ = true;
1051
1052 if (hiLimit >= (*next & pageBits))
1053 badLink_ = true;
1054 }
1055
1056 {
1057 auto const& nftokens = sle->getFieldArray(sfNFTokens);
1058
1059 // An NFTokenPage should never contain too many tokens or be empty.
1060 if (std::size_t const nftokenCount = nftokens.size();
1061 (!isDelete && nftokenCount == 0) ||
1062 nftokenCount > dirMaxTokensPerPage)
1063 invalidSize_ = true;
1064
1065 // If prev is valid, use it to establish a lower bound for
1066 // page entries. If prev is not valid the lower bound is zero.
1067 uint256 const loLimit =
1068 prev ? *prev & pageBits : uint256(beast::zero);
1069
1070 // Also verify that all NFTokenIDs in the page are sorted.
1071 uint256 loCmp = loLimit;
1072 for (auto const& obj : nftokens)
1073 {
1074 uint256 const tokenID = obj[sfNFTokenID];
1075 if (!nft::compareTokens(loCmp, tokenID))
1076 badSort_ = true;
1077 loCmp = tokenID;
1078
1079 // None of the NFTs on this page should belong on lower or
1080 // higher pages.
1081 if (uint256 const tokenPageBits = tokenID & pageBits;
1082 tokenPageBits < loLimit || tokenPageBits >= hiLimit)
1083 badEntry_ = true;
1084
1085 if (auto uri = obj[~sfURI]; uri && uri->empty())
1086 badURI_ = true;
1087 }
1088 }
1089 };
1090
1091 if (before)
1092 {
1093 check(before);
1094
1095 // While an account's NFToken directory contains any NFTokens, the last
1096 // NFTokenPage (with 96 bits of 1 in the low part of the index) should
1097 // never be deleted.
1098 if (isDelete && (before->key() & nft::pageMask) == nft::pageMask &&
1099 before->isFieldPresent(sfPreviousPageMin))
1100 {
1101 deletedFinalPage_ = true;
1102 }
1103 }
1104
1105 if (after)
1106 check(after);
1107
1108 if (!isDelete && before && after)
1109 {
1110 // If the NFTokenPage
1111 // 1. Has a NextMinPage field in before, but loses it in after, and
1112 // 2. This is not the last page in the directory
1113 // Then we have identified a corruption in the links between the
1114 // NFToken pages in the NFToken directory.
1115 if ((before->key() & nft::pageMask) != nft::pageMask &&
1116 before->isFieldPresent(sfNextPageMin) &&
1117 !after->isFieldPresent(sfNextPageMin))
1118 {
1119 deletedLink_ = true;
1120 }
1121 }
1122}
1123
1124bool
1126 STTx const& tx,
1127 TER const result,
1128 XRPAmount const,
1129 ReadView const& view,
1130 beast::Journal const& j)
1131{
1132 if (badLink_)
1133 {
1134 JLOG(j.fatal()) << "Invariant failed: NFT page is improperly linked.";
1135 return false;
1136 }
1137
1138 if (badEntry_)
1139 {
1140 JLOG(j.fatal()) << "Invariant failed: NFT found in incorrect page.";
1141 return false;
1142 }
1143
1144 if (badSort_)
1145 {
1146 JLOG(j.fatal()) << "Invariant failed: NFTs on page are not sorted.";
1147 return false;
1148 }
1149
1150 if (badURI_)
1151 {
1152 JLOG(j.fatal()) << "Invariant failed: NFT contains empty URI.";
1153 return false;
1154 }
1155
1156 if (invalidSize_)
1157 {
1158 JLOG(j.fatal()) << "Invariant failed: NFT page has invalid size.";
1159 return false;
1160 }
1161
1162 if (view.rules().enabled(fixNFTokenPageLinks))
1163 {
1165 {
1166 JLOG(j.fatal()) << "Invariant failed: Last NFT page deleted with "
1167 "non-empty directory.";
1168 return false;
1169 }
1170 if (deletedLink_)
1171 {
1172 JLOG(j.fatal()) << "Invariant failed: Lost NextMinPage link.";
1173 return false;
1174 }
1175 }
1176
1177 return true;
1178}
1179
1180//------------------------------------------------------------------------------
1181void
1183 bool,
1184 std::shared_ptr<SLE const> const& before,
1186{
1187 if (before && before->getType() == ltACCOUNT_ROOT)
1188 {
1189 beforeMintedTotal += (*before)[~sfMintedNFTokens].value_or(0);
1190 beforeBurnedTotal += (*before)[~sfBurnedNFTokens].value_or(0);
1191 }
1192
1193 if (after && after->getType() == ltACCOUNT_ROOT)
1194 {
1195 afterMintedTotal += (*after)[~sfMintedNFTokens].value_or(0);
1196 afterBurnedTotal += (*after)[~sfBurnedNFTokens].value_or(0);
1197 }
1198}
1199
1200bool
1202 STTx const& tx,
1203 TER const result,
1204 XRPAmount const,
1205 ReadView const& view,
1206 beast::Journal const& j)
1207{
1208 if (TxType const txType = tx.getTxnType();
1209 txType != ttNFTOKEN_MINT && txType != ttNFTOKEN_BURN)
1210 {
1212 {
1213 JLOG(j.fatal()) << "Invariant failed: the number of minted tokens "
1214 "changed without a mint transaction!";
1215 return false;
1216 }
1217
1219 {
1220 JLOG(j.fatal()) << "Invariant failed: the number of burned tokens "
1221 "changed without a burn transaction!";
1222 return false;
1223 }
1224
1225 return true;
1226 }
1227
1228 if (tx.getTxnType() == ttNFTOKEN_MINT)
1229 {
1230 if (result == tesSUCCESS && beforeMintedTotal >= afterMintedTotal)
1231 {
1232 JLOG(j.fatal())
1233 << "Invariant failed: successful minting didn't increase "
1234 "the number of minted tokens.";
1235 return false;
1236 }
1237
1238 if (result != tesSUCCESS && beforeMintedTotal != afterMintedTotal)
1239 {
1240 JLOG(j.fatal()) << "Invariant failed: failed minting changed the "
1241 "number of minted tokens.";
1242 return false;
1243 }
1244
1246 {
1247 JLOG(j.fatal())
1248 << "Invariant failed: minting changed the number of "
1249 "burned tokens.";
1250 return false;
1251 }
1252 }
1253
1254 if (tx.getTxnType() == ttNFTOKEN_BURN)
1255 {
1256 if (result == tesSUCCESS)
1257 {
1259 {
1260 JLOG(j.fatal())
1261 << "Invariant failed: successful burning didn't increase "
1262 "the number of burned tokens.";
1263 return false;
1264 }
1265 }
1266
1267 if (result != tesSUCCESS && beforeBurnedTotal != afterBurnedTotal)
1268 {
1269 JLOG(j.fatal()) << "Invariant failed: failed burning changed the "
1270 "number of burned tokens.";
1271 return false;
1272 }
1273
1275 {
1276 JLOG(j.fatal())
1277 << "Invariant failed: burning changed the number of "
1278 "minted tokens.";
1279 return false;
1280 }
1281 }
1282
1283 return true;
1284}
1285
1286//------------------------------------------------------------------------------
1287
1288void
1290 bool,
1291 std::shared_ptr<SLE const> const& before,
1293{
1294 if (before && before->getType() == ltRIPPLE_STATE)
1296
1297 if (before && before->getType() == ltMPTOKEN)
1299}
1300
1301bool
1303 STTx const& tx,
1304 TER const result,
1305 XRPAmount const,
1306 ReadView const& view,
1307 beast::Journal const& j)
1308{
1309 if (tx.getTxnType() != ttCLAWBACK)
1310 return true;
1311
1312 if (result == tesSUCCESS)
1313 {
1314 if (trustlinesChanged > 1)
1315 {
1316 JLOG(j.fatal())
1317 << "Invariant failed: more than one trustline changed.";
1318 return false;
1319 }
1320
1321 if (mptokensChanged > 1)
1322 {
1323 JLOG(j.fatal())
1324 << "Invariant failed: more than one mptokens changed.";
1325 return false;
1326 }
1327
1328 if (trustlinesChanged == 1)
1329 {
1330 AccountID const issuer = tx.getAccountID(sfAccount);
1331 STAmount const& amount = tx.getFieldAmount(sfAmount);
1332 AccountID const& holder = amount.getIssuer();
1333 STAmount const holderBalance = accountHolds(
1334 view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j);
1335
1336 if (holderBalance.signum() < 0)
1337 {
1338 JLOG(j.fatal())
1339 << "Invariant failed: trustline balance is negative";
1340 return false;
1341 }
1342 }
1343 }
1344 else
1345 {
1346 if (trustlinesChanged != 0)
1347 {
1348 JLOG(j.fatal()) << "Invariant failed: some trustlines were changed "
1349 "despite failure of the transaction.";
1350 return false;
1351 }
1352
1353 if (mptokensChanged != 0)
1354 {
1355 JLOG(j.fatal()) << "Invariant failed: some mptokens were changed "
1356 "despite failure of the transaction.";
1357 return false;
1358 }
1359 }
1360
1361 return true;
1362}
1363
1364//------------------------------------------------------------------------------
1365
1366void
1368 bool isDelete,
1369 std::shared_ptr<SLE const> const& before,
1371{
1372 if (after && after->getType() == ltMPTOKEN_ISSUANCE)
1373 {
1374 if (isDelete)
1376 else if (!before)
1378 }
1379
1380 if (after && after->getType() == ltMPTOKEN)
1381 {
1382 if (isDelete)
1384 else if (!before)
1386 }
1387}
1388
1389bool
1391 STTx const& tx,
1392 TER const result,
1393 XRPAmount const _fee,
1394 ReadView const& _view,
1395 beast::Journal const& j)
1396{
1397 if (result == tesSUCCESS)
1398 {
1399 if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_CREATE ||
1400 tx.getTxnType() == ttVAULT_CREATE)
1401 {
1402 if (mptIssuancesCreated_ == 0)
1403 {
1404 JLOG(j.fatal()) << "Invariant failed: transaction "
1405 "succeeded without creating a MPT issuance";
1406 }
1407 else if (mptIssuancesDeleted_ != 0)
1408 {
1409 JLOG(j.fatal()) << "Invariant failed: transaction "
1410 "succeeded while removing MPT issuances";
1411 }
1412 else if (mptIssuancesCreated_ > 1)
1413 {
1414 JLOG(j.fatal()) << "Invariant failed: transaction "
1415 "succeeded but created multiple issuances";
1416 }
1417
1418 return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0;
1419 }
1420
1421 if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_DESTROY ||
1422 tx.getTxnType() == ttVAULT_DELETE)
1423 {
1424 if (mptIssuancesDeleted_ == 0)
1425 {
1426 JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
1427 "succeeded without removing a MPT issuance";
1428 }
1429 else if (mptIssuancesCreated_ > 0)
1430 {
1431 JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
1432 "succeeded while creating MPT issuances";
1433 }
1434 else if (mptIssuancesDeleted_ > 1)
1435 {
1436 JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
1437 "succeeded but deleted multiple issuances";
1438 }
1439
1440 return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1;
1441 }
1442
1443 if (tx.getTxnType() == ttMPTOKEN_AUTHORIZE ||
1444 tx.getTxnType() == ttVAULT_DEPOSIT)
1445 {
1446 bool const submittedByIssuer = tx.isFieldPresent(sfHolder);
1447
1448 if (mptIssuancesCreated_ > 0)
1449 {
1450 JLOG(j.fatal()) << "Invariant failed: MPT authorize "
1451 "succeeded but created MPT issuances";
1452 return false;
1453 }
1454 else if (mptIssuancesDeleted_ > 0)
1455 {
1456 JLOG(j.fatal()) << "Invariant failed: MPT authorize "
1457 "succeeded but deleted issuances";
1458 return false;
1459 }
1460 else if (
1461 submittedByIssuer &&
1463 {
1464 JLOG(j.fatal())
1465 << "Invariant failed: MPT authorize submitted by issuer "
1466 "succeeded but created/deleted mptokens";
1467 return false;
1468 }
1469 else if (
1470 !submittedByIssuer && (tx.getTxnType() != ttVAULT_DEPOSIT) &&
1472 {
1473 // if the holder submitted this tx, then a mptoken must be
1474 // either created or deleted.
1475 JLOG(j.fatal())
1476 << "Invariant failed: MPT authorize submitted by holder "
1477 "succeeded but created/deleted bad number of mptokens";
1478 return false;
1479 }
1480
1481 return true;
1482 }
1483
1484 if (tx.getTxnType() == ttMPTOKEN_ISSUANCE_SET)
1485 {
1486 if (mptIssuancesDeleted_ > 0)
1487 {
1488 JLOG(j.fatal()) << "Invariant failed: MPT issuance set "
1489 "succeeded while removing MPT issuances";
1490 }
1491 else if (mptIssuancesCreated_ > 0)
1492 {
1493 JLOG(j.fatal()) << "Invariant failed: MPT issuance set "
1494 "succeeded while creating MPT issuances";
1495 }
1496 else if (mptokensDeleted_ > 0)
1497 {
1498 JLOG(j.fatal()) << "Invariant failed: MPT issuance set "
1499 "succeeded while removing MPTokens";
1500 }
1501 else if (mptokensCreated_ > 0)
1502 {
1503 JLOG(j.fatal()) << "Invariant failed: MPT issuance set "
1504 "succeeded while creating MPTokens";
1505 }
1506
1507 return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 &&
1509 }
1510
1511 if (tx.getTxnType() == ttESCROW_FINISH)
1512 return true;
1513
1514 if ((tx.getTxnType() == ttVAULT_CLAWBACK ||
1515 tx.getTxnType() == ttVAULT_WITHDRAW) &&
1516 mptokensDeleted_ == 1 && mptokensCreated_ == 0 &&
1518 return true;
1519 }
1520
1521 if (mptIssuancesCreated_ != 0)
1522 {
1523 JLOG(j.fatal()) << "Invariant failed: a MPT issuance was created";
1524 }
1525 else if (mptIssuancesDeleted_ != 0)
1526 {
1527 JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted";
1528 }
1529 else if (mptokensCreated_ != 0)
1530 {
1531 JLOG(j.fatal()) << "Invariant failed: a MPToken was created";
1532 }
1533 else if (mptokensDeleted_ != 0)
1534 {
1535 JLOG(j.fatal()) << "Invariant failed: a MPToken was deleted";
1536 }
1537
1538 return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 &&
1540}
1541
1542//------------------------------------------------------------------------------
1543
1544void
1546 bool,
1547 std::shared_ptr<SLE const> const& before,
1549{
1550 if (before && before->getType() != ltPERMISSIONED_DOMAIN)
1551 return;
1552 if (after && after->getType() != ltPERMISSIONED_DOMAIN)
1553 return;
1554
1555 auto check = [](SleStatus& sleStatus,
1556 std::shared_ptr<SLE const> const& sle) {
1557 auto const& credentials = sle->getFieldArray(sfAcceptedCredentials);
1558 sleStatus.credentialsSize_ = credentials.size();
1559 auto const sorted = credentials::makeSorted(credentials);
1560 sleStatus.isUnique_ = !sorted.empty();
1561
1562 // If array have duplicates then all the other checks are invalid
1563 sleStatus.isSorted_ = false;
1564
1565 if (sleStatus.isUnique_)
1566 {
1567 unsigned i = 0;
1568 for (auto const& cred : sorted)
1569 {
1570 auto const& credTx = credentials[i++];
1571 sleStatus.isSorted_ = (cred.first == credTx[sfIssuer]) &&
1572 (cred.second == credTx[sfCredentialType]);
1573 if (!sleStatus.isSorted_)
1574 break;
1575 }
1576 }
1577 };
1578
1579 if (before)
1580 {
1581 sleStatus_[0] = SleStatus();
1582 check(*sleStatus_[0], after);
1583 }
1584
1585 if (after)
1586 {
1587 sleStatus_[1] = SleStatus();
1588 check(*sleStatus_[1], after);
1589 }
1590}
1591
1592bool
1594 STTx const& tx,
1595 TER const result,
1596 XRPAmount const,
1597 ReadView const& view,
1598 beast::Journal const& j)
1599{
1600 if (tx.getTxnType() != ttPERMISSIONED_DOMAIN_SET || result != tesSUCCESS)
1601 return true;
1602
1603 auto check = [](SleStatus const& sleStatus, beast::Journal const& j) {
1604 if (!sleStatus.credentialsSize_)
1605 {
1606 JLOG(j.fatal()) << "Invariant failed: permissioned domain with "
1607 "no rules.";
1608 return false;
1609 }
1610
1611 if (sleStatus.credentialsSize_ >
1613 {
1614 JLOG(j.fatal()) << "Invariant failed: permissioned domain bad "
1615 "credentials size "
1616 << sleStatus.credentialsSize_;
1617 return false;
1618 }
1619
1620 if (!sleStatus.isUnique_)
1621 {
1622 JLOG(j.fatal())
1623 << "Invariant failed: permissioned domain credentials "
1624 "aren't unique";
1625 return false;
1626 }
1627
1628 if (!sleStatus.isSorted_)
1629 {
1630 JLOG(j.fatal())
1631 << "Invariant failed: permissioned domain credentials "
1632 "aren't sorted";
1633 return false;
1634 }
1635
1636 return true;
1637 };
1638
1639 return (sleStatus_[0] ? check(*sleStatus_[0], j) : true) &&
1640 (sleStatus_[1] ? check(*sleStatus_[1], j) : true);
1641}
1642
1643void
1645 bool,
1646 std::shared_ptr<SLE const> const& before,
1648{
1649 if (after && after->getType() == ltDIR_NODE)
1650 {
1651 if (after->isFieldPresent(sfDomainID))
1652 domains_.insert(after->getFieldH256(sfDomainID));
1653 }
1654
1655 if (after && after->getType() == ltOFFER)
1656 {
1657 if (after->isFieldPresent(sfDomainID))
1658 domains_.insert(after->getFieldH256(sfDomainID));
1659 else
1660 regularOffers_ = true;
1661
1662 // if a hybrid offer is missing domain or additional book, there's
1663 // something wrong
1664 if (after->isFlag(lsfHybrid) &&
1665 (!after->isFieldPresent(sfDomainID) ||
1666 !after->isFieldPresent(sfAdditionalBooks) ||
1667 after->getFieldArray(sfAdditionalBooks).size() > 1))
1668 badHybrids_ = true;
1669 }
1670}
1671
1672bool
1674 STTx const& tx,
1675 TER const result,
1676 XRPAmount const,
1677 ReadView const& view,
1678 beast::Journal const& j)
1679{
1680 auto const txType = tx.getTxnType();
1681 if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) ||
1682 result != tesSUCCESS)
1683 return true;
1684
1685 // For each offercreate transaction, check if
1686 // permissioned offers are valid
1687 if (txType == ttOFFER_CREATE && badHybrids_)
1688 {
1689 JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed";
1690 return false;
1691 }
1692
1693 if (!tx.isFieldPresent(sfDomainID))
1694 return true;
1695
1696 auto const domain = tx.getFieldH256(sfDomainID);
1697
1698 if (!view.exists(keylet::permissionedDomain(domain)))
1699 {
1700 JLOG(j.fatal()) << "Invariant failed: domain doesn't exist";
1701 return false;
1702 }
1703
1704 // for both payment and offercreate, there shouldn't be another domain
1705 // that's different from the domain specified
1706 for (auto const& d : domains_)
1707 {
1708 if (d != domain)
1709 {
1710 JLOG(j.fatal()) << "Invariant failed: transaction"
1711 " consumed wrong domains";
1712 return false;
1713 }
1714 }
1715
1716 if (regularOffers_)
1717 {
1718 JLOG(j.fatal()) << "Invariant failed: domain transaction"
1719 " affected regular offers";
1720 return false;
1721 }
1722
1723 return true;
1724}
1725
1726void
1728 bool isDelete,
1729 std::shared_ptr<SLE const> const& before,
1731{
1732 if (isDelete)
1733 return;
1734
1735 if (after)
1736 {
1737 auto const type = after->getType();
1738 // AMM object changed
1739 if (type == ltAMM)
1740 {
1741 ammAccount_ = after->getAccountID(sfAccount);
1742 lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance);
1743 }
1744 // AMM pool changed
1745 else if (
1746 (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) ||
1747 (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)))
1748 {
1749 ammPoolChanged_ = true;
1750 }
1751 }
1752
1753 if (before)
1754 {
1755 // AMM object changed
1756 if (before->getType() == ltAMM)
1757 {
1758 lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance);
1759 }
1760 }
1761}
1762
1763static bool
1765 STAmount const& amount,
1766 STAmount const& amount2,
1767 STAmount const& lptAMMBalance,
1768 ValidAMM::ZeroAllowed zeroAllowed)
1769{
1770 bool const positive = amount > beast::zero && amount2 > beast::zero &&
1771 lptAMMBalance > beast::zero;
1772 if (zeroAllowed == ValidAMM::ZeroAllowed::Yes)
1773 return positive ||
1774 (amount == beast::zero && amount2 == beast::zero &&
1775 lptAMMBalance == beast::zero);
1776 return positive;
1777}
1778
1779bool
1780ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const
1781{
1783 {
1784 // LPTokens and the pool can not change on vote
1785 // LCOV_EXCL_START
1786 JLOG(j.error()) << "AMMVote invariant failed: "
1789 << ammPoolChanged_;
1790 if (enforce)
1791 return false;
1792 // LCOV_EXCL_STOP
1793 }
1794
1795 return true;
1796}
1797
1798bool
1799ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const
1800{
1801 if (ammPoolChanged_)
1802 {
1803 // The pool can not change on bid
1804 // LCOV_EXCL_START
1805 JLOG(j.error()) << "AMMBid invariant failed: pool changed";
1806 if (enforce)
1807 return false;
1808 // LCOV_EXCL_STOP
1809 }
1810 // LPTokens are burnt, therefore there should be fewer LPTokens
1811 else if (
1814 *lptAMMBalanceAfter_ <= beast::zero))
1815 {
1816 // LCOV_EXCL_START
1817 JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_
1818 << " " << *lptAMMBalanceAfter_;
1819 if (enforce)
1820 return false;
1821 // LCOV_EXCL_STOP
1822 }
1823
1824 return true;
1825}
1826
1827bool
1829 STTx const& tx,
1830 ReadView const& view,
1831 bool enforce,
1832 beast::Journal const& j) const
1833{
1834 if (!ammAccount_)
1835 {
1836 // LCOV_EXCL_START
1837 JLOG(j.error())
1838 << "AMMCreate invariant failed: AMM object is not created";
1839 if (enforce)
1840 return false;
1841 // LCOV_EXCL_STOP
1842 }
1843 else
1844 {
1845 auto const [amount, amount2] = ammPoolHolds(
1846 view,
1847 *ammAccount_,
1848 tx[sfAmount].get<Issue>(),
1849 tx[sfAmount2].get<Issue>(),
1851 j);
1852 // Create invariant:
1853 // sqrt(amount * amount2) == LPTokens
1854 // all balances are greater than zero
1855 if (!validBalances(
1856 amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) ||
1857 ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) !=
1859 {
1860 JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " "
1861 << amount2 << " " << *lptAMMBalanceAfter_;
1862 if (enforce)
1863 return false;
1864 }
1865 }
1866
1867 return true;
1868}
1869
1870bool
1871ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const
1872{
1873 if (ammAccount_)
1874 {
1875 // LCOV_EXCL_START
1876 std::string const msg = (res == tesSUCCESS)
1877 ? "AMM object is not deleted on tesSUCCESS"
1878 : "AMM object is changed on tecINCOMPLETE";
1879 JLOG(j.error()) << "AMMDelete invariant failed: " << msg;
1880 if (enforce)
1881 return false;
1882 // LCOV_EXCL_STOP
1883 }
1884
1885 return true;
1886}
1887
1888bool
1889ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const
1890{
1891 if (ammAccount_)
1892 {
1893 // LCOV_EXCL_START
1894 JLOG(j.error()) << "AMM swap invariant failed: AMM object changed";
1895 if (enforce)
1896 return false;
1897 // LCOV_EXCL_STOP
1898 }
1899
1900 return true;
1901}
1902
1903bool
1905 ripple::STTx const& tx,
1906 ripple::ReadView const& view,
1907 ZeroAllowed zeroAllowed,
1908 beast::Journal const& j) const
1909{
1910 auto const [amount, amount2] = ammPoolHolds(
1911 view,
1912 *ammAccount_,
1913 tx[sfAsset].get<Issue>(),
1914 tx[sfAsset2].get<Issue>(),
1916 j);
1917 // Deposit and Withdrawal invariant:
1918 // sqrt(amount * amount2) >= LPTokens
1919 // all balances are greater than zero
1920 // unless on last withdrawal
1921 auto const poolProductMean = root2(amount * amount2);
1922 bool const nonNegativeBalances =
1923 validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed);
1924 bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_;
1925 // Allow for a small relative error if strongInvariantCheck fails
1926 auto weakInvariantCheck = [&]() {
1927 return *lptAMMBalanceAfter_ != beast::zero &&
1929 poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11});
1930 };
1931 if (!nonNegativeBalances ||
1932 (!strongInvariantCheck && !weakInvariantCheck()))
1933 {
1934 JLOG(j.error()) << "AMM " << tx.getTxnType() << " invariant failed: "
1936 << ammPoolChanged_ << " " << amount << " " << amount2
1937 << " " << poolProductMean << " "
1938 << lptAMMBalanceAfter_->getText() << " "
1939 << ((*lptAMMBalanceAfter_ == beast::zero)
1940 ? Number{1}
1941 : ((*lptAMMBalanceAfter_ - poolProductMean) /
1942 poolProductMean));
1943 return false;
1944 }
1945
1946 return true;
1947}
1948
1949bool
1951 ripple::STTx const& tx,
1952 ripple::ReadView const& view,
1953 bool enforce,
1954 beast::Journal const& j) const
1955{
1956 if (!ammAccount_)
1957 {
1958 // LCOV_EXCL_START
1959 JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted";
1960 if (enforce)
1961 return false;
1962 // LCOV_EXCL_STOP
1963 }
1964 else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce)
1965 return false;
1966
1967 return true;
1968}
1969
1970bool
1972 ripple::STTx const& tx,
1973 ripple::ReadView const& view,
1974 bool enforce,
1975 beast::Journal const& j) const
1976{
1977 if (!ammAccount_)
1978 {
1979 // Last Withdraw or Clawback deleted AMM
1980 }
1981 else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j))
1982 {
1983 if (enforce)
1984 return false;
1985 }
1986
1987 return true;
1988}
1989
1990bool
1992 STTx const& tx,
1993 TER const result,
1994 XRPAmount const,
1995 ReadView const& view,
1996 beast::Journal const& j)
1997{
1998 // Delete may return tecINCOMPLETE if there are too many
1999 // trustlines to delete.
2000 if (result != tesSUCCESS && result != tecINCOMPLETE)
2001 return true;
2002
2003 bool const enforce = view.rules().enabled(fixAMMv1_3);
2004
2005 switch (tx.getTxnType())
2006 {
2007 case ttAMM_CREATE:
2008 return finalizeCreate(tx, view, enforce, j);
2009 case ttAMM_DEPOSIT:
2010 return finalizeDeposit(tx, view, enforce, j);
2011 case ttAMM_CLAWBACK:
2012 case ttAMM_WITHDRAW:
2013 return finalizeWithdraw(tx, view, enforce, j);
2014 case ttAMM_BID:
2015 return finalizeBid(enforce, j);
2016 case ttAMM_VOTE:
2017 return finalizeVote(enforce, j);
2018 case ttAMM_DELETE:
2019 return finalizeDelete(enforce, result, j);
2020 case ttCHECK_CASH:
2021 case ttOFFER_CREATE:
2022 case ttPAYMENT:
2023 return finalizeDEX(enforce, j);
2024 default:
2025 break;
2026 }
2027
2028 return true;
2029}
2030
2031} // namespace ripple
A generic endpoint for log messages.
Definition Journal.h:60
Stream fatal() const
Definition Journal.h:352
Stream error() const
Definition Journal.h:346
Stream debug() const
Definition Journal.h:328
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
std::vector< std::shared_ptr< SLE const > > accountsDeleted_
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
A currency issued by an account.
Definition Issue.h:33
Item const * findByType(KeyType type) const
Retrieve a format based on its type.
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
static LedgerFormats const & getInstance()
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
A view into a ledger.
Definition ReadView.h:52
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
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.
virtual bool exists(Keylet const &k) const =0
Determine if a state item exists.
LedgerIndex seq() const
Returns the sequence number of the base ledger.
Definition ReadView.h:119
virtual Rules const & rules() const =0
Returns the tx processing rules.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:130
Currency const & getCurrency() const
Definition STAmount.h:502
XRPAmount xrp() const
Definition STAmount.cpp:306
int signum() const noexcept
Definition STAmount.h:514
AccountID const & getIssuer() const
Definition STAmount.h:508
bool native() const noexcept
Definition STAmount.h:458
STAmount zeroed() const
Returns a zero value with the same issuer and currency.
Definition STAmount.h:520
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:651
STAmount const & getFieldAmount(SField const &field) const
Definition STObject.cpp:665
uint256 getHash(HashPrefix prefix) const
Definition STObject.cpp:395
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:484
uint256 getFieldH256(SField const &field) const
Definition STObject.cpp:645
TxType getTxnType() const
Definition STTx.h:207
uint256 getTransactionID() const
Definition STTx.h:219
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
std::map< AccountID, std::shared_ptr< SLE const > const > possibleIssuers_
bool isValidEntry(std::shared_ptr< SLE const > const &before, std::shared_ptr< SLE const > const &after)
void recordBalance(Issue const &issue, BalanceChange change)
std::shared_ptr< SLE const > findIssuer(AccountID const &issuerID, ReadView const &view)
bool validateIssuerChanges(std::shared_ptr< SLE const > const &issuer, IssuerChanges const &changes, STTx const &tx, beast::Journal const &j, bool enforce)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
STAmount calculateBalanceChange(std::shared_ptr< SLE const > const &before, std::shared_ptr< SLE const > const &after, bool isDelete)
void recordBalanceChanges(std::shared_ptr< SLE const > const &after, STAmount const &balanceChange)
bool validateFrozenState(BalanceChange const &change, bool high, STTx const &tx, beast::Journal const &j, bool enforce, bool globalFreeze)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
bool finalizeWithdraw(STTx const &, ReadView const &, bool enforce, beast::Journal const &) const
bool finalizeDEX(bool enforce, beast::Journal const &) const
std::optional< STAmount > lptAMMBalanceAfter_
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalizeBid(bool enforce, beast::Journal const &) const
std::optional< AccountID > ammAccount_
bool finalizeDelete(bool enforce, TER res, beast::Journal const &) const
bool finalizeCreate(STTx const &, ReadView const &, bool enforce, beast::Journal const &) const
bool finalizeVote(bool enforce, beast::Journal const &) const
bool finalizeDeposit(STTx const &, ReadView const &, bool enforce, beast::Journal const &) const
bool generalInvariant(STTx const &, ReadView const &, ZeroAllowed zeroAllowed, beast::Journal const &) const
std::optional< STAmount > lptAMMBalanceBefore_
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
std::uint32_t trustlinesChanged
std::uint32_t mptokensChanged
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
std::uint32_t mptIssuancesCreated_
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
std::uint32_t mptIssuancesDeleted_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
hash_set< uint256 > domains_
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
std::optional< SleStatus > sleStatus_[2]
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:177
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
base_uint next() const
Definition base_uint.h:455
T invoke(T... args)
std::set< std::pair< AccountID, Slice > > makeSorted(STArray const &credentials)
Keylet permissionedDomain(AccountID const &account, std::uint32_t seq) noexcept
Definition Indexes.cpp:570
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:446
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:184
Keylet nftpage_min(AccountID const &owner)
NFT page keylets.
Definition Indexes.cpp:403
Keylet nftpage_max(AccountID const &owner)
A keylet for the owner's last possible NFT page.
Definition Indexes.cpp:411
bool compareTokens(uint256 const &a, uint256 const &b)
uint256 constexpr pageMask(std::string_view("0000000000000000000000000000000000000000ffffffffffffffffffffffff"))
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:115
Currency const & badCurrency()
We deliberately disallow the currency that looks like "XRP" because too many people were using it ins...
@ fhIGNORE_FREEZE
Definition View.h:78
bool isXRP(AccountID const &c)
Definition AccountID.h:90
TxType
Transaction type identifiers.
Definition TxFormats.h:57
static bool validBalances(STAmount const &amount, STAmount const &amount2, STAmount const &lptAMMBalance, ValidAMM::ZeroAllowed zeroAllowed)
base_uint< 256 > uint256
Definition base_uint.h:558
std::size_t constexpr maxPermissionedDomainCredentialsArraySize
The maximum number of credentials can be passed in array for permissioned domain.
Definition Protocol.h:111
std::uint64_t constexpr maxMPTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:117
@ lsfHighDeepFreeze
@ lsfDefaultRipple
@ lsfDisableMaster
@ lsfGlobalFreeze
@ lsfLowDeepFreeze
constexpr XRPAmount INITIAL_XRP
Configure the native currency.
std::size_t constexpr dirMaxTokensPerPage
The maximum number of items in an NFT page.
Definition Protocol.h:63
std::array< keyletDesc< AccountID const & >, 6 > const directAccountKeylets
Definition Indexes.h:384
std::pair< STAmount, STAmount > ammPoolHolds(ReadView const &view, AccountID const &ammAccountID, Issue const &issue1, Issue const &issue2, FreezeHandling freezeHandling, beast::Journal const j)
Get AMM pool balances.
Definition AMMUtils.cpp:31
@ tecINCOMPLETE
Definition TER.h:335
@ tesSUCCESS
Definition TER.h:244
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j)
Definition View.cpp:385
STAmount ammLPTokens(STAmount const &asset1, STAmount const &asset2, Issue const &lptIssue)
Calculate LP Tokens given AMM pool reserves.
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:3179
@ transactionID
transaction plus signature to give transaction ID
bool withinRelativeDistance(Quality const &calcQuality, Quality const &reqQuality, Number const &dist)
Check if the relative distance between the qualities is within the requested distance.
Definition AMMHelpers.h:129
Number root2(Number f)
Definition Number.cpp:701
bool isPseudoAccount(std::shared_ptr< SLE const > sleAcct)
Definition View.cpp:1139
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:39
uint256 key
Definition Keylet.h:40
std::shared_ptr< SLE const > const line
std::vector< BalanceChange > receivers
std::vector< BalanceChange > senders
T to_string(T... args)
T value_or(T... args)