rippled
Loading...
Searching...
No Matches
InvariantCheck.cpp
1#include <xrpld/app/misc/AMMHelpers.h>
2#include <xrpld/app/misc/AMMUtils.h>
3#include <xrpld/app/tx/detail/InvariantCheck.h>
4#include <xrpld/app/tx/detail/NFTokenUtils.h>
5#include <xrpld/app/tx/detail/PermissionedDomainSet.h>
6
7#include <xrpl/basics/Log.h>
8#include <xrpl/beast/utility/instrumentation.h>
9#include <xrpl/ledger/CredentialHelpers.h>
10#include <xrpl/ledger/ReadView.h>
11#include <xrpl/ledger/View.h>
12#include <xrpl/protocol/Feature.h>
13#include <xrpl/protocol/Indexes.h>
14#include <xrpl/protocol/LedgerFormats.h>
15#include <xrpl/protocol/MPTIssue.h>
16#include <xrpl/protocol/SField.h>
17#include <xrpl/protocol/STArray.h>
18#include <xrpl/protocol/STNumber.h>
19#include <xrpl/protocol/SystemParameters.h>
20#include <xrpl/protocol/TER.h>
21#include <xrpl/protocol/TxFormats.h>
22#include <xrpl/protocol/Units.h>
23#include <xrpl/protocol/nftPageMask.h>
24
25#include <cstdint>
26#include <optional>
27
28namespace xrpl {
29
30/*
31assert(enforce)
32
33There are several asserts (or XRPL_ASSERTs) in this file that check a variable
34named `enforce` when an invariant fails. At first glance, those asserts may look
35incorrect, but they are not.
36
37Those asserts take advantage of two facts:
381. `asserts` are not (normally) executed in release builds.
392. Invariants should *never* fail, except in tests that specifically modify
40 the open ledger to break them.
41
42This makes `assert(enforce)` sort of a second-layer of invariant enforcement
43aimed at _developers_. It's designed to fire if a developer writes code that
44violates an invariant, and runs it in unit tests or a develop build that _does
45not have the relevant amendments enabled_. It's intentionally a pain in the neck
46so that bad code gets caught and fixed as early as possible.
47*/
48
50 noPriv =
51 0x0000, // The transaction can not do any of the enumerated operations
53 0x0001, // The transaction can create a new ACCOUNT_ROOT object.
54 createPseudoAcct = 0x0002, // The transaction can create a pseudo account,
55 // which implies createAcct
57 0x0004, // The transaction must delete an ACCOUNT_ROOT object
58 mayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT
59 // object, but does not have to
60 overrideFreeze = 0x0010, // The transaction can override some freeze rules
61 changeNFTCounts = 0x0020, // The transaction can mint or burn an NFT
63 0x0040, // The transaction can create a new MPT issuance
64 destroyMPTIssuance = 0x0080, // The transaction can destroy an MPT issuance
65 mustAuthorizeMPT = 0x0100, // The transaction MUST create or delete an MPT
66 // object (except by issuer)
67 mayAuthorizeMPT = 0x0200, // The transaction MAY create or delete an MPT
68 // object (except by issuer)
70 0x0400, // The transaction MAY delete an MPT object. May not create.
72 0x0800, // The transaction must modify, delete or create, a vault
74 0x1000, // The transaction MAY modify, delete or create, a vault
75};
76constexpr Privilege
78{
79 return safe_cast<Privilege>(
82}
83
84#pragma push_macro("TRANSACTION")
85#undef TRANSACTION
86
87#define TRANSACTION(tag, value, name, delegable, amendment, privileges, ...) \
88 case tag: { \
89 return (privileges) & priv; \
90 }
91
92bool
93hasPrivilege(STTx const& tx, Privilege priv)
94{
95 switch (tx.getTxnType())
96 {
97#include <xrpl/protocol/detail/transactions.macro>
98 // Deprecated types
99 default:
100 return false;
101 }
102};
103
104#undef TRANSACTION
105#pragma pop_macro("TRANSACTION")
106
107void
109 bool,
112{
113 // nothing to do
114}
115
116bool
118 STTx const& tx,
119 TER const,
120 XRPAmount const fee,
121 ReadView const&,
122 beast::Journal const& j)
123{
124 // We should never charge a negative fee
125 if (fee.drops() < 0)
126 {
127 JLOG(j.fatal()) << "Invariant failed: fee paid was negative: "
128 << fee.drops();
129 return false;
130 }
131
132 // We should never charge a fee that's greater than or equal to the
133 // entire XRP supply.
134 if (fee >= INITIAL_XRP)
135 {
136 JLOG(j.fatal()) << "Invariant failed: fee paid exceeds system limit: "
137 << fee.drops();
138 return false;
139 }
140
141 // We should never charge more for a transaction than the transaction
142 // authorizes. It's possible to charge less in some circumstances.
143 if (fee > tx.getFieldAmount(sfFee).xrp())
144 {
145 JLOG(j.fatal()) << "Invariant failed: fee paid is " << fee.drops()
146 << " exceeds fee specified in transaction.";
147 return false;
148 }
149
150 return true;
151}
152
153//------------------------------------------------------------------------------
154
155void
157 bool isDelete,
158 std::shared_ptr<SLE const> const& before,
160{
161 /* We go through all modified ledger entries, looking only at account roots,
162 * escrow payments, and payment channels. We remove from the total any
163 * previous XRP values and add to the total any new XRP values. The net
164 * balance of a payment channel is computed from two fields (amount and
165 * balance) and deletions are ignored for paychan and escrow because the
166 * amount fields have not been adjusted for those in the case of deletion.
167 */
168 if (before)
169 {
170 switch (before->getType())
171 {
172 case ltACCOUNT_ROOT:
173 drops_ -= (*before)[sfBalance].xrp().drops();
174 break;
175 case ltPAYCHAN:
176 drops_ -=
177 ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
178 break;
179 case ltESCROW:
180 if (isXRP((*before)[sfAmount]))
181 drops_ -= (*before)[sfAmount].xrp().drops();
182 break;
183 default:
184 break;
185 }
186 }
187
188 if (after)
189 {
190 switch (after->getType())
191 {
192 case ltACCOUNT_ROOT:
193 drops_ += (*after)[sfBalance].xrp().drops();
194 break;
195 case ltPAYCHAN:
196 if (!isDelete)
197 drops_ += ((*after)[sfAmount] - (*after)[sfBalance])
198 .xrp()
199 .drops();
200 break;
201 case ltESCROW:
202 if (!isDelete && isXRP((*after)[sfAmount]))
203 drops_ += (*after)[sfAmount].xrp().drops();
204 break;
205 default:
206 break;
207 }
208 }
209}
210
211bool
213 STTx const& tx,
214 TER const,
215 XRPAmount const fee,
216 ReadView const&,
217 beast::Journal const& j)
218{
219 // The net change should never be positive, as this would mean that the
220 // transaction created XRP out of thin air. That's not possible.
221 if (drops_ > 0)
222 {
223 JLOG(j.fatal()) << "Invariant failed: XRP net change was positive: "
224 << drops_;
225 return false;
226 }
227
228 // The negative of the net change should be equal to actual fee charged.
229 if (-drops_ != fee.drops())
230 {
231 JLOG(j.fatal()) << "Invariant failed: XRP net change of " << drops_
232 << " doesn't match fee " << fee.drops();
233 return false;
234 }
235
236 return true;
237}
238
239//------------------------------------------------------------------------------
240
241void
243 bool,
244 std::shared_ptr<SLE const> const& before,
246{
247 auto isBad = [](STAmount const& balance) {
248 if (!balance.native())
249 return true;
250
251 auto const drops = balance.xrp();
252
253 // Can't have more than the number of drops instantiated
254 // in the genesis ledger.
255 if (drops > INITIAL_XRP)
256 return true;
257
258 // Can't have a negative balance (0 is OK)
259 if (drops < XRPAmount{0})
260 return true;
261
262 return false;
263 };
264
265 if (before && before->getType() == ltACCOUNT_ROOT)
266 bad_ |= isBad((*before)[sfBalance]);
267
268 if (after && after->getType() == ltACCOUNT_ROOT)
269 bad_ |= isBad((*after)[sfBalance]);
270}
271
272bool
274 STTx const&,
275 TER const,
276 XRPAmount const,
277 ReadView const&,
278 beast::Journal const& j)
279{
280 if (bad_)
281 {
282 JLOG(j.fatal()) << "Invariant failed: incorrect account XRP balance";
283 return false;
284 }
285
286 return true;
287}
288
289//------------------------------------------------------------------------------
290
291void
293 bool isDelete,
294 std::shared_ptr<SLE const> const& before,
296{
297 auto isBad = [](STAmount const& pays, STAmount const& gets) {
298 // An offer should never be negative
299 if (pays < beast::zero)
300 return true;
301
302 if (gets < beast::zero)
303 return true;
304
305 // Can't have an XRP to XRP offer:
306 return pays.native() && gets.native();
307 };
308
309 if (before && before->getType() == ltOFFER)
310 bad_ |= isBad((*before)[sfTakerPays], (*before)[sfTakerGets]);
311
312 if (after && after->getType() == ltOFFER)
313 bad_ |= isBad((*after)[sfTakerPays], (*after)[sfTakerGets]);
314}
315
316bool
318 STTx const&,
319 TER const,
320 XRPAmount const,
321 ReadView const&,
322 beast::Journal const& j)
323{
324 if (bad_)
325 {
326 JLOG(j.fatal()) << "Invariant failed: offer with a bad amount";
327 return false;
328 }
329
330 return true;
331}
332
333//------------------------------------------------------------------------------
334
335void
337 bool isDelete,
338 std::shared_ptr<SLE const> const& before,
340{
341 auto isBad = [](STAmount const& amount) {
342 // XRP case
343 if (amount.native())
344 {
345 if (amount.xrp() <= XRPAmount{0})
346 return true;
347
348 if (amount.xrp() >= INITIAL_XRP)
349 return true;
350 }
351 else
352 {
353 // IOU case
354 if (amount.holds<Issue>())
355 {
356 if (amount <= beast::zero)
357 return true;
358
359 if (badCurrency() == amount.getCurrency())
360 return true;
361 }
362
363 // MPT case
364 if (amount.holds<MPTIssue>())
365 {
366 if (amount <= beast::zero)
367 return true;
368
369 if (amount.mpt() > MPTAmount{maxMPTokenAmount})
370 return true; // LCOV_EXCL_LINE
371 }
372 }
373 return false;
374 };
375
376 if (before && before->getType() == ltESCROW)
377 bad_ |= isBad((*before)[sfAmount]);
378
379 if (after && after->getType() == ltESCROW)
380 bad_ |= isBad((*after)[sfAmount]);
381
382 auto checkAmount = [this](std::int64_t amount) {
383 if (amount > maxMPTokenAmount || amount < 0)
384 bad_ = true;
385 };
386
387 if (after && after->getType() == ltMPTOKEN_ISSUANCE)
388 {
389 auto const outstanding = (*after)[sfOutstandingAmount];
390 checkAmount(outstanding);
391 if (auto const locked = (*after)[~sfLockedAmount])
392 {
393 checkAmount(*locked);
394 bad_ = outstanding < *locked;
395 }
396 }
397
398 if (after && after->getType() == ltMPTOKEN)
399 {
400 auto const mptAmount = (*after)[sfMPTAmount];
401 checkAmount(mptAmount);
402 if (auto const locked = (*after)[~sfLockedAmount])
403 {
404 checkAmount(*locked);
405 }
406 }
407}
408
409bool
411 STTx const& txn,
412 TER const,
413 XRPAmount const,
414 ReadView const& rv,
415 beast::Journal const& j)
416{
417 if (bad_)
418 {
419 JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount";
420 return false;
421 }
422
423 return true;
424}
425
426//------------------------------------------------------------------------------
427
428void
430 bool isDelete,
431 std::shared_ptr<SLE const> const& before,
433{
434 if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
436}
437
438bool
440 STTx const& tx,
441 TER const result,
442 XRPAmount const,
443 ReadView const&,
444 beast::Journal const& j)
445{
446 // AMM account root can be deleted as the result of AMM withdraw/delete
447 // transaction when the total AMM LP Tokens balance goes to 0.
448 // A successful AccountDelete or AMMDelete MUST delete exactly
449 // one account root.
450 if (hasPrivilege(tx, mustDeleteAcct) && result == tesSUCCESS)
451 {
452 if (accountsDeleted_ == 1)
453 return true;
454
455 if (accountsDeleted_ == 0)
456 JLOG(j.fatal()) << "Invariant failed: account deletion "
457 "succeeded without deleting an account";
458 else
459 JLOG(j.fatal()) << "Invariant failed: account deletion "
460 "succeeded but deleted multiple accounts!";
461 return false;
462 }
463
464 // A successful AMMWithdraw/AMMClawback MAY delete one account root
465 // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw
466 // deletes the AMM account, accountsDeleted_ is set if it is deleted.
467 if (hasPrivilege(tx, mayDeleteAcct) && result == tesSUCCESS &&
468 accountsDeleted_ == 1)
469 return true;
470
471 if (accountsDeleted_ == 0)
472 return true;
473
474 JLOG(j.fatal()) << "Invariant failed: an account root was deleted";
475 return false;
476}
477
478//------------------------------------------------------------------------------
479
480void
482 bool isDelete,
483 std::shared_ptr<SLE const> const& before,
485{
486 if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
487 accountsDeleted_.emplace_back(before, after);
488}
489
490bool
492 STTx const& tx,
493 TER const result,
494 XRPAmount const,
495 ReadView const& view,
496 beast::Journal const& j)
497{
498 // Always check for objects in the ledger, but to prevent differing
499 // transaction processing results, however unlikely, only fail if the
500 // feature is enabled. Enabled, or not, though, a fatal-level message will
501 // be logged
502 [[maybe_unused]] bool const enforce =
503 view.rules().enabled(featureInvariantsV1_1) ||
504 view.rules().enabled(featureSingleAssetVault) ||
505 view.rules().enabled(featureLendingProtocol);
506
507 auto const objectExists = [&view, enforce, &j](auto const& keylet) {
508 (void)enforce;
509 if (auto const sle = view.read(keylet))
510 {
511 // Finding the object is bad
512 auto const typeName = [&sle]() {
513 auto item =
514 LedgerFormats::getInstance().findByType(sle->getType());
515
516 if (item != nullptr)
517 return item->getName();
518 return std::to_string(sle->getType());
519 }();
520
521 JLOG(j.fatal())
522 << "Invariant failed: account deletion left behind a "
523 << typeName << " object";
524 // The comment above starting with "assert(enforce)" explains this
525 // assert.
526 XRPL_ASSERT(
527 enforce,
528 "xrpl::AccountRootsDeletedClean::finalize::objectExists : "
529 "account deletion left no objects behind");
530 return true;
531 }
532 return false;
533 };
534
535 for (auto const& [before, after] : accountsDeleted_)
536 {
537 auto const accountID = before->getAccountID(sfAccount);
538 // An account should not be deleted with a balance
539 if (after->at(sfBalance) != beast::zero)
540 {
541 JLOG(j.fatal()) << "Invariant failed: account deletion left "
542 "behind a non-zero balance";
543 XRPL_ASSERT(
544 enforce,
545 "xrpl::AccountRootsDeletedClean::finalize : "
546 "deleted account has zero balance");
547 if (enforce)
548 return false;
549 }
550 // An account should not be deleted with a non-zero owner count
551 if (after->at(sfOwnerCount) != 0)
552 {
553 JLOG(j.fatal()) << "Invariant failed: account deletion left "
554 "behind a non-zero owner count";
555 XRPL_ASSERT(
556 enforce,
557 "xrpl::AccountRootsDeletedClean::finalize : "
558 "deleted account has zero owner count");
559 if (enforce)
560 return false;
561 }
562 // Simple types
563 for (auto const& [keyletfunc, _, __] : directAccountKeylets)
564 {
565 if (objectExists(std::invoke(keyletfunc, accountID)) && enforce)
566 return false;
567 }
568
569 {
570 // NFT pages. nftpage_min and nftpage_max were already explicitly
571 // checked above as entries in directAccountKeylets. This uses
572 // view.succ() to check for any NFT pages in between the two
573 // endpoints.
574 Keylet const first = keylet::nftpage_min(accountID);
575 Keylet const last = keylet::nftpage_max(accountID);
576
577 std::optional<uint256> key = view.succ(first.key, last.key.next());
578
579 // current page
580 if (key && objectExists(Keylet{ltNFTOKEN_PAGE, *key}) && enforce)
581 return false;
582 }
583
584 // If the account is a pseudo account, then the linked object must
585 // also be deleted. e.g. AMM, Vault, etc.
586 for (auto const& field : getPseudoAccountFields())
587 {
588 if (before->isFieldPresent(*field))
589 {
590 auto const key = before->getFieldH256(*field);
591 if (objectExists(keylet::unchecked(key)) && enforce)
592 return false;
593 }
594 }
595 }
596
597 return true;
598}
599
600//------------------------------------------------------------------------------
601
602void
604 bool,
605 std::shared_ptr<SLE const> const& before,
607{
608 if (before && after && before->getType() != after->getType())
609 typeMismatch_ = true;
610
611 if (after)
612 {
613#pragma push_macro("LEDGER_ENTRY")
614#undef LEDGER_ENTRY
615
616#define LEDGER_ENTRY(tag, ...) case tag:
617
618 switch (after->getType())
619 {
620#include <xrpl/protocol/detail/ledger_entries.macro>
621
622 break;
623 default:
624 invalidTypeAdded_ = true;
625 break;
626 }
627
628#undef LEDGER_ENTRY
629#pragma pop_macro("LEDGER_ENTRY")
630 }
631}
632
633bool
635 STTx const&,
636 TER const,
637 XRPAmount const,
638 ReadView const&,
639 beast::Journal const& j)
640{
641 if ((!typeMismatch_) && (!invalidTypeAdded_))
642 return true;
643
644 if (typeMismatch_)
645 {
646 JLOG(j.fatal()) << "Invariant failed: ledger entry type mismatch";
647 }
648
650 {
651 JLOG(j.fatal()) << "Invariant failed: invalid ledger entry type added";
652 }
653
654 return false;
655}
656
657//------------------------------------------------------------------------------
658
659void
661 bool,
664{
665 if (after && after->getType() == ltRIPPLE_STATE)
666 {
667 // checking the issue directly here instead of
668 // relying on .native() just in case native somehow
669 // were systematically incorrect
671 after->getFieldAmount(sfLowLimit).issue() == xrpIssue() ||
672 after->getFieldAmount(sfHighLimit).issue() == xrpIssue();
673 }
674}
675
676bool
678 STTx const&,
679 TER const,
680 XRPAmount const,
681 ReadView const&,
682 beast::Journal const& j)
683{
684 if (!xrpTrustLine_)
685 return true;
686
687 JLOG(j.fatal()) << "Invariant failed: an XRP trust line was created";
688 return false;
689}
690
691//------------------------------------------------------------------------------
692
693void
695 bool,
698{
699 if (after && after->getType() == ltRIPPLE_STATE)
700 {
701 std::uint32_t const uFlags = after->getFieldU32(sfFlags);
702 bool const lowFreeze = uFlags & lsfLowFreeze;
703 bool const lowDeepFreeze = uFlags & lsfLowDeepFreeze;
704
705 bool const highFreeze = uFlags & lsfHighFreeze;
706 bool const highDeepFreeze = uFlags & lsfHighDeepFreeze;
707
709 (lowDeepFreeze && !lowFreeze) || (highDeepFreeze && !highFreeze);
710 }
711}
712
713bool
715 STTx const&,
716 TER const,
717 XRPAmount const,
718 ReadView const&,
719 beast::Journal const& j)
720{
722 return true;
723
724 JLOG(j.fatal()) << "Invariant failed: a trust line with deep freeze flag "
725 "without normal freeze was created";
726 return false;
727}
728
729//------------------------------------------------------------------------------
730
731void
733 bool isDelete,
734 std::shared_ptr<SLE const> const& before,
736{
737 /*
738 * A trust line freeze state alone doesn't determine if a transfer is
739 * frozen. The transfer must be examined "end-to-end" because both sides of
740 * the transfer may have different freeze states and freeze impact depends
741 * on the transfer direction. This is why first we need to track the
742 * transfers using IssuerChanges senders/receivers.
743 *
744 * Only in validateIssuerChanges, after we collected all changes can we
745 * determine if the transfer is valid.
746 */
747 if (!isValidEntry(before, after))
748 {
749 return;
750 }
751
752 auto const balanceChange = calculateBalanceChange(before, after, isDelete);
753 if (balanceChange.signum() == 0)
754 {
755 return;
756 }
757
758 recordBalanceChanges(after, balanceChange);
759}
760
761bool
763 STTx const& tx,
764 TER const ter,
765 XRPAmount const fee,
766 ReadView const& view,
767 beast::Journal const& j)
768{
769 /*
770 * We check this invariant regardless of deep freeze amendment status,
771 * allowing for detection and logging of potential issues even when the
772 * amendment is disabled.
773 *
774 * If an exploit that allows moving frozen assets is discovered,
775 * we can alert operators who monitor fatal messages and trigger assert in
776 * debug builds for an early warning.
777 *
778 * In an unlikely event that an exploit is found, this early detection
779 * enables encouraging the UNL to expedite deep freeze amendment activation
780 * or deploy hotfixes via new amendments. In case of a new amendment, we'd
781 * only have to change this line setting 'enforce' variable.
782 * enforce = view.rules().enabled(featureDeepFreeze) ||
783 * view.rules().enabled(fixFreezeExploit);
784 */
785 [[maybe_unused]] bool const enforce =
786 view.rules().enabled(featureDeepFreeze);
787
788 for (auto const& [issue, changes] : balanceChanges_)
789 {
790 auto const issuerSle = findIssuer(issue.account, view);
791 // It should be impossible for the issuer to not be found, but check
792 // just in case so rippled doesn't crash in release.
793 if (!issuerSle)
794 {
795 // The comment above starting with "assert(enforce)" explains this
796 // assert.
797 XRPL_ASSERT(
798 enforce,
799 "xrpl::TransfersNotFrozen::finalize : enforce "
800 "invariant.");
801 if (enforce)
802 {
803 return false;
804 }
805 continue;
806 }
807
808 if (!validateIssuerChanges(issuerSle, changes, tx, j, enforce))
809 {
810 return false;
811 }
812 }
813
814 return true;
815}
816
817bool
819 std::shared_ptr<SLE const> const& before,
821{
822 // `after` can never be null, even if the trust line is deleted.
823 XRPL_ASSERT(after, "xrpl::TransfersNotFrozen::isValidEntry : valid after.");
824 if (!after)
825 {
826 return false;
827 }
828
829 if (after->getType() == ltACCOUNT_ROOT)
830 {
831 possibleIssuers_.emplace(after->at(sfAccount), after);
832 return false;
833 }
834
835 /* While LedgerEntryTypesMatch invariant also checks types, all invariants
836 * are processed regardless of previous failures.
837 *
838 * This type check is still necessary here because it prevents potential
839 * issues in subsequent processing.
840 */
841 return after->getType() == ltRIPPLE_STATE &&
842 (!before || before->getType() == ltRIPPLE_STATE);
843}
844
847 std::shared_ptr<SLE const> const& before,
849 bool isDelete)
850{
851 auto const getBalance = [](auto const& line, auto const& other, bool zero) {
852 STAmount amt =
853 line ? line->at(sfBalance) : other->at(sfBalance).zeroed();
854 return zero ? amt.zeroed() : amt;
855 };
856
857 /* Trust lines can be created dynamically by other transactions such as
858 * Payment and OfferCreate that cross offers. Such trust line won't be
859 * created frozen, but the sender might be, so the starting balance must be
860 * treated as zero.
861 */
862 auto const balanceBefore = getBalance(before, after, false);
863
864 /* Same as above, trust lines can be dynamically deleted, and for frozen
865 * trust lines, payments not involving the issuer must be blocked. This is
866 * achieved by treating the final balance as zero when isDelete=true to
867 * ensure frozen line restrictions are enforced even during deletion.
868 */
869 auto const balanceAfter = getBalance(after, before, isDelete);
870
871 return balanceAfter - balanceBefore;
872}
873
874void
876{
877 XRPL_ASSERT(
878 change.balanceChangeSign,
879 "xrpl::TransfersNotFrozen::recordBalance : valid trustline "
880 "balance sign.");
881 auto& changes = balanceChanges_[issue];
882 if (change.balanceChangeSign < 0)
883 changes.senders.emplace_back(std::move(change));
884 else
885 changes.receivers.emplace_back(std::move(change));
886}
887
888void
891 STAmount const& balanceChange)
892{
893 auto const balanceChangeSign = balanceChange.signum();
894 auto const currency = after->at(sfBalance).getCurrency();
895
896 // Change from low account's perspective, which is trust line default
898 {currency, after->at(sfHighLimit).getIssuer()},
899 {after, balanceChangeSign});
900
901 // Change from high account's perspective, which reverses the sign.
903 {currency, after->at(sfLowLimit).getIssuer()},
904 {after, -balanceChangeSign});
905}
906
909{
910 if (auto it = possibleIssuers_.find(issuerID); it != possibleIssuers_.end())
911 {
912 return it->second;
913 }
914
915 return view.read(keylet::account(issuerID));
916}
917
918bool
920 std::shared_ptr<SLE const> const& issuer,
921 IssuerChanges const& changes,
922 STTx const& tx,
923 beast::Journal const& j,
924 bool enforce)
925{
926 if (!issuer)
927 {
928 return false;
929 }
930
931 bool const globalFreeze = issuer->isFlag(lsfGlobalFreeze);
932 if (changes.receivers.empty() || changes.senders.empty())
933 {
934 /* If there are no receivers, then the holder(s) are returning
935 * their tokens to the issuer. Likewise, if there are no
936 * senders, then the issuer is issuing tokens to the holder(s).
937 * This is allowed regardless of the issuer's freeze flags. (The
938 * holder may have contradicting freeze flags, but that will be
939 * checked when the holder is treated as issuer.)
940 */
941 return true;
942 }
943
944 for (auto const& actors : {changes.senders, changes.receivers})
945 {
946 for (auto const& change : actors)
947 {
948 bool const high = change.line->at(sfLowLimit).getIssuer() ==
949 issuer->at(sfAccount);
950
952 change, high, tx, j, enforce, globalFreeze))
953 {
954 return false;
955 }
956 }
957 }
958 return true;
959}
960
961bool
963 BalanceChange const& change,
964 bool high,
965 STTx const& tx,
966 beast::Journal const& j,
967 bool enforce,
968 bool globalFreeze)
969{
970 bool const freeze = change.balanceChangeSign < 0 &&
971 change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze);
972 bool const deepFreeze =
973 change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze);
974 bool const frozen = globalFreeze || deepFreeze || freeze;
975
976 bool const isAMMLine = change.line->isFlag(lsfAMMNode);
977
978 if (!frozen)
979 {
980 return true;
981 }
982
983 // AMMClawbacks are allowed to override some freeze rules
984 if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, overrideFreeze))
985 {
986 JLOG(j.debug()) << "Invariant check allowing funds to be moved "
987 << (change.balanceChangeSign > 0 ? "to" : "from")
988 << " a frozen trustline for AMMClawback "
989 << tx.getTransactionID();
990 return true;
991 }
992
993 JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for "
994 << tx.getTransactionID();
995 // The comment above starting with "assert(enforce)" explains this assert.
996 XRPL_ASSERT(
997 enforce,
998 "xrpl::TransfersNotFrozen::validateFrozenState : enforce "
999 "invariant.");
1000
1001 if (enforce)
1002 {
1003 return false;
1004 }
1005
1006 return true;
1007}
1008
1009//------------------------------------------------------------------------------
1010
1011void
1013 bool,
1014 std::shared_ptr<SLE const> const& before,
1016{
1017 if (!before && after->getType() == ltACCOUNT_ROOT)
1018 {
1020 accountSeq_ = (*after)[sfSequence];
1022 flags_ = after->getFlags();
1023 }
1024}
1025
1026bool
1028 STTx const& tx,
1029 TER const result,
1030 XRPAmount const,
1031 ReadView const& view,
1032 beast::Journal const& j)
1033{
1034 if (accountsCreated_ == 0)
1035 return true;
1036
1037 if (accountsCreated_ > 1)
1038 {
1039 JLOG(j.fatal()) << "Invariant failed: multiple accounts "
1040 "created in a single transaction";
1041 return false;
1042 }
1043
1044 // From this point on we know exactly one account was created.
1045 if (hasPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS)
1046 {
1047 bool const pseudoAccount =
1048 (pseudoAccount_ &&
1049 (view.rules().enabled(featureSingleAssetVault) ||
1050 view.rules().enabled(featureLendingProtocol)));
1051
1052 if (pseudoAccount && !hasPrivilege(tx, createPseudoAcct))
1053 {
1054 JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a "
1055 "wrong transaction type";
1056 return false;
1057 }
1058
1059 std::uint32_t const startingSeq = pseudoAccount ? 0 : view.seq();
1060
1061 if (accountSeq_ != startingSeq)
1062 {
1063 JLOG(j.fatal()) << "Invariant failed: account created with "
1064 "wrong starting sequence number";
1065 return false;
1066 }
1067
1068 if (pseudoAccount)
1069 {
1070 std::uint32_t const expected =
1072 if (flags_ != expected)
1073 {
1074 JLOG(j.fatal())
1075 << "Invariant failed: pseudo-account created with "
1076 "wrong flags";
1077 return false;
1078 }
1079 }
1080
1081 return true;
1082 }
1083
1084 JLOG(j.fatal()) << "Invariant failed: account root created illegally";
1085 return false;
1086} // namespace xrpl
1087
1088//------------------------------------------------------------------------------
1089
1090void
1092 bool isDelete,
1093 std::shared_ptr<SLE const> const& before,
1095{
1096 static constexpr uint256 const& pageBits = nft::pageMask;
1097 static constexpr uint256 const accountBits = ~pageBits;
1098
1099 if ((before && before->getType() != ltNFTOKEN_PAGE) ||
1100 (after && after->getType() != ltNFTOKEN_PAGE))
1101 return;
1102
1103 auto check = [this, isDelete](std::shared_ptr<SLE const> const& sle) {
1104 uint256 const account = sle->key() & accountBits;
1105 uint256 const hiLimit = sle->key() & pageBits;
1106 std::optional<uint256> const prev = (*sle)[~sfPreviousPageMin];
1107
1108 // Make sure that any page links...
1109 // 1. Are properly associated with the owning account and
1110 // 2. The page is correctly ordered between links.
1111 if (prev)
1112 {
1113 if (account != (*prev & accountBits))
1114 badLink_ = true;
1115
1116 if (hiLimit <= (*prev & pageBits))
1117 badLink_ = true;
1118 }
1119
1120 if (auto const next = (*sle)[~sfNextPageMin])
1121 {
1122 if (account != (*next & accountBits))
1123 badLink_ = true;
1124
1125 if (hiLimit >= (*next & pageBits))
1126 badLink_ = true;
1127 }
1128
1129 {
1130 auto const& nftokens = sle->getFieldArray(sfNFTokens);
1131
1132 // An NFTokenPage should never contain too many tokens or be empty.
1133 if (std::size_t const nftokenCount = nftokens.size();
1134 (!isDelete && nftokenCount == 0) ||
1135 nftokenCount > dirMaxTokensPerPage)
1136 invalidSize_ = true;
1137
1138 // If prev is valid, use it to establish a lower bound for
1139 // page entries. If prev is not valid the lower bound is zero.
1140 uint256 const loLimit =
1141 prev ? *prev & pageBits : uint256(beast::zero);
1142
1143 // Also verify that all NFTokenIDs in the page are sorted.
1144 uint256 loCmp = loLimit;
1145 for (auto const& obj : nftokens)
1146 {
1147 uint256 const tokenID = obj[sfNFTokenID];
1148 if (!nft::compareTokens(loCmp, tokenID))
1149 badSort_ = true;
1150 loCmp = tokenID;
1151
1152 // None of the NFTs on this page should belong on lower or
1153 // higher pages.
1154 if (uint256 const tokenPageBits = tokenID & pageBits;
1155 tokenPageBits < loLimit || tokenPageBits >= hiLimit)
1156 badEntry_ = true;
1157
1158 if (auto uri = obj[~sfURI]; uri && uri->empty())
1159 badURI_ = true;
1160 }
1161 }
1162 };
1163
1164 if (before)
1165 {
1166 check(before);
1167
1168 // While an account's NFToken directory contains any NFTokens, the last
1169 // NFTokenPage (with 96 bits of 1 in the low part of the index) should
1170 // never be deleted.
1171 if (isDelete && (before->key() & nft::pageMask) == nft::pageMask &&
1172 before->isFieldPresent(sfPreviousPageMin))
1173 {
1174 deletedFinalPage_ = true;
1175 }
1176 }
1177
1178 if (after)
1179 check(after);
1180
1181 if (!isDelete && before && after)
1182 {
1183 // If the NFTokenPage
1184 // 1. Has a NextMinPage field in before, but loses it in after, and
1185 // 2. This is not the last page in the directory
1186 // Then we have identified a corruption in the links between the
1187 // NFToken pages in the NFToken directory.
1188 if ((before->key() & nft::pageMask) != nft::pageMask &&
1189 before->isFieldPresent(sfNextPageMin) &&
1190 !after->isFieldPresent(sfNextPageMin))
1191 {
1192 deletedLink_ = true;
1193 }
1194 }
1195}
1196
1197bool
1199 STTx const& tx,
1200 TER const result,
1201 XRPAmount const,
1202 ReadView const& view,
1203 beast::Journal const& j)
1204{
1205 if (badLink_)
1206 {
1207 JLOG(j.fatal()) << "Invariant failed: NFT page is improperly linked.";
1208 return false;
1209 }
1210
1211 if (badEntry_)
1212 {
1213 JLOG(j.fatal()) << "Invariant failed: NFT found in incorrect page.";
1214 return false;
1215 }
1216
1217 if (badSort_)
1218 {
1219 JLOG(j.fatal()) << "Invariant failed: NFTs on page are not sorted.";
1220 return false;
1221 }
1222
1223 if (badURI_)
1224 {
1225 JLOG(j.fatal()) << "Invariant failed: NFT contains empty URI.";
1226 return false;
1227 }
1228
1229 if (invalidSize_)
1230 {
1231 JLOG(j.fatal()) << "Invariant failed: NFT page has invalid size.";
1232 return false;
1233 }
1234
1235 if (view.rules().enabled(fixNFTokenPageLinks))
1236 {
1238 {
1239 JLOG(j.fatal()) << "Invariant failed: Last NFT page deleted with "
1240 "non-empty directory.";
1241 return false;
1242 }
1243 if (deletedLink_)
1244 {
1245 JLOG(j.fatal()) << "Invariant failed: Lost NextMinPage link.";
1246 return false;
1247 }
1248 }
1249
1250 return true;
1251}
1252
1253//------------------------------------------------------------------------------
1254void
1256 bool,
1257 std::shared_ptr<SLE const> const& before,
1259{
1260 if (before && before->getType() == ltACCOUNT_ROOT)
1261 {
1262 beforeMintedTotal += (*before)[~sfMintedNFTokens].value_or(0);
1263 beforeBurnedTotal += (*before)[~sfBurnedNFTokens].value_or(0);
1264 }
1265
1266 if (after && after->getType() == ltACCOUNT_ROOT)
1267 {
1268 afterMintedTotal += (*after)[~sfMintedNFTokens].value_or(0);
1269 afterBurnedTotal += (*after)[~sfBurnedNFTokens].value_or(0);
1270 }
1271}
1272
1273bool
1275 STTx const& tx,
1276 TER const result,
1277 XRPAmount const,
1278 ReadView const& view,
1279 beast::Journal const& j)
1280{
1281 if (!hasPrivilege(tx, changeNFTCounts))
1282 {
1284 {
1285 JLOG(j.fatal()) << "Invariant failed: the number of minted tokens "
1286 "changed without a mint transaction!";
1287 return false;
1288 }
1289
1291 {
1292 JLOG(j.fatal()) << "Invariant failed: the number of burned tokens "
1293 "changed without a burn transaction!";
1294 return false;
1295 }
1296
1297 return true;
1298 }
1299
1300 if (tx.getTxnType() == ttNFTOKEN_MINT)
1301 {
1302 if (result == tesSUCCESS && beforeMintedTotal >= afterMintedTotal)
1303 {
1304 JLOG(j.fatal())
1305 << "Invariant failed: successful minting didn't increase "
1306 "the number of minted tokens.";
1307 return false;
1308 }
1309
1310 if (result != tesSUCCESS && beforeMintedTotal != afterMintedTotal)
1311 {
1312 JLOG(j.fatal()) << "Invariant failed: failed minting changed the "
1313 "number of minted tokens.";
1314 return false;
1315 }
1316
1318 {
1319 JLOG(j.fatal())
1320 << "Invariant failed: minting changed the number of "
1321 "burned tokens.";
1322 return false;
1323 }
1324 }
1325
1326 if (tx.getTxnType() == ttNFTOKEN_BURN)
1327 {
1328 if (result == tesSUCCESS)
1329 {
1331 {
1332 JLOG(j.fatal())
1333 << "Invariant failed: successful burning didn't increase "
1334 "the number of burned tokens.";
1335 return false;
1336 }
1337 }
1338
1339 if (result != tesSUCCESS && beforeBurnedTotal != afterBurnedTotal)
1340 {
1341 JLOG(j.fatal()) << "Invariant failed: failed burning changed the "
1342 "number of burned tokens.";
1343 return false;
1344 }
1345
1347 {
1348 JLOG(j.fatal())
1349 << "Invariant failed: burning changed the number of "
1350 "minted tokens.";
1351 return false;
1352 }
1353 }
1354
1355 return true;
1356}
1357
1358//------------------------------------------------------------------------------
1359
1360void
1362 bool,
1363 std::shared_ptr<SLE const> const& before,
1365{
1366 if (before && before->getType() == ltRIPPLE_STATE)
1368
1369 if (before && before->getType() == ltMPTOKEN)
1371}
1372
1373bool
1375 STTx const& tx,
1376 TER const result,
1377 XRPAmount const,
1378 ReadView const& view,
1379 beast::Journal const& j)
1380{
1381 if (tx.getTxnType() != ttCLAWBACK)
1382 return true;
1383
1384 if (result == tesSUCCESS)
1385 {
1386 if (trustlinesChanged > 1)
1387 {
1388 JLOG(j.fatal())
1389 << "Invariant failed: more than one trustline changed.";
1390 return false;
1391 }
1392
1393 if (mptokensChanged > 1)
1394 {
1395 JLOG(j.fatal())
1396 << "Invariant failed: more than one mptokens changed.";
1397 return false;
1398 }
1399
1400 if (trustlinesChanged == 1)
1401 {
1402 AccountID const issuer = tx.getAccountID(sfAccount);
1403 STAmount const& amount = tx.getFieldAmount(sfAmount);
1404 AccountID const& holder = amount.getIssuer();
1405 STAmount const holderBalance = accountHolds(
1406 view, holder, amount.getCurrency(), issuer, fhIGNORE_FREEZE, j);
1407
1408 if (holderBalance.signum() < 0)
1409 {
1410 JLOG(j.fatal())
1411 << "Invariant failed: trustline balance is negative";
1412 return false;
1413 }
1414 }
1415 }
1416 else
1417 {
1418 if (trustlinesChanged != 0)
1419 {
1420 JLOG(j.fatal()) << "Invariant failed: some trustlines were changed "
1421 "despite failure of the transaction.";
1422 return false;
1423 }
1424
1425 if (mptokensChanged != 0)
1426 {
1427 JLOG(j.fatal()) << "Invariant failed: some mptokens were changed "
1428 "despite failure of the transaction.";
1429 return false;
1430 }
1431 }
1432
1433 return true;
1434}
1435
1436//------------------------------------------------------------------------------
1437
1438void
1440 bool isDelete,
1441 std::shared_ptr<SLE const> const& before,
1443{
1444 if (after && after->getType() == ltMPTOKEN_ISSUANCE)
1445 {
1446 if (isDelete)
1448 else if (!before)
1450 }
1451
1452 if (after && after->getType() == ltMPTOKEN)
1453 {
1454 if (isDelete)
1456 else if (!before)
1457 {
1459 MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)};
1460 if (mptIssue.getIssuer() == after->at(sfAccount))
1461 mptCreatedByIssuer_ = true;
1462 }
1463 }
1464}
1465
1466bool
1468 STTx const& tx,
1469 TER const result,
1470 XRPAmount const _fee,
1471 ReadView const& view,
1472 beast::Journal const& j)
1473{
1474 if (result == tesSUCCESS)
1475 {
1476 auto const& rules = view.rules();
1477 [[maybe_unused]]
1478 bool enforceCreatedByIssuer = rules.enabled(featureSingleAssetVault) ||
1479 rules.enabled(featureLendingProtocol);
1481 {
1482 JLOG(j.fatal())
1483 << "Invariant failed: MPToken created for the MPT issuer";
1484 // The comment above starting with "assert(enforce)" explains this
1485 // assert.
1486 XRPL_ASSERT_PARTS(
1487 enforceCreatedByIssuer,
1488 "xrpl::ValidMPTIssuance::finalize",
1489 "no issuer MPToken");
1490 if (enforceCreatedByIssuer)
1491 return false;
1492 }
1493
1494 auto const txnType = tx.getTxnType();
1496 {
1497 if (mptIssuancesCreated_ == 0)
1498 {
1499 JLOG(j.fatal()) << "Invariant failed: transaction "
1500 "succeeded without creating a MPT issuance";
1501 }
1502 else if (mptIssuancesDeleted_ != 0)
1503 {
1504 JLOG(j.fatal()) << "Invariant failed: transaction "
1505 "succeeded while removing MPT issuances";
1506 }
1507 else if (mptIssuancesCreated_ > 1)
1508 {
1509 JLOG(j.fatal()) << "Invariant failed: transaction "
1510 "succeeded but created multiple issuances";
1511 }
1512
1513 return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0;
1514 }
1515
1517 {
1518 if (mptIssuancesDeleted_ == 0)
1519 {
1520 JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
1521 "succeeded without removing a MPT issuance";
1522 }
1523 else if (mptIssuancesCreated_ > 0)
1524 {
1525 JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
1526 "succeeded while creating MPT issuances";
1527 }
1528 else if (mptIssuancesDeleted_ > 1)
1529 {
1530 JLOG(j.fatal()) << "Invariant failed: MPT issuance deletion "
1531 "succeeded but deleted multiple issuances";
1532 }
1533
1534 return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1;
1535 }
1536
1537 bool const lendingProtocolEnabled =
1538 view.rules().enabled(featureLendingProtocol);
1539 // ttESCROW_FINISH may authorize an MPT, but it can't have the
1540 // mayAuthorizeMPT privilege, because that may cause
1541 // non-amendment-gated side effects.
1542 bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) &&
1543 (view.rules().enabled(featureSingleAssetVault) ||
1544 lendingProtocolEnabled);
1546 enforceEscrowFinish)
1547 {
1548 bool const submittedByIssuer = tx.isFieldPresent(sfHolder);
1549
1550 if (mptIssuancesCreated_ > 0)
1551 {
1552 JLOG(j.fatal()) << "Invariant failed: MPT authorize "
1553 "succeeded but created MPT issuances";
1554 return false;
1555 }
1556 else if (mptIssuancesDeleted_ > 0)
1557 {
1558 JLOG(j.fatal()) << "Invariant failed: MPT authorize "
1559 "succeeded but deleted issuances";
1560 return false;
1561 }
1562 else if (
1563 lendingProtocolEnabled &&
1565 {
1566 JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
1567 "but created/deleted bad number mptokens";
1568 return false;
1569 }
1570 else if (
1571 submittedByIssuer &&
1573 {
1574 JLOG(j.fatal())
1575 << "Invariant failed: MPT authorize submitted by issuer "
1576 "succeeded but created/deleted mptokens";
1577 return false;
1578 }
1579 else if (
1580 !submittedByIssuer && hasPrivilege(tx, mustAuthorizeMPT) &&
1582 {
1583 // if the holder submitted this tx, then a mptoken must be
1584 // either created or deleted.
1585 JLOG(j.fatal())
1586 << "Invariant failed: MPT authorize submitted by holder "
1587 "succeeded but created/deleted bad number of mptokens";
1588 return false;
1589 }
1590
1591 return true;
1592 }
1593 if (txnType == ttESCROW_FINISH)
1594 {
1595 // ttESCROW_FINISH may authorize an MPT, but it can't have the
1596 // mayAuthorizeMPT privilege, because that may cause
1597 // non-amendment-gated side effects.
1598 XRPL_ASSERT_PARTS(
1599 !enforceEscrowFinish,
1600 "xrpl::ValidMPTIssuance::finalize",
1601 "not escrow finish tx");
1602 return true;
1603 }
1604
1605 if (hasPrivilege(tx, mayDeleteMPT) && mptokensDeleted_ == 1 &&
1608 return true;
1609 }
1610
1611 if (mptIssuancesCreated_ != 0)
1612 {
1613 JLOG(j.fatal()) << "Invariant failed: a MPT issuance was created";
1614 }
1615 else if (mptIssuancesDeleted_ != 0)
1616 {
1617 JLOG(j.fatal()) << "Invariant failed: a MPT issuance was deleted";
1618 }
1619 else if (mptokensCreated_ != 0)
1620 {
1621 JLOG(j.fatal()) << "Invariant failed: a MPToken was created";
1622 }
1623 else if (mptokensDeleted_ != 0)
1624 {
1625 JLOG(j.fatal()) << "Invariant failed: a MPToken was deleted";
1626 }
1627
1628 return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 &&
1630}
1631
1632//------------------------------------------------------------------------------
1633
1634void
1636 bool,
1637 std::shared_ptr<SLE const> const& before,
1639{
1640 if (before && before->getType() != ltPERMISSIONED_DOMAIN)
1641 return;
1642 if (after && after->getType() != ltPERMISSIONED_DOMAIN)
1643 return;
1644
1645 auto check = [](SleStatus& sleStatus,
1646 std::shared_ptr<SLE const> const& sle) {
1647 auto const& credentials = sle->getFieldArray(sfAcceptedCredentials);
1648 sleStatus.credentialsSize_ = credentials.size();
1649 auto const sorted = credentials::makeSorted(credentials);
1650 sleStatus.isUnique_ = !sorted.empty();
1651
1652 // If array have duplicates then all the other checks are invalid
1653 sleStatus.isSorted_ = false;
1654
1655 if (sleStatus.isUnique_)
1656 {
1657 unsigned i = 0;
1658 for (auto const& cred : sorted)
1659 {
1660 auto const& credTx = credentials[i++];
1661 sleStatus.isSorted_ = (cred.first == credTx[sfIssuer]) &&
1662 (cred.second == credTx[sfCredentialType]);
1663 if (!sleStatus.isSorted_)
1664 break;
1665 }
1666 }
1667 };
1668
1669 if (before)
1670 {
1671 sleStatus_[0] = SleStatus();
1672 check(*sleStatus_[0], after);
1673 }
1674
1675 if (after)
1676 {
1677 sleStatus_[1] = SleStatus();
1678 check(*sleStatus_[1], after);
1679 }
1680}
1681
1682bool
1684 STTx const& tx,
1685 TER const result,
1686 XRPAmount const,
1687 ReadView const& view,
1688 beast::Journal const& j)
1689{
1690 if (tx.getTxnType() != ttPERMISSIONED_DOMAIN_SET || result != tesSUCCESS)
1691 return true;
1692
1693 auto check = [](SleStatus const& sleStatus, beast::Journal const& j) {
1694 if (!sleStatus.credentialsSize_)
1695 {
1696 JLOG(j.fatal()) << "Invariant failed: permissioned domain with "
1697 "no rules.";
1698 return false;
1699 }
1700
1701 if (sleStatus.credentialsSize_ >
1703 {
1704 JLOG(j.fatal()) << "Invariant failed: permissioned domain bad "
1705 "credentials size "
1706 << sleStatus.credentialsSize_;
1707 return false;
1708 }
1709
1710 if (!sleStatus.isUnique_)
1711 {
1712 JLOG(j.fatal())
1713 << "Invariant failed: permissioned domain credentials "
1714 "aren't unique";
1715 return false;
1716 }
1717
1718 if (!sleStatus.isSorted_)
1719 {
1720 JLOG(j.fatal())
1721 << "Invariant failed: permissioned domain credentials "
1722 "aren't sorted";
1723 return false;
1724 }
1725
1726 return true;
1727 };
1728
1729 return (sleStatus_[0] ? check(*sleStatus_[0], j) : true) &&
1730 (sleStatus_[1] ? check(*sleStatus_[1], j) : true);
1731}
1732
1733//------------------------------------------------------------------------------
1734
1735void
1737 bool isDelete,
1738 std::shared_ptr<SLE const> const& before,
1740{
1741 if (isDelete)
1742 // Deletion is ignored
1743 return;
1744
1745 if (after && after->getType() == ltACCOUNT_ROOT)
1746 {
1747 bool const isPseudo = [&]() {
1748 // isPseudoAccount checks that any of the pseudo-account fields are
1749 // set.
1751 return true;
1752 // Not all pseudo-accounts have a zero sequence, but all accounts
1753 // with a zero sequence had better be pseudo-accounts.
1754 if (after->at(sfSequence) == 0)
1755 return true;
1756
1757 return false;
1758 }();
1759 if (isPseudo)
1760 {
1761 // Pseudo accounts must have the following properties:
1762 // 1. Exactly one of the pseudo-account fields is set.
1763 // 2. The sequence number is not changed.
1764 // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth
1765 // flags are set.
1766 // 4. The RegularKey is not set.
1767 {
1768 std::vector<SField const*> const& fields =
1770
1771 auto const numFields = std::count_if(
1772 fields.begin(),
1773 fields.end(),
1774 [&after](SField const* sf) -> bool {
1775 return after->isFieldPresent(*sf);
1776 });
1777 if (numFields != 1)
1778 {
1779 std::stringstream error;
1780 error << "pseudo-account has " << numFields
1781 << " pseudo-account fields set";
1782 errors_.emplace_back(error.str());
1783 }
1784 }
1785 if (before && before->at(sfSequence) != after->at(sfSequence))
1786 {
1787 errors_.emplace_back("pseudo-account sequence changed");
1788 }
1789 if (!after->isFlag(
1791 {
1792 errors_.emplace_back("pseudo-account flags are not set");
1793 }
1794 if (after->isFieldPresent(sfRegularKey))
1795 {
1796 errors_.emplace_back("pseudo-account has a regular key");
1797 }
1798 }
1799 }
1800}
1801
1802bool
1804 STTx const& tx,
1805 TER const,
1806 XRPAmount const,
1807 ReadView const& view,
1808 beast::Journal const& j)
1809{
1810 bool const enforce = view.rules().enabled(featureSingleAssetVault);
1811 XRPL_ASSERT(
1812 errors_.empty() || enforce,
1813 "xrpl::ValidPseudoAccounts::finalize : no bad "
1814 "changes or enforce invariant");
1815 if (!errors_.empty())
1816 {
1817 for (auto const& error : errors_)
1818 {
1819 JLOG(j.fatal()) << "Invariant failed: " << error;
1820 }
1821 if (enforce)
1822 return false;
1823 }
1824 return true;
1825}
1826
1827//------------------------------------------------------------------------------
1828
1829void
1831 bool,
1832 std::shared_ptr<SLE const> const& before,
1834{
1835 if (after && after->getType() == ltDIR_NODE)
1836 {
1837 if (after->isFieldPresent(sfDomainID))
1838 domains_.insert(after->getFieldH256(sfDomainID));
1839 }
1840
1841 if (after && after->getType() == ltOFFER)
1842 {
1843 if (after->isFieldPresent(sfDomainID))
1844 domains_.insert(after->getFieldH256(sfDomainID));
1845 else
1846 regularOffers_ = true;
1847
1848 // if a hybrid offer is missing domain or additional book, there's
1849 // something wrong
1850 if (after->isFlag(lsfHybrid) &&
1851 (!after->isFieldPresent(sfDomainID) ||
1852 !after->isFieldPresent(sfAdditionalBooks) ||
1853 after->getFieldArray(sfAdditionalBooks).size() > 1))
1854 badHybrids_ = true;
1855 }
1856}
1857
1858bool
1860 STTx const& tx,
1861 TER const result,
1862 XRPAmount const,
1863 ReadView const& view,
1864 beast::Journal const& j)
1865{
1866 auto const txType = tx.getTxnType();
1867 if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) ||
1868 result != tesSUCCESS)
1869 return true;
1870
1871 // For each offercreate transaction, check if
1872 // permissioned offers are valid
1873 if (txType == ttOFFER_CREATE && badHybrids_)
1874 {
1875 JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed";
1876 return false;
1877 }
1878
1879 if (!tx.isFieldPresent(sfDomainID))
1880 return true;
1881
1882 auto const domain = tx.getFieldH256(sfDomainID);
1883
1884 if (!view.exists(keylet::permissionedDomain(domain)))
1885 {
1886 JLOG(j.fatal()) << "Invariant failed: domain doesn't exist";
1887 return false;
1888 }
1889
1890 // for both payment and offercreate, there shouldn't be another domain
1891 // that's different from the domain specified
1892 for (auto const& d : domains_)
1893 {
1894 if (d != domain)
1895 {
1896 JLOG(j.fatal()) << "Invariant failed: transaction"
1897 " consumed wrong domains";
1898 return false;
1899 }
1900 }
1901
1902 if (regularOffers_)
1903 {
1904 JLOG(j.fatal()) << "Invariant failed: domain transaction"
1905 " affected regular offers";
1906 return false;
1907 }
1908
1909 return true;
1910}
1911
1912void
1914 bool isDelete,
1915 std::shared_ptr<SLE const> const& before,
1917{
1918 if (isDelete)
1919 return;
1920
1921 if (after)
1922 {
1923 auto const type = after->getType();
1924 // AMM object changed
1925 if (type == ltAMM)
1926 {
1927 ammAccount_ = after->getAccountID(sfAccount);
1928 lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance);
1929 }
1930 // AMM pool changed
1931 else if (
1932 (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) ||
1933 (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)))
1934 {
1935 ammPoolChanged_ = true;
1936 }
1937 }
1938
1939 if (before)
1940 {
1941 // AMM object changed
1942 if (before->getType() == ltAMM)
1943 {
1944 lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance);
1945 }
1946 }
1947}
1948
1949static bool
1951 STAmount const& amount,
1952 STAmount const& amount2,
1953 STAmount const& lptAMMBalance,
1954 ValidAMM::ZeroAllowed zeroAllowed)
1955{
1956 bool const positive = amount > beast::zero && amount2 > beast::zero &&
1957 lptAMMBalance > beast::zero;
1958 if (zeroAllowed == ValidAMM::ZeroAllowed::Yes)
1959 return positive ||
1960 (amount == beast::zero && amount2 == beast::zero &&
1961 lptAMMBalance == beast::zero);
1962 return positive;
1963}
1964
1965bool
1966ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const
1967{
1969 {
1970 // LPTokens and the pool can not change on vote
1971 // LCOV_EXCL_START
1972 JLOG(j.error()) << "AMMVote invariant failed: "
1975 << ammPoolChanged_;
1976 if (enforce)
1977 return false;
1978 // LCOV_EXCL_STOP
1979 }
1980
1981 return true;
1982}
1983
1984bool
1985ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const
1986{
1987 if (ammPoolChanged_)
1988 {
1989 // The pool can not change on bid
1990 // LCOV_EXCL_START
1991 JLOG(j.error()) << "AMMBid invariant failed: pool changed";
1992 if (enforce)
1993 return false;
1994 // LCOV_EXCL_STOP
1995 }
1996 // LPTokens are burnt, therefore there should be fewer LPTokens
1997 else if (
2000 *lptAMMBalanceAfter_ <= beast::zero))
2001 {
2002 // LCOV_EXCL_START
2003 JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_
2004 << " " << *lptAMMBalanceAfter_;
2005 if (enforce)
2006 return false;
2007 // LCOV_EXCL_STOP
2008 }
2009
2010 return true;
2011}
2012
2013bool
2015 STTx const& tx,
2016 ReadView const& view,
2017 bool enforce,
2018 beast::Journal const& j) const
2019{
2020 if (!ammAccount_)
2021 {
2022 // LCOV_EXCL_START
2023 JLOG(j.error())
2024 << "AMMCreate invariant failed: AMM object is not created";
2025 if (enforce)
2026 return false;
2027 // LCOV_EXCL_STOP
2028 }
2029 else
2030 {
2031 auto const [amount, amount2] = ammPoolHolds(
2032 view,
2033 *ammAccount_,
2034 tx[sfAmount].get<Issue>(),
2035 tx[sfAmount2].get<Issue>(),
2037 j);
2038 // Create invariant:
2039 // sqrt(amount * amount2) == LPTokens
2040 // all balances are greater than zero
2041 if (!validBalances(
2042 amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) ||
2043 ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) !=
2045 {
2046 JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " "
2047 << amount2 << " " << *lptAMMBalanceAfter_;
2048 if (enforce)
2049 return false;
2050 }
2051 }
2052
2053 return true;
2054}
2055
2056bool
2057ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const
2058{
2059 if (ammAccount_)
2060 {
2061 // LCOV_EXCL_START
2062 std::string const msg = (res == tesSUCCESS)
2063 ? "AMM object is not deleted on tesSUCCESS"
2064 : "AMM object is changed on tecINCOMPLETE";
2065 JLOG(j.error()) << "AMMDelete invariant failed: " << msg;
2066 if (enforce)
2067 return false;
2068 // LCOV_EXCL_STOP
2069 }
2070
2071 return true;
2072}
2073
2074bool
2075ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const
2076{
2077 if (ammAccount_)
2078 {
2079 // LCOV_EXCL_START
2080 JLOG(j.error()) << "AMM swap invariant failed: AMM object changed";
2081 if (enforce)
2082 return false;
2083 // LCOV_EXCL_STOP
2084 }
2085
2086 return true;
2087}
2088
2089bool
2091 xrpl::STTx const& tx,
2092 xrpl::ReadView const& view,
2093 ZeroAllowed zeroAllowed,
2094 beast::Journal const& j) const
2095{
2096 auto const [amount, amount2] = ammPoolHolds(
2097 view,
2098 *ammAccount_,
2099 tx[sfAsset].get<Issue>(),
2100 tx[sfAsset2].get<Issue>(),
2102 j);
2103 // Deposit and Withdrawal invariant:
2104 // sqrt(amount * amount2) >= LPTokens
2105 // all balances are greater than zero
2106 // unless on last withdrawal
2107 auto const poolProductMean = root2(amount * amount2);
2108 bool const nonNegativeBalances =
2109 validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed);
2110 bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_;
2111 // Allow for a small relative error if strongInvariantCheck fails
2112 auto weakInvariantCheck = [&]() {
2113 return *lptAMMBalanceAfter_ != beast::zero &&
2115 poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11});
2116 };
2117 if (!nonNegativeBalances ||
2118 (!strongInvariantCheck && !weakInvariantCheck()))
2119 {
2120 JLOG(j.error()) << "AMM " << tx.getTxnType() << " invariant failed: "
2122 << ammPoolChanged_ << " " << amount << " " << amount2
2123 << " " << poolProductMean << " "
2124 << lptAMMBalanceAfter_->getText() << " "
2125 << ((*lptAMMBalanceAfter_ == beast::zero)
2126 ? Number{1}
2127 : ((*lptAMMBalanceAfter_ - poolProductMean) /
2128 poolProductMean));
2129 return false;
2130 }
2131
2132 return true;
2133}
2134
2135bool
2137 xrpl::STTx const& tx,
2138 xrpl::ReadView const& view,
2139 bool enforce,
2140 beast::Journal const& j) const
2141{
2142 if (!ammAccount_)
2143 {
2144 // LCOV_EXCL_START
2145 JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted";
2146 if (enforce)
2147 return false;
2148 // LCOV_EXCL_STOP
2149 }
2150 else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce)
2151 return false;
2152
2153 return true;
2154}
2155
2156bool
2158 xrpl::STTx const& tx,
2159 xrpl::ReadView const& view,
2160 bool enforce,
2161 beast::Journal const& j) const
2162{
2163 if (!ammAccount_)
2164 {
2165 // Last Withdraw or Clawback deleted AMM
2166 }
2167 else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j))
2168 {
2169 if (enforce)
2170 return false;
2171 }
2172
2173 return true;
2174}
2175
2176bool
2178 STTx const& tx,
2179 TER const result,
2180 XRPAmount const,
2181 ReadView const& view,
2182 beast::Journal const& j)
2183{
2184 // Delete may return tecINCOMPLETE if there are too many
2185 // trustlines to delete.
2186 if (result != tesSUCCESS && result != tecINCOMPLETE)
2187 return true;
2188
2189 bool const enforce = view.rules().enabled(fixAMMv1_3);
2190
2191 switch (tx.getTxnType())
2192 {
2193 case ttAMM_CREATE:
2194 return finalizeCreate(tx, view, enforce, j);
2195 case ttAMM_DEPOSIT:
2196 return finalizeDeposit(tx, view, enforce, j);
2197 case ttAMM_CLAWBACK:
2198 case ttAMM_WITHDRAW:
2199 return finalizeWithdraw(tx, view, enforce, j);
2200 case ttAMM_BID:
2201 return finalizeBid(enforce, j);
2202 case ttAMM_VOTE:
2203 return finalizeVote(enforce, j);
2204 case ttAMM_DELETE:
2205 return finalizeDelete(enforce, result, j);
2206 case ttCHECK_CASH:
2207 case ttOFFER_CREATE:
2208 case ttPAYMENT:
2209 return finalizeDEX(enforce, j);
2210 default:
2211 break;
2212 }
2213
2214 return true;
2215}
2216
2217//------------------------------------------------------------------------------
2218
2219void
2221 bool isDelete,
2222 std::shared_ptr<SLE const> const& before,
2224{
2225 if (isDelete || !before)
2226 // Creation and deletion are ignored
2227 return;
2228
2229 changedEntries_.emplace(before, after);
2230}
2231
2232bool
2234 STTx const& tx,
2235 TER const,
2236 XRPAmount const,
2237 ReadView const& view,
2238 beast::Journal const& j)
2239{
2240 static auto const fieldChanged =
2241 [](auto const& before, auto const& after, auto const& field) {
2242 bool const beforeField = before->isFieldPresent(field);
2243 bool const afterField = after->isFieldPresent(field);
2244 return beforeField != afterField ||
2245 (afterField && before->at(field) != after->at(field));
2246 };
2247 for (auto const& slePair : changedEntries_)
2248 {
2249 auto const& before = slePair.first;
2250 auto const& after = slePair.second;
2251 auto const type = after->getType();
2252 bool bad = false;
2253 [[maybe_unused]] bool enforce = false;
2254 switch (type)
2255 {
2256 case ltLOAN_BROKER:
2257 /*
2258 * We check this invariant regardless of lending protocol
2259 * amendment status, allowing for detection and logging of
2260 * potential issues even when the amendment is disabled.
2261 */
2262 enforce = view.rules().enabled(featureLendingProtocol);
2263 bad = fieldChanged(before, after, sfLedgerEntryType) ||
2264 fieldChanged(before, after, sfLedgerIndex) ||
2265 fieldChanged(before, after, sfSequence) ||
2266 fieldChanged(before, after, sfOwnerNode) ||
2267 fieldChanged(before, after, sfVaultNode) ||
2268 fieldChanged(before, after, sfVaultID) ||
2269 fieldChanged(before, after, sfAccount) ||
2270 fieldChanged(before, after, sfOwner) ||
2271 fieldChanged(before, after, sfManagementFeeRate) ||
2272 fieldChanged(before, after, sfCoverRateMinimum) ||
2273 fieldChanged(before, after, sfCoverRateLiquidation);
2274 break;
2275 case ltLOAN:
2276 /*
2277 * We check this invariant regardless of lending protocol
2278 * amendment status, allowing for detection and logging of
2279 * potential issues even when the amendment is disabled.
2280 */
2281 enforce = view.rules().enabled(featureLendingProtocol);
2282 bad = fieldChanged(before, after, sfLedgerEntryType) ||
2283 fieldChanged(before, after, sfLedgerIndex) ||
2284 fieldChanged(before, after, sfSequence) ||
2285 fieldChanged(before, after, sfOwnerNode) ||
2286 fieldChanged(before, after, sfLoanBrokerNode) ||
2287 fieldChanged(before, after, sfLoanBrokerID) ||
2288 fieldChanged(before, after, sfBorrower) ||
2289 fieldChanged(before, after, sfLoanOriginationFee) ||
2290 fieldChanged(before, after, sfLoanServiceFee) ||
2291 fieldChanged(before, after, sfLatePaymentFee) ||
2292 fieldChanged(before, after, sfClosePaymentFee) ||
2293 fieldChanged(before, after, sfOverpaymentFee) ||
2294 fieldChanged(before, after, sfInterestRate) ||
2295 fieldChanged(before, after, sfLateInterestRate) ||
2296 fieldChanged(before, after, sfCloseInterestRate) ||
2297 fieldChanged(before, after, sfOverpaymentInterestRate) ||
2298 fieldChanged(before, after, sfStartDate) ||
2299 fieldChanged(before, after, sfPaymentInterval) ||
2300 fieldChanged(before, after, sfGracePeriod) ||
2301 fieldChanged(before, after, sfLoanScale);
2302 break;
2303 default:
2304 /*
2305 * We check this invariant regardless of lending protocol
2306 * amendment status, allowing for detection and logging of
2307 * potential issues even when the amendment is disabled.
2308 *
2309 * We use the lending protocol as a gate, even though
2310 * all transactions are affected because that's when it
2311 * was added.
2312 */
2313 enforce = view.rules().enabled(featureLendingProtocol);
2314 bad = fieldChanged(before, after, sfLedgerEntryType) ||
2315 fieldChanged(before, after, sfLedgerIndex);
2316 }
2317 XRPL_ASSERT(
2318 !bad || enforce,
2319 "xrpl::NoModifiedUnmodifiableFields::finalize : no bad "
2320 "changes or enforce invariant");
2321 if (bad)
2322 {
2323 JLOG(j.fatal())
2324 << "Invariant failed: changed an unchangeable field for "
2325 << tx.getTransactionID();
2326 if (enforce)
2327 return false;
2328 }
2329 }
2330 return true;
2331}
2332
2333//------------------------------------------------------------------------------
2334
2335void
2337 bool isDelete,
2338 std::shared_ptr<SLE const> const& before,
2340{
2341 if (after)
2342 {
2343 if (after->getType() == ltLOAN_BROKER)
2344 {
2345 auto& broker = brokers_[after->key()];
2346 broker.brokerBefore = before;
2347 broker.brokerAfter = after;
2348 }
2349 else if (
2350 after->getType() == ltACCOUNT_ROOT &&
2351 after->isFieldPresent(sfLoanBrokerID))
2352 {
2353 auto const& loanBrokerID = after->at(sfLoanBrokerID);
2354 // create an entry if one doesn't already exist
2355 brokers_.emplace(loanBrokerID, BrokerInfo{});
2356 }
2357 else if (after->getType() == ltRIPPLE_STATE)
2358 {
2359 lines_.emplace_back(after);
2360 }
2361 else if (after->getType() == ltMPTOKEN)
2362 {
2363 mpts_.emplace_back(after);
2364 }
2365 }
2366}
2367
2368bool
2370 ReadView const& view,
2371 SLE::const_ref dir,
2372 beast::Journal const& j) const
2373{
2374 auto const next = dir->at(~sfIndexNext);
2375 auto const prev = dir->at(~sfIndexPrevious);
2376 if ((prev && *prev) || (next && *next))
2377 {
2378 JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
2379 "OwnerCount has multiple directory pages";
2380 return false;
2381 }
2382 auto indexes = dir->getFieldV256(sfIndexes);
2383 if (indexes.size() > 1)
2384 {
2385 JLOG(j.fatal())
2386 << "Invariant failed: Loan Broker with zero "
2387 "OwnerCount has multiple indexes in the Directory root";
2388 return false;
2389 }
2390 if (indexes.size() == 1)
2391 {
2392 auto const index = indexes.value().front();
2393 auto const sle = view.read(keylet::unchecked(index));
2394 if (!sle)
2395 {
2396 JLOG(j.fatal())
2397 << "Invariant failed: Loan Broker directory corrupt";
2398 return false;
2399 }
2400 if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN)
2401 {
2402 JLOG(j.fatal())
2403 << "Invariant failed: Loan Broker with zero "
2404 "OwnerCount has an unexpected entry in the directory";
2405 return false;
2406 }
2407 }
2408
2409 return true;
2410}
2411
2412bool
2414 STTx const& tx,
2415 TER const,
2416 XRPAmount const,
2417 ReadView const& view,
2418 beast::Journal const& j)
2419{
2420 // Loan Brokers will not exist on ledger if the Lending Protocol amendment
2421 // is not enabled, so there's no need to check it.
2422
2423 for (auto const& line : lines_)
2424 {
2425 for (auto const& field : {&sfLowLimit, &sfHighLimit})
2426 {
2427 auto const account =
2428 view.read(keylet::account(line->at(*field).getIssuer()));
2429 // This Invariant doesn't know about the rules for Trust Lines, so
2430 // if the account is missing, don't treat it as an error. This
2431 // loop is only concerned with finding Broker pseudo-accounts
2432 if (account && account->isFieldPresent(sfLoanBrokerID))
2433 {
2434 auto const& loanBrokerID = account->at(sfLoanBrokerID);
2435 // create an entry if one doesn't already exist
2436 brokers_.emplace(loanBrokerID, BrokerInfo{});
2437 }
2438 }
2439 }
2440 for (auto const& mpt : mpts_)
2441 {
2442 auto const account = view.read(keylet::account(mpt->at(sfAccount)));
2443 // This Invariant doesn't know about the rules for MPTokens, so
2444 // if the account is missing, don't treat is as an error. This
2445 // loop is only concerned with finding Broker pseudo-accounts
2446 if (account && account->isFieldPresent(sfLoanBrokerID))
2447 {
2448 auto const& loanBrokerID = account->at(sfLoanBrokerID);
2449 // create an entry if one doesn't already exist
2450 brokers_.emplace(loanBrokerID, BrokerInfo{});
2451 }
2452 }
2453
2454 for (auto const& [brokerID, broker] : brokers_)
2455 {
2456 auto const& after = broker.brokerAfter
2457 ? broker.brokerAfter
2458 : view.read(keylet::loanbroker(brokerID));
2459
2460 if (!after)
2461 {
2462 JLOG(j.fatal()) << "Invariant failed: Loan Broker missing";
2463 return false;
2464 }
2465
2466 auto const& before = broker.brokerBefore;
2467
2468 // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants
2469 // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most
2470 // one node (the root), which will only hold entries for `RippleState`
2471 // or `MPToken` objects.
2472 if (after->at(sfOwnerCount) == 0)
2473 {
2474 auto const dir = view.read(keylet::ownerDir(after->at(sfAccount)));
2475 if (dir)
2476 {
2477 if (!goodZeroDirectory(view, dir, j))
2478 {
2479 return false;
2480 }
2481 }
2482 }
2483 if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence))
2484 {
2485 JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number "
2486 "decreased";
2487 return false;
2488 }
2489 if (after->at(sfDebtTotal) < 0)
2490 {
2491 JLOG(j.fatal())
2492 << "Invariant failed: Loan Broker debt total is negative";
2493 return false;
2494 }
2495 if (after->at(sfCoverAvailable) < 0)
2496 {
2497 JLOG(j.fatal())
2498 << "Invariant failed: Loan Broker cover available is negative";
2499 return false;
2500 }
2501 auto const vault = view.read(keylet::vault(after->at(sfVaultID)));
2502 if (!vault)
2503 {
2504 JLOG(j.fatal())
2505 << "Invariant failed: Loan Broker vault ID is invalid";
2506 return false;
2507 }
2508 auto const& vaultAsset = vault->at(sfAsset);
2509 if (after->at(sfCoverAvailable) < accountHolds(
2510 view,
2511 after->at(sfAccount),
2512 vaultAsset,
2515 j))
2516 {
2517 JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available "
2518 "is less than pseudo-account asset balance";
2519 return false;
2520 }
2521 }
2522 return true;
2523}
2524
2525//------------------------------------------------------------------------------
2526
2527void
2529 bool isDelete,
2530 std::shared_ptr<SLE const> const& before,
2532{
2533 if (after && after->getType() == ltLOAN)
2534 {
2535 loans_.emplace_back(before, after);
2536 }
2537}
2538
2539bool
2541 STTx const& tx,
2542 TER const,
2543 XRPAmount const,
2544 ReadView const& view,
2545 beast::Journal const& j)
2546{
2547 // Loans will not exist on ledger if the Lending Protocol amendment
2548 // is not enabled, so there's no need to check it.
2549
2550 for (auto const& [before, after] : loans_)
2551 {
2552 // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
2553 // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off
2554 if (after->at(sfPaymentRemaining) == 0 &&
2555 (after->at(sfTotalValueOutstanding) != beast::zero ||
2556 after->at(sfPrincipalOutstanding) != beast::zero ||
2557 after->at(sfManagementFeeOutstanding) != beast::zero))
2558 {
2559 JLOG(j.fatal()) << "Invariant failed: Loan with zero payments "
2560 "remaining has not been paid off";
2561 return false;
2562 }
2563 // If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid
2564 // off
2565 if (after->at(sfPaymentRemaining) != 0 &&
2566 after->at(sfTotalValueOutstanding) == beast::zero &&
2567 after->at(sfPrincipalOutstanding) == beast::zero &&
2568 after->at(sfManagementFeeOutstanding) == beast::zero)
2569 {
2570 JLOG(j.fatal()) << "Invariant failed: Loan with zero payments "
2571 "remaining has not been paid off";
2572 return false;
2573 }
2574 if (before &&
2575 (before->isFlag(lsfLoanOverpayment) !=
2576 after->isFlag(lsfLoanOverpayment)))
2577 {
2578 JLOG(j.fatal())
2579 << "Invariant failed: Loan Overpayment flag changed";
2580 return false;
2581 }
2582 // Must not be negative - STNumber
2583 for (auto const field :
2584 {&sfLoanServiceFee,
2585 &sfLatePaymentFee,
2586 &sfClosePaymentFee,
2587 &sfPrincipalOutstanding,
2588 &sfTotalValueOutstanding,
2589 &sfManagementFeeOutstanding})
2590 {
2591 if (after->at(*field) < 0)
2592 {
2593 JLOG(j.fatal()) << "Invariant failed: " << field->getName()
2594 << " is negative ";
2595 return false;
2596 }
2597 }
2598 // Must be positive - STNumber
2599 for (auto const field : {
2600 &sfPeriodicPayment,
2601 })
2602 {
2603 if (after->at(*field) <= 0)
2604 {
2605 JLOG(j.fatal()) << "Invariant failed: " << field->getName()
2606 << " is zero or negative ";
2607 return false;
2608 }
2609 }
2610 }
2611 return true;
2612}
2613
2616{
2617 XRPL_ASSERT(
2618 from.getType() == ltVAULT,
2619 "ValidVault::Vault::make : from Vault object");
2620
2621 ValidVault::Vault self;
2622 self.key = from.key();
2623 self.asset = from.at(sfAsset);
2624 self.pseudoId = from.getAccountID(sfAccount);
2625 self.shareMPTID = from.getFieldH192(sfShareMPTID);
2626 self.assetsTotal = from.at(sfAssetsTotal);
2627 self.assetsAvailable = from.at(sfAssetsAvailable);
2628 self.assetsMaximum = from.at(sfAssetsMaximum);
2629 self.lossUnrealized = from.at(sfLossUnrealized);
2630 return self;
2631}
2632
2635{
2636 XRPL_ASSERT(
2637 from.getType() == ltMPTOKEN_ISSUANCE,
2638 "ValidVault::Shares::make : from MPTokenIssuance object");
2639
2640 ValidVault::Shares self;
2641 self.share = MPTIssue(
2642 makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer)));
2643 self.sharesTotal = from.at(sfOutstandingAmount);
2644 self.sharesMaximum = from[~sfMaximumAmount].value_or(maxMPTokenAmount);
2645 return self;
2646}
2647
2648void
2650 bool isDelete,
2651 std::shared_ptr<SLE const> const& before,
2653{
2654 // If `before` is empty, this means an object is being created, in which
2655 // case `isDelete` must be false. Otherwise `before` and `after` are set and
2656 // `isDelete` indicates whether an object is being deleted or modified.
2657 XRPL_ASSERT(
2658 after != nullptr && (before != nullptr || !isDelete),
2659 "xrpl::ValidVault::visitEntry : some object is available");
2660
2661 // Number balanceDelta will capture the difference (delta) between "before"
2662 // state (zero if created) and "after" state (zero if destroyed), so the
2663 // invariants can validate that the change in account balances matches the
2664 // change in vault balances, stored to deltas_ at the end of this function.
2665 Number balanceDelta{};
2666
2667 std::int8_t sign = 0;
2668 if (before)
2669 {
2670 switch (before->getType())
2671 {
2672 case ltVAULT:
2673 beforeVault_.push_back(Vault::make(*before));
2674 break;
2675 case ltMPTOKEN_ISSUANCE:
2676 // At this moment we have no way of telling if this object holds
2677 // vault shares or something else. Save it for finalize.
2678 beforeMPTs_.push_back(Shares::make(*before));
2679 balanceDelta = static_cast<std::int64_t>(
2680 before->getFieldU64(sfOutstandingAmount));
2681 sign = 1;
2682 break;
2683 case ltMPTOKEN:
2684 balanceDelta =
2685 static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
2686 sign = -1;
2687 break;
2688 case ltACCOUNT_ROOT:
2689 case ltRIPPLE_STATE:
2690 balanceDelta = before->getFieldAmount(sfBalance);
2691 sign = -1;
2692 break;
2693 default:;
2694 }
2695 }
2696
2697 if (!isDelete && after)
2698 {
2699 switch (after->getType())
2700 {
2701 case ltVAULT:
2702 afterVault_.push_back(Vault::make(*after));
2703 break;
2704 case ltMPTOKEN_ISSUANCE:
2705 // At this moment we have no way of telling if this object holds
2706 // vault shares or something else. Save it for finalize.
2707 afterMPTs_.push_back(Shares::make(*after));
2708 balanceDelta -= Number(static_cast<std::int64_t>(
2709 after->getFieldU64(sfOutstandingAmount)));
2710 sign = 1;
2711 break;
2712 case ltMPTOKEN:
2713 balanceDelta -= Number(
2714 static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
2715 sign = -1;
2716 break;
2717 case ltACCOUNT_ROOT:
2718 case ltRIPPLE_STATE:
2719 balanceDelta -= Number(after->getFieldAmount(sfBalance));
2720 sign = -1;
2721 break;
2722 default:;
2723 }
2724 }
2725
2726 uint256 const key = (before ? before->key() : after->key());
2727 // Append to deltas if sign is non-zero, i.e. an object of an interesting
2728 // type has been updated. A transaction may update an object even when
2729 // its balance has not changed, e.g. transaction fee equals the amount
2730 // transferred to the account. We intentionally do not compare balanceDelta
2731 // against zero, to avoid missing such updates.
2732 if (sign != 0)
2733 deltas_[key] = balanceDelta * sign;
2734}
2735
2736bool
2738 STTx const& tx,
2739 TER const ret,
2740 XRPAmount const fee,
2741 ReadView const& view,
2742 beast::Journal const& j)
2743{
2744 bool const enforce = view.rules().enabled(featureSingleAssetVault);
2745
2746 if (!isTesSuccess(ret))
2747 return true; // Do not perform checks
2748
2749 if (afterVault_.empty() && beforeVault_.empty())
2750 {
2752 {
2753 JLOG(j.fatal()) << //
2754 "Invariant failed: vault operation succeeded without modifying "
2755 "a vault";
2756 XRPL_ASSERT(
2757 enforce, "xrpl::ValidVault::finalize : vault noop invariant");
2758 return !enforce;
2759 }
2760
2761 return true; // Not a vault operation
2762 }
2763 else if (!(hasPrivilege(tx, mustModifyVault) ||
2765 {
2766 JLOG(j.fatal()) << //
2767 "Invariant failed: vault updated by a wrong transaction type";
2768 XRPL_ASSERT(
2769 enforce,
2770 "xrpl::ValidVault::finalize : illegal vault transaction "
2771 "invariant");
2772 return !enforce; // Also not a vault operation
2773 }
2774
2775 if (beforeVault_.size() > 1 || afterVault_.size() > 1)
2776 {
2777 JLOG(j.fatal()) << //
2778 "Invariant failed: vault operation updated more than single vault";
2779 XRPL_ASSERT(
2780 enforce, "xrpl::ValidVault::finalize : single vault invariant");
2781 return !enforce; // That's all we can do here
2782 }
2783
2784 auto const txnType = tx.getTxnType();
2785
2786 // We do special handling for ttVAULT_DELETE first, because it's the only
2787 // vault-modifying transaction without an "after" state of the vault
2788 if (afterVault_.empty())
2789 {
2790 if (txnType != ttVAULT_DELETE)
2791 {
2792 JLOG(j.fatal()) << //
2793 "Invariant failed: vault deleted by a wrong transaction type";
2794 XRPL_ASSERT(
2795 enforce,
2796 "xrpl::ValidVault::finalize : illegal vault deletion "
2797 "invariant");
2798 return !enforce; // That's all we can do here
2799 }
2800
2801 // Note, if afterVault_ is empty then we know that beforeVault_ is not
2802 // empty, as enforced at the top of this function
2803 auto const& beforeVault = beforeVault_[0];
2804
2805 // At this moment we only know a vault is being deleted and there
2806 // might be some MPTokenIssuance objects which are deleted in the
2807 // same transaction. Find the one matching this vault.
2808 auto const deletedShares = [&]() -> std::optional<Shares> {
2809 for (auto const& e : beforeMPTs_)
2810 {
2811 if (e.share.getMptID() == beforeVault.shareMPTID)
2812 return std::move(e);
2813 }
2814 return std::nullopt;
2815 }();
2816
2817 if (!deletedShares)
2818 {
2819 JLOG(j.fatal()) << "Invariant failed: deleted vault must also "
2820 "delete shares";
2821 XRPL_ASSERT(
2822 enforce,
2823 "xrpl::ValidVault::finalize : shares deletion invariant");
2824 return !enforce; // That's all we can do here
2825 }
2826
2827 bool result = true;
2828 if (deletedShares->sharesTotal != 0)
2829 {
2830 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
2831 "shares outstanding";
2832 result = false;
2833 }
2834 if (beforeVault.assetsTotal != zero)
2835 {
2836 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
2837 "assets outstanding";
2838 result = false;
2839 }
2840 if (beforeVault.assetsAvailable != zero)
2841 {
2842 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
2843 "assets available";
2844 result = false;
2845 }
2846
2847 return result;
2848 }
2849 else if (txnType == ttVAULT_DELETE)
2850 {
2851 JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without "
2852 "deleting a vault";
2853 XRPL_ASSERT(
2854 enforce, "xrpl::ValidVault::finalize : vault deletion invariant");
2855 return !enforce; // That's all we can do here
2856 }
2857
2858 // Note, `afterVault_.empty()` is handled above
2859 auto const& afterVault = afterVault_[0];
2860 XRPL_ASSERT(
2861 beforeVault_.empty() || beforeVault_[0].key == afterVault.key,
2862 "xrpl::ValidVault::finalize : single vault operation");
2863
2864 auto const updatedShares = [&]() -> std::optional<Shares> {
2865 // At this moment we only know that a vault is being updated and there
2866 // might be some MPTokenIssuance objects which are also updated in the
2867 // same transaction. Find the one matching the shares to this vault.
2868 // Note, we expect updatedMPTs collection to be extremely small. For
2869 // such collections linear search is faster than lookup.
2870 for (auto const& e : afterMPTs_)
2871 {
2872 if (e.share.getMptID() == afterVault.shareMPTID)
2873 return e;
2874 }
2875
2876 auto const sleShares =
2877 view.read(keylet::mptIssuance(afterVault.shareMPTID));
2878
2879 return sleShares ? std::optional<Shares>(Shares::make(*sleShares))
2880 : std::nullopt;
2881 }();
2882
2883 bool result = true;
2884
2885 // Universal transaction checks
2886 if (!beforeVault_.empty())
2887 {
2888 auto const& beforeVault = beforeVault_[0];
2889 if (afterVault.asset != beforeVault.asset ||
2890 afterVault.pseudoId != beforeVault.pseudoId ||
2891 afterVault.shareMPTID != beforeVault.shareMPTID)
2892 {
2893 JLOG(j.fatal())
2894 << "Invariant failed: violation of vault immutable data";
2895 result = false;
2896 }
2897 }
2898
2899 if (!updatedShares)
2900 {
2901 JLOG(j.fatal()) << "Invariant failed: updated vault must have shares";
2902 XRPL_ASSERT(
2903 enforce, "xrpl::ValidVault::finalize : vault has shares invariant");
2904 return !enforce; // That's all we can do here
2905 }
2906
2907 if (updatedShares->sharesTotal == 0)
2908 {
2909 if (afterVault.assetsTotal != zero)
2910 {
2911 JLOG(j.fatal()) << "Invariant failed: updated zero sized "
2912 "vault must have no assets outstanding";
2913 result = false;
2914 }
2915 if (afterVault.assetsAvailable != zero)
2916 {
2917 JLOG(j.fatal()) << "Invariant failed: updated zero sized "
2918 "vault must have no assets available";
2919 result = false;
2920 }
2921 }
2922 else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
2923 {
2924 JLOG(j.fatal()) //
2925 << "Invariant failed: updated shares must not exceed maximum "
2926 << updatedShares->sharesMaximum;
2927 result = false;
2928 }
2929
2930 if (afterVault.assetsAvailable < zero)
2931 {
2932 JLOG(j.fatal())
2933 << "Invariant failed: assets available must be positive";
2934 result = false;
2935 }
2936
2937 if (afterVault.assetsAvailable > afterVault.assetsTotal)
2938 {
2939 JLOG(j.fatal()) << "Invariant failed: assets available must "
2940 "not be greater than assets outstanding";
2941 result = false;
2942 }
2943 else if (
2944 afterVault.lossUnrealized >
2945 afterVault.assetsTotal - afterVault.assetsAvailable)
2946 {
2947 JLOG(j.fatal()) //
2948 << "Invariant failed: loss unrealized must not exceed "
2949 "the difference between assets outstanding and available";
2950 result = false;
2951 }
2952
2953 if (afterVault.assetsTotal < zero)
2954 {
2955 JLOG(j.fatal())
2956 << "Invariant failed: assets outstanding must be positive";
2957 result = false;
2958 }
2959
2960 if (afterVault.assetsMaximum < zero)
2961 {
2962 JLOG(j.fatal()) << "Invariant failed: assets maximum must be positive";
2963 result = false;
2964 }
2965
2966 // Thanks to this check we can simply do `assert(!beforeVault_.empty()` when
2967 // enforcing invariants on transaction types other than ttVAULT_CREATE
2968 if (beforeVault_.empty() && txnType != ttVAULT_CREATE)
2969 {
2970 JLOG(j.fatal()) << //
2971 "Invariant failed: vault created by a wrong transaction type";
2972 XRPL_ASSERT(
2973 enforce, "xrpl::ValidVault::finalize : vault creation invariant");
2974 return !enforce; // That's all we can do here
2975 }
2976
2977 if (!beforeVault_.empty() &&
2978 afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
2979 txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
2980 {
2981 JLOG(j.fatal()) << //
2982 "Invariant failed: vault transaction must not change loss "
2983 "unrealized";
2984 result = false;
2985 }
2986
2987 auto const beforeShares = [&]() -> std::optional<Shares> {
2988 if (beforeVault_.empty())
2989 return std::nullopt;
2990 auto const& beforeVault = beforeVault_[0];
2991
2992 for (auto const& e : beforeMPTs_)
2993 {
2994 if (e.share.getMptID() == beforeVault.shareMPTID)
2995 return std::move(e);
2996 }
2997 return std::nullopt;
2998 }();
2999
3000 if (!beforeShares &&
3001 (tx.getTxnType() == ttVAULT_DEPOSIT || //
3002 tx.getTxnType() == ttVAULT_WITHDRAW || //
3003 tx.getTxnType() == ttVAULT_CLAWBACK))
3004 {
3005 JLOG(j.fatal()) << "Invariant failed: vault operation succeeded "
3006 "without updating shares";
3007 XRPL_ASSERT(
3008 enforce, "xrpl::ValidVault::finalize : shares noop invariant");
3009 return !enforce; // That's all we can do here
3010 }
3011
3012 auto const& vaultAsset = afterVault.asset;
3013 auto const deltaAssets = [&](AccountID const& id) -> std::optional<Number> {
3014 auto const get = //
3015 [&](auto const& it, std::int8_t sign = 1) -> std::optional<Number> {
3016 if (it == deltas_.end())
3017 return std::nullopt;
3018
3019 return it->second * sign;
3020 };
3021
3022 return std::visit(
3023 [&]<typename TIss>(TIss const& issue) {
3024 if constexpr (std::is_same_v<TIss, Issue>)
3025 {
3026 if (isXRP(issue))
3027 return get(deltas_.find(keylet::account(id).key));
3028 return get(
3029 deltas_.find(keylet::line(id, issue).key),
3030 id > issue.getIssuer() ? -1 : 1);
3031 }
3032 else if constexpr (std::is_same_v<TIss, MPTIssue>)
3033 {
3034 return get(deltas_.find(
3035 keylet::mptoken(issue.getMptID(), id).key));
3036 }
3037 },
3038 vaultAsset.value());
3039 };
3040 auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> {
3041 auto ret = deltaAssets(tx[sfAccount]);
3042 // Nothing returned or not XRP transaction
3043 if (!ret.has_value() || !vaultAsset.native())
3044 return ret;
3045
3046 // Delegated transaction; no need to compensate for fees
3047 if (auto const delegate = tx[~sfDelegate];
3048 delegate.has_value() && *delegate != tx[sfAccount])
3049 return ret;
3050
3051 *ret += fee.drops();
3052 if (*ret == zero)
3053 return std::nullopt;
3054
3055 return ret;
3056 };
3057 auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
3058 auto const it = [&]() {
3059 if (id == afterVault.pseudoId)
3060 return deltas_.find(
3061 keylet::mptIssuance(afterVault.shareMPTID).key);
3062 return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
3063 }();
3064
3065 return it != deltas_.end() ? std::optional<Number>(it->second)
3066 : std::nullopt;
3067 };
3068
3069 // Technically this does not need to be a lambda, but it's more
3070 // convenient thanks to early "return false"; the not-so-nice
3071 // alternatives are several layers of nested if/else or more complex
3072 // (i.e. brittle) if statements.
3073 result &= [&]() {
3074 switch (txnType)
3075 {
3076 case ttVAULT_CREATE: {
3077 bool result = true;
3078
3079 if (!beforeVault_.empty())
3080 {
3081 JLOG(j.fatal()) //
3082 << "Invariant failed: create operation must not have "
3083 "updated a vault";
3084 result = false;
3085 }
3086
3087 if (afterVault.assetsAvailable != zero ||
3088 afterVault.assetsTotal != zero ||
3089 afterVault.lossUnrealized != zero ||
3090 updatedShares->sharesTotal != 0)
3091 {
3092 JLOG(j.fatal()) //
3093 << "Invariant failed: created vault must be empty";
3094 result = false;
3095 }
3096
3097 if (afterVault.pseudoId != updatedShares->share.getIssuer())
3098 {
3099 JLOG(j.fatal()) //
3100 << "Invariant failed: shares issuer and vault "
3101 "pseudo-account must be the same";
3102 result = false;
3103 }
3104
3105 auto const sleSharesIssuer = view.read(
3106 keylet::account(updatedShares->share.getIssuer()));
3107 if (!sleSharesIssuer)
3108 {
3109 JLOG(j.fatal()) //
3110 << "Invariant failed: shares issuer must exist";
3111 return false;
3112 }
3113
3114 if (!isPseudoAccount(sleSharesIssuer))
3115 {
3116 JLOG(j.fatal()) //
3117 << "Invariant failed: shares issuer must be a "
3118 "pseudo-account";
3119 result = false;
3120 }
3121
3122 if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID];
3123 !vaultId || *vaultId != afterVault.key)
3124 {
3125 JLOG(j.fatal()) //
3126 << "Invariant failed: shares issuer pseudo-account "
3127 "must point back to the vault";
3128 result = false;
3129 }
3130
3131 return result;
3132 }
3133 case ttVAULT_SET: {
3134 bool result = true;
3135
3136 XRPL_ASSERT(
3137 !beforeVault_.empty(),
3138 "xrpl::ValidVault::finalize : set updated a vault");
3139 auto const& beforeVault = beforeVault_[0];
3140
3141 auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
3142 if (vaultDeltaAssets)
3143 {
3144 JLOG(j.fatal()) << //
3145 "Invariant failed: set must not change vault balance";
3146 result = false;
3147 }
3148
3149 if (beforeVault.assetsTotal != afterVault.assetsTotal)
3150 {
3151 JLOG(j.fatal()) << //
3152 "Invariant failed: set must not change assets "
3153 "outstanding";
3154 result = false;
3155 }
3156
3157 if (afterVault.assetsMaximum > zero &&
3158 afterVault.assetsTotal > afterVault.assetsMaximum)
3159 {
3160 JLOG(j.fatal()) << //
3161 "Invariant failed: set assets outstanding must not "
3162 "exceed assets maximum";
3163 result = false;
3164 }
3165
3166 if (beforeVault.assetsAvailable != afterVault.assetsAvailable)
3167 {
3168 JLOG(j.fatal()) << //
3169 "Invariant failed: set must not change assets "
3170 "available";
3171 result = false;
3172 }
3173
3174 if (beforeShares && updatedShares &&
3175 beforeShares->sharesTotal != updatedShares->sharesTotal)
3176 {
3177 JLOG(j.fatal()) << //
3178 "Invariant failed: set must not change shares "
3179 "outstanding";
3180 result = false;
3181 }
3182
3183 return result;
3184 }
3185 case ttVAULT_DEPOSIT: {
3186 bool result = true;
3187
3188 XRPL_ASSERT(
3189 !beforeVault_.empty(),
3190 "xrpl::ValidVault::finalize : deposit updated a vault");
3191 auto const& beforeVault = beforeVault_[0];
3192
3193 auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
3194
3195 if (!vaultDeltaAssets)
3196 {
3197 JLOG(j.fatal()) << //
3198 "Invariant failed: deposit must change vault balance";
3199 return false; // That's all we can do
3200 }
3201
3202 if (*vaultDeltaAssets > tx[sfAmount])
3203 {
3204 JLOG(j.fatal()) << //
3205 "Invariant failed: deposit must not change vault "
3206 "balance by more than deposited amount";
3207 result = false;
3208 }
3209
3210 if (*vaultDeltaAssets <= zero)
3211 {
3212 JLOG(j.fatal()) << //
3213 "Invariant failed: deposit must increase vault balance";
3214 result = false;
3215 }
3216
3217 // Any payments (including deposits) made by the issuer
3218 // do not change their balance, but create funds instead.
3219 bool const issuerDeposit = [&]() -> bool {
3220 if (vaultAsset.native())
3221 return false;
3222 return tx[sfAccount] == vaultAsset.getIssuer();
3223 }();
3224
3225 if (!issuerDeposit)
3226 {
3227 auto const accountDeltaAssets = deltaAssetsTxAccount();
3228 if (!accountDeltaAssets)
3229 {
3230 JLOG(j.fatal()) << //
3231 "Invariant failed: deposit must change depositor "
3232 "balance";
3233 return false;
3234 }
3235
3236 if (*accountDeltaAssets >= zero)
3237 {
3238 JLOG(j.fatal()) << //
3239 "Invariant failed: deposit must decrease depositor "
3240 "balance";
3241 result = false;
3242 }
3243
3244 if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
3245 {
3246 JLOG(j.fatal()) << //
3247 "Invariant failed: deposit must change vault and "
3248 "depositor balance by equal amount";
3249 result = false;
3250 }
3251 }
3252
3253 if (afterVault.assetsMaximum > zero &&
3254 afterVault.assetsTotal > afterVault.assetsMaximum)
3255 {
3256 JLOG(j.fatal()) << //
3257 "Invariant failed: deposit assets outstanding must not "
3258 "exceed assets maximum";
3259 result = false;
3260 }
3261
3262 auto const accountDeltaShares = deltaShares(tx[sfAccount]);
3263 if (!accountDeltaShares)
3264 {
3265 JLOG(j.fatal()) << //
3266 "Invariant failed: deposit must change depositor "
3267 "shares";
3268 return false; // That's all we can do
3269 }
3270
3271 if (*accountDeltaShares <= zero)
3272 {
3273 JLOG(j.fatal()) << //
3274 "Invariant failed: deposit must increase depositor "
3275 "shares";
3276 result = false;
3277 }
3278
3279 auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
3280 if (!vaultDeltaShares || *vaultDeltaShares == zero)
3281 {
3282 JLOG(j.fatal()) << //
3283 "Invariant failed: deposit must change vault shares";
3284 return false; // That's all we can do
3285 }
3286
3287 if (*vaultDeltaShares * -1 != *accountDeltaShares)
3288 {
3289 JLOG(j.fatal()) << //
3290 "Invariant failed: deposit must change depositor and "
3291 "vault shares by equal amount";
3292 result = false;
3293 }
3294
3295 if (beforeVault.assetsTotal + *vaultDeltaAssets !=
3296 afterVault.assetsTotal)
3297 {
3298 JLOG(j.fatal()) << "Invariant failed: deposit and assets "
3299 "outstanding must add up";
3300 result = false;
3301 }
3302 if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
3303 afterVault.assetsAvailable)
3304 {
3305 JLOG(j.fatal()) << "Invariant failed: deposit and assets "
3306 "available must add up";
3307 result = false;
3308 }
3309
3310 return result;
3311 }
3312 case ttVAULT_WITHDRAW: {
3313 bool result = true;
3314
3315 XRPL_ASSERT(
3316 !beforeVault_.empty(),
3317 "xrpl::ValidVault::finalize : withdrawal updated a "
3318 "vault");
3319 auto const& beforeVault = beforeVault_[0];
3320
3321 auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
3322
3323 if (!vaultDeltaAssets)
3324 {
3325 JLOG(j.fatal()) << "Invariant failed: withdrawal must "
3326 "change vault balance";
3327 return false; // That's all we can do
3328 }
3329
3330 if (*vaultDeltaAssets >= zero)
3331 {
3332 JLOG(j.fatal()) << "Invariant failed: withdrawal must "
3333 "decrease vault balance";
3334 result = false;
3335 }
3336
3337 // Any payments (including withdrawal) going to the issuer
3338 // do not change their balance, but destroy funds instead.
3339 bool const issuerWithdrawal = [&]() -> bool {
3340 if (vaultAsset.native())
3341 return false;
3342 auto const destination =
3343 tx[~sfDestination].value_or(tx[sfAccount]);
3344 return destination == vaultAsset.getIssuer();
3345 }();
3346
3347 if (!issuerWithdrawal)
3348 {
3349 auto const accountDeltaAssets = deltaAssetsTxAccount();
3350 auto const otherAccountDelta =
3351 [&]() -> std::optional<Number> {
3352 if (auto const destination = tx[~sfDestination];
3353 destination && *destination != tx[sfAccount])
3354 return deltaAssets(*destination);
3355 return std::nullopt;
3356 }();
3357
3358 if (accountDeltaAssets.has_value() ==
3359 otherAccountDelta.has_value())
3360 {
3361 JLOG(j.fatal()) << //
3362 "Invariant failed: withdrawal must change one "
3363 "destination balance";
3364 return false;
3365 }
3366
3367 auto const destinationDelta = //
3368 accountDeltaAssets ? *accountDeltaAssets
3369 : *otherAccountDelta;
3370
3371 if (destinationDelta <= zero)
3372 {
3373 JLOG(j.fatal()) << //
3374 "Invariant failed: withdrawal must increase "
3375 "destination balance";
3376 result = false;
3377 }
3378
3379 if (*vaultDeltaAssets * -1 != destinationDelta)
3380 {
3381 JLOG(j.fatal()) << //
3382 "Invariant failed: withdrawal must change vault "
3383 "and destination balance by equal amount";
3384 result = false;
3385 }
3386 }
3387
3388 auto const accountDeltaShares = deltaShares(tx[sfAccount]);
3389 if (!accountDeltaShares)
3390 {
3391 JLOG(j.fatal()) << //
3392 "Invariant failed: withdrawal must change depositor "
3393 "shares";
3394 return false;
3395 }
3396
3397 if (*accountDeltaShares >= zero)
3398 {
3399 JLOG(j.fatal()) << //
3400 "Invariant failed: withdrawal must decrease depositor "
3401 "shares";
3402 result = false;
3403 }
3404
3405 auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
3406 if (!vaultDeltaShares || *vaultDeltaShares == zero)
3407 {
3408 JLOG(j.fatal()) << //
3409 "Invariant failed: withdrawal must change vault shares";
3410 return false; // That's all we can do
3411 }
3412
3413 if (*vaultDeltaShares * -1 != *accountDeltaShares)
3414 {
3415 JLOG(j.fatal()) << //
3416 "Invariant failed: withdrawal must change depositor "
3417 "and vault shares by equal amount";
3418 result = false;
3419 }
3420
3421 // Note, vaultBalance is negative (see check above)
3422 if (beforeVault.assetsTotal + *vaultDeltaAssets !=
3423 afterVault.assetsTotal)
3424 {
3425 JLOG(j.fatal()) << "Invariant failed: withdrawal and "
3426 "assets outstanding must add up";
3427 result = false;
3428 }
3429
3430 if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
3431 afterVault.assetsAvailable)
3432 {
3433 JLOG(j.fatal()) << "Invariant failed: withdrawal and "
3434 "assets available must add up";
3435 result = false;
3436 }
3437
3438 return result;
3439 }
3440 case ttVAULT_CLAWBACK: {
3441 bool result = true;
3442
3443 XRPL_ASSERT(
3444 !beforeVault_.empty(),
3445 "xrpl::ValidVault::finalize : clawback updated a vault");
3446 auto const& beforeVault = beforeVault_[0];
3447
3448 if (vaultAsset.native() ||
3449 vaultAsset.getIssuer() != tx[sfAccount])
3450 {
3451 JLOG(j.fatal()) << //
3452 "Invariant failed: clawback may only be performed by "
3453 "the asset issuer";
3454 return false; // That's all we can do
3455 }
3456
3457 auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
3458
3459 if (!vaultDeltaAssets)
3460 {
3461 JLOG(j.fatal()) << //
3462 "Invariant failed: clawback must change vault balance";
3463 return false; // That's all we can do
3464 }
3465
3466 if (*vaultDeltaAssets >= zero)
3467 {
3468 JLOG(j.fatal()) << //
3469 "Invariant failed: clawback must decrease vault "
3470 "balance";
3471 result = false;
3472 }
3473
3474 auto const accountDeltaShares = deltaShares(tx[sfHolder]);
3475 if (!accountDeltaShares)
3476 {
3477 JLOG(j.fatal()) << //
3478 "Invariant failed: clawback must change holder shares";
3479 return false; // That's all we can do
3480 }
3481
3482 if (*accountDeltaShares >= zero)
3483 {
3484 JLOG(j.fatal()) << //
3485 "Invariant failed: clawback must decrease holder "
3486 "shares";
3487 result = false;
3488 }
3489
3490 auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
3491 if (!vaultDeltaShares || *vaultDeltaShares == zero)
3492 {
3493 JLOG(j.fatal()) << //
3494 "Invariant failed: clawback must change vault shares";
3495 return false; // That's all we can do
3496 }
3497
3498 if (*vaultDeltaShares * -1 != *accountDeltaShares)
3499 {
3500 JLOG(j.fatal()) << //
3501 "Invariant failed: clawback must change holder and "
3502 "vault shares by equal amount";
3503 result = false;
3504 }
3505
3506 if (beforeVault.assetsTotal + *vaultDeltaAssets !=
3507 afterVault.assetsTotal)
3508 {
3509 JLOG(j.fatal()) << //
3510 "Invariant failed: clawback and assets outstanding "
3511 "must add up";
3512 result = false;
3513 }
3514
3515 if (beforeVault.assetsAvailable + *vaultDeltaAssets !=
3516 afterVault.assetsAvailable)
3517 {
3518 JLOG(j.fatal()) << //
3519 "Invariant failed: clawback and assets available must "
3520 "add up";
3521 result = false;
3522 }
3523
3524 return result;
3525 }
3526
3527 case ttLOAN_SET:
3528 case ttLOAN_MANAGE:
3529 case ttLOAN_PAY: {
3530 // TBD
3531 return true;
3532 }
3533
3534 default:
3535 // LCOV_EXCL_START
3536 UNREACHABLE(
3537 "xrpl::ValidVault::finalize : unknown transaction type");
3538 return false;
3539 // LCOV_EXCL_STOP
3540 }
3541 }();
3542
3543 if (!result)
3544 {
3545 // The comment at the top of this file starting with "assert(enforce)"
3546 // explains this assert.
3547 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault invariants");
3548 return !enforce;
3549 }
3550
3551 return true;
3552}
3553
3554} // namespace xrpl
T begin(T... args)
A generic endpoint for log messages.
Definition Journal.h:41
Stream fatal() const
Definition Journal.h:333
Stream error() const
Definition Journal.h:327
Stream debug() const
Definition Journal.h:309
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
std::vector< std::pair< std::shared_ptr< SLE const >, std::shared_ptr< SLE const > > > accountsDeleted_
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 currency issued by an account.
Definition Issue.h:14
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()
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 &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
std::set< std::pair< SLE::const_pointer, SLE::const_pointer > > changedEntries_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, 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 &)
A view into a ledger.
Definition ReadView.h:32
virtual Rules const & rules() const =0
Returns the tx processing rules.
virtual bool exists(Keylet const &k) const =0
Determine if a state item exists.
LedgerIndex seq() const
Returns the sequence number of the base ledger.
Definition ReadView.h:99
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 std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:111
Identifies fields.
Definition SField.h:127
int signum() const noexcept
Definition STAmount.h:506
STAmount zeroed() const
Returns a zero value with the same issuer and currency.
Definition STAmount.h:512
bool native() const noexcept
Definition STAmount.h:450
XRPAmount xrp() const
Definition STAmount.cpp:264
uint256 const & key() const
Returns the 'key' (or 'index') of this item.
LedgerEntryType getType() const
uint192 getFieldH192(SField const &field) const
Definition STObject.cpp:620
T::value_type at(TypedField< T > const &f) const
Get the value of a field.
Definition STObject.h:1055
std::uint32_t getFieldU32(SField const &field) const
Definition STObject.cpp:596
uint256 getHash(HashPrefix prefix) const
Definition STObject.cpp:376
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:465
uint256 getFieldH256(SField const &field) const
Definition STObject.cpp:626
AccountID getAccountID(SField const &field) const
Definition STObject.cpp:638
STAmount const & getFieldAmount(SField const &field) const
Definition STObject.cpp:652
TxType getTxnType() const
Definition STTx.h:187
uint256 getTransactionID() const
Definition STTx.h:199
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::shared_ptr< SLE const > findIssuer(AccountID const &issuerID, ReadView const &view)
void recordBalance(Issue const &issue, BalanceChange change)
bool isValidEntry(std::shared_ptr< SLE const > const &before, std::shared_ptr< SLE const > const &after)
bool validateIssuerChanges(std::shared_ptr< SLE const > const &issuer, IssuerChanges const &changes, STTx const &tx, beast::Journal const &j, bool enforce)
std::map< AccountID, std::shared_ptr< SLE const > const > possibleIssuers_
STAmount calculateBalanceChange(std::shared_ptr< SLE const > const &before, std::shared_ptr< SLE const > const &after, bool isDelete)
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
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 &)
void recordBalanceChanges(std::shared_ptr< SLE const > const &after, STAmount const &balanceChange)
std::optional< AccountID > ammAccount_
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 finalizeBid(bool enforce, beast::Journal const &) const
bool finalizeCreate(STTx const &, ReadView const &, bool enforce, beast::Journal const &) const
bool finalizeDEX(bool enforce, beast::Journal const &) const
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE 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 > lptAMMBalanceAfter_
bool finalizeVote(bool enforce, beast::Journal const &) const
bool finalizeDelete(bool enforce, TER res, beast::Journal const &) const
std::optional< STAmount > lptAMMBalanceBefore_
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 &)
std::uint32_t trustlinesChanged
std::uint32_t mptokensChanged
std::map< uint256, BrokerInfo > brokers_
bool goodZeroDirectory(ReadView const &view, SLE::const_ref dir, beast::Journal const &j) const
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
std::vector< SLE::const_pointer > mpts_
std::vector< SLE::const_pointer > lines_
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 &)
std::vector< std::pair< SLE::const_pointer, SLE::const_pointer > > loans_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
std::uint32_t mptokensCreated_
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::uint32_t mptokensDeleted_
std::uint32_t mptIssuancesCreated_
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 &)
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 &)
hash_set< uint256 > domains_
std::optional< SleStatus > sleStatus_[2]
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 &)
std::vector< std::string > errors_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
static Number constexpr zero
std::vector< Shares > afterMPTs_
std::unordered_map< uint256, Number > deltas_
std::vector< Vault > afterVault_
std::vector< Shares > beforeMPTs_
bool finalize(STTx const &, TER const, XRPAmount const, ReadView const &, beast::Journal const &)
std::vector< Vault > beforeVault_
constexpr value_type drops() const
Returns the number of drops.
Definition XRPAmount.h:158
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 &)
base_uint next() const
Definition base_uint.h:436
T count_if(T... args)
T emplace_back(T... args)
T empty(T... args)
T end(T... args)
T invoke(T... args)
T is_same_v
std::set< std::pair< AccountID, Slice > > makeSorted(STArray const &credentials)
Keylet nftpage_max(AccountID const &owner)
A keylet for the owner's last possible NFT page.
Definition Indexes.cpp:393
Keylet unchecked(uint256 const &key) noexcept
Any ledger entry.
Definition Indexes.cpp:350
Keylet loanbroker(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:552
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:356
Keylet mptIssuance(std::uint32_t seq, AccountID const &issuer) noexcept
Definition Indexes.cpp:508
Keylet vault(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:546
Keylet line(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:226
Keylet nftpage_min(AccountID const &owner)
NFT page keylets.
Definition Indexes.cpp:385
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:522
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:166
Keylet permissionedDomain(AccountID const &account, std::uint32_t seq) noexcept
Definition Indexes.cpp:564
uint256 constexpr pageMask(std::string_view("0000000000000000000000000000000000000000ffffffffffffffffffffffff"))
bool compareTokens(uint256 const &a, uint256 const &b)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
std::size_t constexpr dirMaxTokensPerPage
The maximum number of items in an NFT page.
Definition Protocol.h:47
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:96
std::vector< SField const * > const & getPseudoAccountFields()
Definition View.cpp:1199
@ fhIGNORE_FREEZE
Definition View.h:59
bool isXRP(AccountID const &c)
Definition AccountID.h:71
T get(Section const &section, std::string const &name, T const &defaultValue=T{})
Retrieve a key/value pair from a section.
constexpr base_uint< Bits, Tag > operator|(base_uint< Bits, Tag > const &a, base_uint< Bits, Tag > const &b)
Definition base_uint.h:596
std::uint64_t constexpr maxMPTokenAmount
The maximum amount of MPTokenIssuance.
Definition Protocol.h:235
STAmount ammLPTokens(STAmount const &asset1, STAmount const &asset2, Issue const &lptIssue)
Calculate LP Tokens given AMM pool reserves.
Definition AMMHelpers.cpp:6
STAmount accountHolds(ReadView const &view, AccountID const &account, Currency const &currency, AccountID const &issuer, FreezeHandling zeroIfFrozen, beast::Journal j)
Definition View.cpp:461
base_uint< 256 > uint256
Definition base_uint.h:539
bool isPseudoAccount(std::shared_ptr< SLE const > sleAcct, std::set< SField const * > const &pseudoFieldFilter={})
Definition View.cpp:1226
@ mustModifyVault
@ destroyMPTIssuance
@ changeNFTCounts
@ createPseudoAcct
@ createMPTIssuance
@ mustAuthorizeMPT
@ mayAuthorizeMPT
static bool validBalances(STAmount const &amount, STAmount const &amount2, STAmount const &lptAMMBalance, ValidAMM::ZeroAllowed zeroAllowed)
bool hasPrivilege(STTx const &tx, Privilege priv)
@ ahIGNORE_AUTH
Definition View.h:62
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:3922
Number root2(Number f)
Definition Number.cpp:709
std::size_t constexpr maxPermissionedDomainCredentialsArraySize
The maximum number of credentials can be passed in array for permissioned domain.
Definition Protocol.h:229
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:12
@ transactionID
transaction plus signature to give transaction ID
bool isTesSuccess(TER x) noexcept
Definition TER.h:659
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
constexpr XRPAmount INITIAL_XRP
Configure the native currency.
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:110
@ tecINCOMPLETE
Definition TER.h:317
@ lsfDepositAuth
@ lsfDefaultRipple
@ lsfLoanOverpayment
@ lsfLowDeepFreeze
@ lsfGlobalFreeze
@ lsfLowFreeze
@ lsfDisableMaster
@ lsfHighFreeze
@ lsfHighDeepFreeze
Currency const & badCurrency()
We deliberately disallow the currency that looks like "XRP" because too many people were using it ins...
MPTID makeMptID(std::uint32_t sequence, AccountID const &account)
Definition Indexes.cpp:152
@ tesSUCCESS
Definition TER.h:226
constexpr std::enable_if_t< std::is_integral_v< Dest > &&std::is_integral_v< Src >, Dest > safe_cast(Src s) noexcept
Definition safe_cast.h:22
std::array< keyletDesc< AccountID const & >, 6 > const directAccountKeylets
Definition Indexes.h:383
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:20
uint256 key
Definition Keylet.h:21
std::shared_ptr< SLE const > const line
std::vector< BalanceChange > senders
std::vector< BalanceChange > receivers
static Shares make(SLE const &)
static Vault make(SLE const &)
T to_string(T... args)
T value_or(T... args)
T visit(T... args)