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