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