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 isDel,
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 = [isDel](std::vector<SleStatus>& sleStatus, std::shared_ptr<SLE const> const& sle) {
1520 auto const& credentials = sle->getFieldArray(sfAcceptedCredentials);
1521 auto const sorted = credentials::makeSorted(credentials);
1522
1523 SleStatus ss{credentials.size(), false, !sorted.empty(), isDel};
1524
1525 // If array have duplicates then all the other checks are invalid
1526 if (ss.isUnique_)
1527 {
1528 unsigned i = 0;
1529 for (auto const& cred : sorted)
1530 {
1531 auto const& credTx = credentials[i++];
1532 ss.isSorted_ = (cred.first == credTx[sfIssuer]) && (cred.second == credTx[sfCredentialType]);
1533 if (!ss.isSorted_)
1534 break;
1535 }
1536 }
1537 sleStatus.emplace_back(std::move(ss));
1538 };
1539
1540 if (after)
1541 check(sleStatus_, after);
1542}
1543
1544bool
1546 STTx const& tx,
1547 TER const result,
1548 XRPAmount const,
1549 ReadView const& view,
1550 beast::Journal const& j)
1551{
1552 auto check = [](SleStatus const& sleStatus, beast::Journal const& j) {
1553 if (!sleStatus.credentialsSize_)
1554 {
1555 JLOG(j.fatal()) << "Invariant failed: permissioned domain with "
1556 "no rules.";
1557 return false;
1558 }
1559
1561 {
1562 JLOG(j.fatal()) << "Invariant failed: permissioned domain bad "
1563 "credentials size "
1564 << sleStatus.credentialsSize_;
1565 return false;
1566 }
1567
1568 if (!sleStatus.isUnique_)
1569 {
1570 JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials "
1571 "aren't unique";
1572 return false;
1573 }
1574
1575 if (!sleStatus.isSorted_)
1576 {
1577 JLOG(j.fatal()) << "Invariant failed: permissioned domain credentials "
1578 "aren't sorted";
1579 return false;
1580 }
1581
1582 return true;
1583 };
1584
1585 if (view.rules().enabled(fixPermissionedDomainInvariant))
1586 {
1587 // No permissioned domains should be affected if the transaction failed
1588 if (result != tesSUCCESS)
1589 // If nothing changed, all is good. If there were changes, that's
1590 // bad.
1591 return sleStatus_.empty();
1592
1593 if (sleStatus_.size() > 1)
1594 {
1595 JLOG(j.fatal()) << "Invariant failed: transaction affected more "
1596 "than 1 permissioned domain entry.";
1597 return false;
1598 }
1599
1600 switch (tx.getTxnType())
1601 {
1602 case ttPERMISSIONED_DOMAIN_SET: {
1603 if (sleStatus_.empty())
1604 {
1605 JLOG(j.fatal()) << "Invariant failed: no domain objects affected by "
1606 "PermissionedDomainSet";
1607 return false;
1608 }
1609
1610 auto const& sleStatus = sleStatus_[0];
1611 if (sleStatus.isDelete_)
1612 {
1613 JLOG(j.fatal()) << "Invariant failed: domain object "
1614 "deleted by PermissionedDomainSet";
1615 return false;
1616 }
1617 return check(sleStatus, j);
1618 }
1619 case ttPERMISSIONED_DOMAIN_DELETE: {
1620 if (sleStatus_.empty())
1621 {
1622 JLOG(j.fatal()) << "Invariant failed: no domain objects affected by "
1623 "PermissionedDomainDelete";
1624 return false;
1625 }
1626
1627 if (!sleStatus_[0].isDelete_)
1628 {
1629 JLOG(j.fatal()) << "Invariant failed: domain object "
1630 "modified, but not deleted by "
1631 "PermissionedDomainDelete";
1632 return false;
1633 }
1634 return true;
1635 }
1636 default: {
1637 if (!sleStatus_.empty())
1638 {
1639 JLOG(j.fatal()) << "Invariant failed: " << sleStatus_.size()
1640 << " domain object(s) affected by an "
1641 "unauthorized transaction. "
1642 << tx.getTxnType();
1643 return false;
1644 }
1645 return true;
1646 }
1647 }
1648 }
1649 else
1650 {
1651 if (tx.getTxnType() != ttPERMISSIONED_DOMAIN_SET || result != tesSUCCESS || sleStatus_.empty())
1652 return true;
1653 return check(sleStatus_[0], j);
1654 }
1655}
1656
1657//------------------------------------------------------------------------------
1658
1659void
1661 bool isDelete,
1662 std::shared_ptr<SLE const> const& before,
1664{
1665 if (isDelete)
1666 // Deletion is ignored
1667 return;
1668
1669 if (after && after->getType() == ltACCOUNT_ROOT)
1670 {
1671 bool const isPseudo = [&]() {
1672 // isPseudoAccount checks that any of the pseudo-account fields are
1673 // set.
1675 return true;
1676 // Not all pseudo-accounts have a zero sequence, but all accounts
1677 // with a zero sequence had better be pseudo-accounts.
1678 if (after->at(sfSequence) == 0)
1679 return true;
1680
1681 return false;
1682 }();
1683 if (isPseudo)
1684 {
1685 // Pseudo accounts must have the following properties:
1686 // 1. Exactly one of the pseudo-account fields is set.
1687 // 2. The sequence number is not changed.
1688 // 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth
1689 // flags are set.
1690 // 4. The RegularKey is not set.
1691 {
1693
1694 auto const numFields = std::count_if(fields.begin(), fields.end(), [&after](SField const* sf) -> bool {
1695 return after->isFieldPresent(*sf);
1696 });
1697 if (numFields != 1)
1698 {
1699 std::stringstream error;
1700 error << "pseudo-account has " << numFields << " pseudo-account fields set";
1701 errors_.emplace_back(error.str());
1702 }
1703 }
1704 if (before && before->at(sfSequence) != after->at(sfSequence))
1705 {
1706 errors_.emplace_back("pseudo-account sequence changed");
1707 }
1709 {
1710 errors_.emplace_back("pseudo-account flags are not set");
1711 }
1712 if (after->isFieldPresent(sfRegularKey))
1713 {
1714 errors_.emplace_back("pseudo-account has a regular key");
1715 }
1716 }
1717 }
1718}
1719
1720bool
1721ValidPseudoAccounts::finalize(STTx const& tx, TER const, XRPAmount const, ReadView const& view, beast::Journal const& j)
1722{
1723 bool const enforce = view.rules().enabled(featureSingleAssetVault);
1724 XRPL_ASSERT(
1725 errors_.empty() || enforce,
1726 "xrpl::ValidPseudoAccounts::finalize : no bad "
1727 "changes or enforce invariant");
1728 if (!errors_.empty())
1729 {
1730 for (auto const& error : errors_)
1731 {
1732 JLOG(j.fatal()) << "Invariant failed: " << error;
1733 }
1734 if (enforce)
1735 return false;
1736 }
1737 return true;
1738}
1739
1740//------------------------------------------------------------------------------
1741
1742void
1744 bool,
1745 std::shared_ptr<SLE const> const& before,
1747{
1748 if (after && after->getType() == ltDIR_NODE)
1749 {
1750 if (after->isFieldPresent(sfDomainID))
1751 domains_.insert(after->getFieldH256(sfDomainID));
1752 }
1753
1754 if (after && after->getType() == ltOFFER)
1755 {
1756 if (after->isFieldPresent(sfDomainID))
1757 domains_.insert(after->getFieldH256(sfDomainID));
1758 else
1759 regularOffers_ = true;
1760
1761 // if a hybrid offer is missing domain or additional book, there's
1762 // something wrong
1763 if (after->isFlag(lsfHybrid) &&
1764 (!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) ||
1765 after->getFieldArray(sfAdditionalBooks).size() > 1))
1766 badHybrids_ = true;
1767 }
1768}
1769
1770bool
1772 STTx const& tx,
1773 TER const result,
1774 XRPAmount const,
1775 ReadView const& view,
1776 beast::Journal const& j)
1777{
1778 auto const txType = tx.getTxnType();
1779 if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) || result != tesSUCCESS)
1780 return true;
1781
1782 // For each offercreate transaction, check if
1783 // permissioned offers are valid
1784 if (txType == ttOFFER_CREATE && badHybrids_)
1785 {
1786 JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed";
1787 return false;
1788 }
1789
1790 if (!tx.isFieldPresent(sfDomainID))
1791 return true;
1792
1793 auto const domain = tx.getFieldH256(sfDomainID);
1794
1795 if (!view.exists(keylet::permissionedDomain(domain)))
1796 {
1797 JLOG(j.fatal()) << "Invariant failed: domain doesn't exist";
1798 return false;
1799 }
1800
1801 // for both payment and offercreate, there shouldn't be another domain
1802 // that's different from the domain specified
1803 for (auto const& d : domains_)
1804 {
1805 if (d != domain)
1806 {
1807 JLOG(j.fatal()) << "Invariant failed: transaction"
1808 " consumed wrong domains";
1809 return false;
1810 }
1811 }
1812
1813 if (regularOffers_)
1814 {
1815 JLOG(j.fatal()) << "Invariant failed: domain transaction"
1816 " affected regular offers";
1817 return false;
1818 }
1819
1820 return true;
1821}
1822
1823void
1825{
1826 if (isDelete)
1827 return;
1828
1829 if (after)
1830 {
1831 auto const type = after->getType();
1832 // AMM object changed
1833 if (type == ltAMM)
1834 {
1835 ammAccount_ = after->getAccountID(sfAccount);
1836 lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance);
1837 }
1838 // AMM pool changed
1839 else if (
1840 (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) ||
1841 (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)))
1842 {
1843 ammPoolChanged_ = true;
1844 }
1845 }
1846
1847 if (before)
1848 {
1849 // AMM object changed
1850 if (before->getType() == ltAMM)
1851 {
1852 lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance);
1853 }
1854 }
1855}
1856
1857static bool
1859 STAmount const& amount,
1860 STAmount const& amount2,
1861 STAmount const& lptAMMBalance,
1862 ValidAMM::ZeroAllowed zeroAllowed)
1863{
1864 bool const positive = amount > beast::zero && amount2 > beast::zero && lptAMMBalance > beast::zero;
1865 if (zeroAllowed == ValidAMM::ZeroAllowed::Yes)
1866 return positive || (amount == beast::zero && amount2 == beast::zero && lptAMMBalance == beast::zero);
1867 return positive;
1868}
1869
1870bool
1871ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const
1872{
1874 {
1875 // LPTokens and the pool can not change on vote
1876 // LCOV_EXCL_START
1877 JLOG(j.error()) << "AMMVote invariant failed: " << lptAMMBalanceBefore_.value_or(STAmount{}) << " "
1879 if (enforce)
1880 return false;
1881 // LCOV_EXCL_STOP
1882 }
1883
1884 return true;
1885}
1886
1887bool
1888ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const
1889{
1890 if (ammPoolChanged_)
1891 {
1892 // The pool can not change on bid
1893 // LCOV_EXCL_START
1894 JLOG(j.error()) << "AMMBid invariant failed: pool changed";
1895 if (enforce)
1896 return false;
1897 // LCOV_EXCL_STOP
1898 }
1899 // LPTokens are burnt, therefore there should be fewer LPTokens
1900 else if (
1903 {
1904 // LCOV_EXCL_START
1905 JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ << " " << *lptAMMBalanceAfter_;
1906 if (enforce)
1907 return false;
1908 // LCOV_EXCL_STOP
1909 }
1910
1911 return true;
1912}
1913
1914bool
1915ValidAMM::finalizeCreate(STTx const& tx, ReadView const& view, bool enforce, beast::Journal const& j) const
1916{
1917 if (!ammAccount_)
1918 {
1919 // LCOV_EXCL_START
1920 JLOG(j.error()) << "AMMCreate invariant failed: AMM object is not created";
1921 if (enforce)
1922 return false;
1923 // LCOV_EXCL_STOP
1924 }
1925 else
1926 {
1927 auto const [amount, amount2] =
1928 ammPoolHolds(view, *ammAccount_, tx[sfAmount].get<Issue>(), tx[sfAmount2].get<Issue>(), fhIGNORE_FREEZE, j);
1929 // Create invariant:
1930 // sqrt(amount * amount2) == LPTokens
1931 // all balances are greater than zero
1932 if (!validBalances(amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) ||
1933 ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != *lptAMMBalanceAfter_)
1934 {
1935 JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " << amount2 << " "
1937 if (enforce)
1938 return false;
1939 }
1940 }
1941
1942 return true;
1943}
1944
1945bool
1946ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const
1947{
1948 if (ammAccount_)
1949 {
1950 // LCOV_EXCL_START
1951 std::string const msg =
1952 (res == tesSUCCESS) ? "AMM object is not deleted on tesSUCCESS" : "AMM object is changed on tecINCOMPLETE";
1953 JLOG(j.error()) << "AMMDelete invariant failed: " << msg;
1954 if (enforce)
1955 return false;
1956 // LCOV_EXCL_STOP
1957 }
1958
1959 return true;
1960}
1961
1962bool
1963ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const
1964{
1965 if (ammAccount_)
1966 {
1967 // LCOV_EXCL_START
1968 JLOG(j.error()) << "AMM swap invariant failed: AMM object changed";
1969 if (enforce)
1970 return false;
1971 // LCOV_EXCL_STOP
1972 }
1973
1974 return true;
1975}
1976
1977bool
1979 xrpl::STTx const& tx,
1980 xrpl::ReadView const& view,
1981 ZeroAllowed zeroAllowed,
1982 beast::Journal const& j) const
1983{
1984 auto const [amount, amount2] =
1985 ammPoolHolds(view, *ammAccount_, tx[sfAsset].get<Issue>(), tx[sfAsset2].get<Issue>(), fhIGNORE_FREEZE, j);
1986 // Deposit and Withdrawal invariant:
1987 // sqrt(amount * amount2) >= LPTokens
1988 // all balances are greater than zero
1989 // unless on last withdrawal
1990 auto const poolProductMean = root2(amount * amount2);
1991 bool const nonNegativeBalances = validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed);
1992 bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_;
1993 // Allow for a small relative error if strongInvariantCheck fails
1994 auto weakInvariantCheck = [&]() {
1995 return *lptAMMBalanceAfter_ != beast::zero &&
1996 withinRelativeDistance(poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11});
1997 };
1998 if (!nonNegativeBalances || (!strongInvariantCheck && !weakInvariantCheck()))
1999 {
2000 JLOG(j.error()) << "AMM " << tx.getTxnType() << " invariant failed: " << tx.getHash(HashPrefix::transactionID)
2001 << " " << ammPoolChanged_ << " " << amount << " " << amount2 << " " << poolProductMean << " "
2002 << lptAMMBalanceAfter_->getText() << " "
2003 << ((*lptAMMBalanceAfter_ == beast::zero)
2004 ? Number{1}
2005 : ((*lptAMMBalanceAfter_ - poolProductMean) / poolProductMean));
2006 return false;
2007 }
2008
2009 return true;
2010}
2011
2012bool
2013ValidAMM::finalizeDeposit(xrpl::STTx const& tx, xrpl::ReadView const& view, bool enforce, beast::Journal const& j) const
2014{
2015 if (!ammAccount_)
2016 {
2017 // LCOV_EXCL_START
2018 JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted";
2019 if (enforce)
2020 return false;
2021 // LCOV_EXCL_STOP
2022 }
2023 else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce)
2024 return false;
2025
2026 return true;
2027}
2028
2029bool
2030ValidAMM::finalizeWithdraw(xrpl::STTx const& tx, xrpl::ReadView const& view, bool enforce, beast::Journal const& j)
2031 const
2032{
2033 if (!ammAccount_)
2034 {
2035 // Last Withdraw or Clawback deleted AMM
2036 }
2037 else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j))
2038 {
2039 if (enforce)
2040 return false;
2041 }
2042
2043 return true;
2044}
2045
2046bool
2047ValidAMM::finalize(STTx const& tx, TER const result, XRPAmount const, ReadView const& view, beast::Journal const& j)
2048{
2049 // Delete may return tecINCOMPLETE if there are too many
2050 // trustlines to delete.
2051 if (result != tesSUCCESS && result != tecINCOMPLETE)
2052 return true;
2053
2054 bool const enforce = view.rules().enabled(fixAMMv1_3);
2055
2056 switch (tx.getTxnType())
2057 {
2058 case ttAMM_CREATE:
2059 return finalizeCreate(tx, view, enforce, j);
2060 case ttAMM_DEPOSIT:
2061 return finalizeDeposit(tx, view, enforce, j);
2062 case ttAMM_CLAWBACK:
2063 case ttAMM_WITHDRAW:
2064 return finalizeWithdraw(tx, view, enforce, j);
2065 case ttAMM_BID:
2066 return finalizeBid(enforce, j);
2067 case ttAMM_VOTE:
2068 return finalizeVote(enforce, j);
2069 case ttAMM_DELETE:
2070 return finalizeDelete(enforce, result, j);
2071 case ttCHECK_CASH:
2072 case ttOFFER_CREATE:
2073 case ttPAYMENT:
2074 return finalizeDEX(enforce, j);
2075 default:
2076 break;
2077 }
2078
2079 return true;
2080}
2081
2082//------------------------------------------------------------------------------
2083
2084void
2086 bool isDelete,
2087 std::shared_ptr<SLE const> const& before,
2089{
2090 if (isDelete || !before)
2091 // Creation and deletion are ignored
2092 return;
2093
2094 changedEntries_.emplace(before, after);
2095}
2096
2097bool
2099 STTx const& tx,
2100 TER const,
2101 XRPAmount const,
2102 ReadView const& view,
2103 beast::Journal const& j)
2104{
2105 static auto const fieldChanged = [](auto const& before, auto const& after, auto const& field) {
2106 bool const beforeField = before->isFieldPresent(field);
2107 bool const afterField = after->isFieldPresent(field);
2108 return beforeField != afterField || (afterField && before->at(field) != after->at(field));
2109 };
2110 for (auto const& slePair : changedEntries_)
2111 {
2112 auto const& before = slePair.first;
2113 auto const& after = slePair.second;
2114 auto const type = after->getType();
2115 bool bad = false;
2116 [[maybe_unused]] bool enforce = false;
2117 switch (type)
2118 {
2119 case ltLOAN_BROKER:
2120 /*
2121 * We check this invariant regardless of lending protocol
2122 * amendment status, allowing for detection and logging of
2123 * potential issues even when the amendment is disabled.
2124 */
2125 enforce = view.rules().enabled(featureLendingProtocol);
2126 bad = fieldChanged(before, after, sfLedgerEntryType) || fieldChanged(before, after, sfLedgerIndex) ||
2127 fieldChanged(before, after, sfSequence) || fieldChanged(before, after, sfOwnerNode) ||
2128 fieldChanged(before, after, sfVaultNode) || fieldChanged(before, after, sfVaultID) ||
2129 fieldChanged(before, after, sfAccount) || fieldChanged(before, after, sfOwner) ||
2130 fieldChanged(before, after, sfManagementFeeRate) ||
2131 fieldChanged(before, after, sfCoverRateMinimum) ||
2132 fieldChanged(before, after, sfCoverRateLiquidation);
2133 break;
2134 case ltLOAN:
2135 /*
2136 * We check this invariant regardless of lending protocol
2137 * amendment status, allowing for detection and logging of
2138 * potential issues even when the amendment is disabled.
2139 */
2140 enforce = view.rules().enabled(featureLendingProtocol);
2141 bad = fieldChanged(before, after, sfLedgerEntryType) || fieldChanged(before, after, sfLedgerIndex) ||
2142 fieldChanged(before, after, sfSequence) || fieldChanged(before, after, sfOwnerNode) ||
2143 fieldChanged(before, after, sfLoanBrokerNode) || fieldChanged(before, after, sfLoanBrokerID) ||
2144 fieldChanged(before, after, sfBorrower) || fieldChanged(before, after, sfLoanOriginationFee) ||
2145 fieldChanged(before, after, sfLoanServiceFee) || fieldChanged(before, after, sfLatePaymentFee) ||
2146 fieldChanged(before, after, sfClosePaymentFee) || fieldChanged(before, after, sfOverpaymentFee) ||
2147 fieldChanged(before, after, sfInterestRate) || fieldChanged(before, after, sfLateInterestRate) ||
2148 fieldChanged(before, after, sfCloseInterestRate) ||
2149 fieldChanged(before, after, sfOverpaymentInterestRate) ||
2150 fieldChanged(before, after, sfStartDate) || fieldChanged(before, after, sfPaymentInterval) ||
2151 fieldChanged(before, after, sfGracePeriod) || fieldChanged(before, after, sfLoanScale);
2152 break;
2153 default:
2154 /*
2155 * We check this invariant regardless of lending protocol
2156 * amendment status, allowing for detection and logging of
2157 * potential issues even when the amendment is disabled.
2158 *
2159 * We use the lending protocol as a gate, even though
2160 * all transactions are affected because that's when it
2161 * was added.
2162 */
2163 enforce = view.rules().enabled(featureLendingProtocol);
2164 bad = fieldChanged(before, after, sfLedgerEntryType) || fieldChanged(before, after, sfLedgerIndex);
2165 }
2166 XRPL_ASSERT(
2167 !bad || enforce,
2168 "xrpl::NoModifiedUnmodifiableFields::finalize : no bad "
2169 "changes or enforce invariant");
2170 if (bad)
2171 {
2172 JLOG(j.fatal()) << "Invariant failed: changed an unchangeable field for " << tx.getTransactionID();
2173 if (enforce)
2174 return false;
2175 }
2176 }
2177 return true;
2178}
2179
2180//------------------------------------------------------------------------------
2181
2182void
2184 bool isDelete,
2185 std::shared_ptr<SLE const> const& before,
2187{
2188 if (after)
2189 {
2190 if (after->getType() == ltLOAN_BROKER)
2191 {
2192 auto& broker = brokers_[after->key()];
2193 broker.brokerBefore = before;
2194 broker.brokerAfter = after;
2195 }
2196 else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID))
2197 {
2198 auto const& loanBrokerID = after->at(sfLoanBrokerID);
2199 // create an entry if one doesn't already exist
2200 brokers_.emplace(loanBrokerID, BrokerInfo{});
2201 }
2202 else if (after->getType() == ltRIPPLE_STATE)
2203 {
2204 lines_.emplace_back(after);
2205 }
2206 else if (after->getType() == ltMPTOKEN)
2207 {
2208 mpts_.emplace_back(after);
2209 }
2210 }
2211}
2212
2213bool
2215{
2216 auto const next = dir->at(~sfIndexNext);
2217 auto const prev = dir->at(~sfIndexPrevious);
2218 if ((prev && *prev) || (next && *next))
2219 {
2220 JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
2221 "OwnerCount has multiple directory pages";
2222 return false;
2223 }
2224 auto indexes = dir->getFieldV256(sfIndexes);
2225 if (indexes.size() > 1)
2226 {
2227 JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
2228 "OwnerCount has multiple indexes in the Directory root";
2229 return false;
2230 }
2231 if (indexes.size() == 1)
2232 {
2233 auto const index = indexes.value().front();
2234 auto const sle = view.read(keylet::unchecked(index));
2235 if (!sle)
2236 {
2237 JLOG(j.fatal()) << "Invariant failed: Loan Broker directory corrupt";
2238 return false;
2239 }
2240 if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN)
2241 {
2242 JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
2243 "OwnerCount has an unexpected entry in the directory";
2244 return false;
2245 }
2246 }
2247
2248 return true;
2249}
2250
2251bool
2252ValidLoanBroker::finalize(STTx const& tx, TER const, XRPAmount const, ReadView const& view, beast::Journal const& j)
2253{
2254 // Loan Brokers will not exist on ledger if the Lending Protocol amendment
2255 // is not enabled, so there's no need to check it.
2256
2257 for (auto const& line : lines_)
2258 {
2259 for (auto const& field : {&sfLowLimit, &sfHighLimit})
2260 {
2261 auto const account = view.read(keylet::account(line->at(*field).getIssuer()));
2262 // This Invariant doesn't know about the rules for Trust Lines, so
2263 // if the account is missing, don't treat it as an error. This
2264 // loop is only concerned with finding Broker pseudo-accounts
2265 if (account && account->isFieldPresent(sfLoanBrokerID))
2266 {
2267 auto const& loanBrokerID = account->at(sfLoanBrokerID);
2268 // create an entry if one doesn't already exist
2269 brokers_.emplace(loanBrokerID, BrokerInfo{});
2270 }
2271 }
2272 }
2273 for (auto const& mpt : mpts_)
2274 {
2275 auto const account = view.read(keylet::account(mpt->at(sfAccount)));
2276 // This Invariant doesn't know about the rules for MPTokens, so
2277 // if the account is missing, don't treat is as an error. This
2278 // loop is only concerned with finding Broker pseudo-accounts
2279 if (account && account->isFieldPresent(sfLoanBrokerID))
2280 {
2281 auto const& loanBrokerID = account->at(sfLoanBrokerID);
2282 // create an entry if one doesn't already exist
2283 brokers_.emplace(loanBrokerID, BrokerInfo{});
2284 }
2285 }
2286
2287 for (auto const& [brokerID, broker] : brokers_)
2288 {
2289 auto const& after = broker.brokerAfter ? broker.brokerAfter : view.read(keylet::loanbroker(brokerID));
2290
2291 if (!after)
2292 {
2293 JLOG(j.fatal()) << "Invariant failed: Loan Broker missing";
2294 return false;
2295 }
2296
2297 auto const& before = broker.brokerBefore;
2298
2299 // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants
2300 // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most
2301 // one node (the root), which will only hold entries for `RippleState`
2302 // or `MPToken` objects.
2303 if (after->at(sfOwnerCount) == 0)
2304 {
2305 auto const dir = view.read(keylet::ownerDir(after->at(sfAccount)));
2306 if (dir)
2307 {
2308 if (!goodZeroDirectory(view, dir, j))
2309 {
2310 return false;
2311 }
2312 }
2313 }
2314 if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence))
2315 {
2316 JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number "
2317 "decreased";
2318 return false;
2319 }
2320 if (after->at(sfDebtTotal) < 0)
2321 {
2322 JLOG(j.fatal()) << "Invariant failed: Loan Broker debt total is negative";
2323 return false;
2324 }
2325 if (after->at(sfCoverAvailable) < 0)
2326 {
2327 JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available is negative";
2328 return false;
2329 }
2330 auto const vault = view.read(keylet::vault(after->at(sfVaultID)));
2331 if (!vault)
2332 {
2333 JLOG(j.fatal()) << "Invariant failed: Loan Broker vault ID is invalid";
2334 return false;
2335 }
2336 auto const& vaultAsset = vault->at(sfAsset);
2337 if (after->at(sfCoverAvailable) < accountHolds(
2338 view,
2339 after->at(sfAccount),
2340 vaultAsset,
2343 j))
2344 {
2345 JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available "
2346 "is less than pseudo-account asset balance";
2347 return false;
2348 }
2349 }
2350 return true;
2351}
2352
2353//------------------------------------------------------------------------------
2354
2355void
2357{
2358 if (after && after->getType() == ltLOAN)
2359 {
2360 loans_.emplace_back(before, after);
2361 }
2362}
2363
2364bool
2365ValidLoan::finalize(STTx const& tx, TER const, XRPAmount const, ReadView const& view, beast::Journal const& j)
2366{
2367 // Loans will not exist on ledger if the Lending Protocol amendment
2368 // is not enabled, so there's no need to check it.
2369
2370 for (auto const& [before, after] : loans_)
2371 {
2372 // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
2373 // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off
2374 if (after->at(sfPaymentRemaining) == 0 &&
2375 (after->at(sfTotalValueOutstanding) != beast::zero || after->at(sfPrincipalOutstanding) != beast::zero ||
2376 after->at(sfManagementFeeOutstanding) != beast::zero))
2377 {
2378 JLOG(j.fatal()) << "Invariant failed: Loan with zero payments "
2379 "remaining has not been paid off";
2380 return false;
2381 }
2382 // If `Loan.PaymentRemaining != 0` then the loan MUST NOT be fully paid
2383 // off
2384 if (after->at(sfPaymentRemaining) != 0 && after->at(sfTotalValueOutstanding) == beast::zero &&
2385 after->at(sfPrincipalOutstanding) == beast::zero && after->at(sfManagementFeeOutstanding) == beast::zero)
2386 {
2387 JLOG(j.fatal()) << "Invariant failed: Loan with zero payments "
2388 "remaining has not been paid off";
2389 return false;
2390 }
2391 if (before && (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment)))
2392 {
2393 JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed";
2394 return false;
2395 }
2396 // Must not be negative - STNumber
2397 for (auto const field :
2398 {&sfLoanServiceFee,
2399 &sfLatePaymentFee,
2400 &sfClosePaymentFee,
2401 &sfPrincipalOutstanding,
2402 &sfTotalValueOutstanding,
2403 &sfManagementFeeOutstanding})
2404 {
2405 if (after->at(*field) < 0)
2406 {
2407 JLOG(j.fatal()) << "Invariant failed: " << field->getName() << " is negative ";
2408 return false;
2409 }
2410 }
2411 // Must be positive - STNumber
2412 for (auto const field : {
2413 &sfPeriodicPayment,
2414 })
2415 {
2416 if (after->at(*field) <= 0)
2417 {
2418 JLOG(j.fatal()) << "Invariant failed: " << field->getName() << " is zero or negative ";
2419 return false;
2420 }
2421 }
2422 }
2423 return true;
2424}
2425
2428{
2429 XRPL_ASSERT(from.getType() == ltVAULT, "ValidVault::Vault::make : from Vault object");
2430
2431 ValidVault::Vault self;
2432 self.key = from.key();
2433 self.asset = from.at(sfAsset);
2434 self.pseudoId = from.getAccountID(sfAccount);
2435 self.owner = from.at(sfOwner);
2436 self.shareMPTID = from.getFieldH192(sfShareMPTID);
2437 self.assetsTotal = from.at(sfAssetsTotal);
2438 self.assetsAvailable = from.at(sfAssetsAvailable);
2439 self.assetsMaximum = from.at(sfAssetsMaximum);
2440 self.lossUnrealized = from.at(sfLossUnrealized);
2441 return self;
2442}
2443
2446{
2447 XRPL_ASSERT(from.getType() == ltMPTOKEN_ISSUANCE, "ValidVault::Shares::make : from MPTokenIssuance object");
2448
2449 ValidVault::Shares self;
2450 self.share = MPTIssue(makeMptID(from.getFieldU32(sfSequence), from.getAccountID(sfIssuer)));
2451 self.sharesTotal = from.at(sfOutstandingAmount);
2452 self.sharesMaximum = from[~sfMaximumAmount].value_or(maxMPTokenAmount);
2453 return self;
2454}
2455
2456void
2458{
2459 // If `before` is empty, this means an object is being created, in which
2460 // case `isDelete` must be false. Otherwise `before` and `after` are set and
2461 // `isDelete` indicates whether an object is being deleted or modified.
2462 XRPL_ASSERT(
2463 after != nullptr && (before != nullptr || !isDelete),
2464 "xrpl::ValidVault::visitEntry : some object is available");
2465
2466 // Number balanceDelta will capture the difference (delta) between "before"
2467 // state (zero if created) and "after" state (zero if destroyed), so the
2468 // invariants can validate that the change in account balances matches the
2469 // change in vault balances, stored to deltas_ at the end of this function.
2470 Number balanceDelta{};
2471
2472 std::int8_t sign = 0;
2473 if (before)
2474 {
2475 switch (before->getType())
2476 {
2477 case ltVAULT:
2478 beforeVault_.push_back(Vault::make(*before));
2479 break;
2480 case ltMPTOKEN_ISSUANCE:
2481 // At this moment we have no way of telling if this object holds
2482 // vault shares or something else. Save it for finalize.
2483 beforeMPTs_.push_back(Shares::make(*before));
2484 balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfOutstandingAmount));
2485 sign = 1;
2486 break;
2487 case ltMPTOKEN:
2488 balanceDelta = static_cast<std::int64_t>(before->getFieldU64(sfMPTAmount));
2489 sign = -1;
2490 break;
2491 case ltACCOUNT_ROOT:
2492 case ltRIPPLE_STATE:
2493 balanceDelta = before->getFieldAmount(sfBalance);
2494 sign = -1;
2495 break;
2496 default:;
2497 }
2498 }
2499
2500 if (!isDelete && after)
2501 {
2502 switch (after->getType())
2503 {
2504 case ltVAULT:
2505 afterVault_.push_back(Vault::make(*after));
2506 break;
2507 case ltMPTOKEN_ISSUANCE:
2508 // At this moment we have no way of telling if this object holds
2509 // vault shares or something else. Save it for finalize.
2510 afterMPTs_.push_back(Shares::make(*after));
2511 balanceDelta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfOutstandingAmount)));
2512 sign = 1;
2513 break;
2514 case ltMPTOKEN:
2515 balanceDelta -= Number(static_cast<std::int64_t>(after->getFieldU64(sfMPTAmount)));
2516 sign = -1;
2517 break;
2518 case ltACCOUNT_ROOT:
2519 case ltRIPPLE_STATE:
2520 balanceDelta -= Number(after->getFieldAmount(sfBalance));
2521 sign = -1;
2522 break;
2523 default:;
2524 }
2525 }
2526
2527 uint256 const key = (before ? before->key() : after->key());
2528 // Append to deltas if sign is non-zero, i.e. an object of an interesting
2529 // type has been updated. A transaction may update an object even when
2530 // its balance has not changed, e.g. transaction fee equals the amount
2531 // transferred to the account. We intentionally do not compare balanceDelta
2532 // against zero, to avoid missing such updates.
2533 if (sign != 0)
2534 deltas_[key] = balanceDelta * sign;
2535}
2536
2537bool
2538ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadView const& view, beast::Journal const& j)
2539{
2540 bool const enforce = view.rules().enabled(featureSingleAssetVault);
2541
2542 if (!isTesSuccess(ret))
2543 return true; // Do not perform checks
2544
2545 if (afterVault_.empty() && beforeVault_.empty())
2546 {
2548 {
2549 JLOG(j.fatal()) << //
2550 "Invariant failed: vault operation succeeded without modifying "
2551 "a vault";
2552 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault noop invariant");
2553 return !enforce;
2554 }
2555
2556 return true; // Not a vault operation
2557 }
2559 {
2560 JLOG(j.fatal()) << //
2561 "Invariant failed: vault updated by a wrong transaction type";
2562 XRPL_ASSERT(
2563 enforce,
2564 "xrpl::ValidVault::finalize : illegal vault transaction "
2565 "invariant");
2566 return !enforce; // Also not a vault operation
2567 }
2568
2569 if (beforeVault_.size() > 1 || afterVault_.size() > 1)
2570 {
2571 JLOG(j.fatal()) << //
2572 "Invariant failed: vault operation updated more than single vault";
2573 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : single vault invariant");
2574 return !enforce; // That's all we can do here
2575 }
2576
2577 auto const txnType = tx.getTxnType();
2578
2579 // We do special handling for ttVAULT_DELETE first, because it's the only
2580 // vault-modifying transaction without an "after" state of the vault
2581 if (afterVault_.empty())
2582 {
2583 if (txnType != ttVAULT_DELETE)
2584 {
2585 JLOG(j.fatal()) << //
2586 "Invariant failed: vault deleted by a wrong transaction type";
2587 XRPL_ASSERT(
2588 enforce,
2589 "xrpl::ValidVault::finalize : illegal vault deletion "
2590 "invariant");
2591 return !enforce; // That's all we can do here
2592 }
2593
2594 // Note, if afterVault_ is empty then we know that beforeVault_ is not
2595 // empty, as enforced at the top of this function
2596 auto const& beforeVault = beforeVault_[0];
2597
2598 // At this moment we only know a vault is being deleted and there
2599 // might be some MPTokenIssuance objects which are deleted in the
2600 // same transaction. Find the one matching this vault.
2601 auto const deletedShares = [&]() -> std::optional<Shares> {
2602 for (auto const& e : beforeMPTs_)
2603 {
2604 if (e.share.getMptID() == beforeVault.shareMPTID)
2605 return std::move(e);
2606 }
2607 return std::nullopt;
2608 }();
2609
2610 if (!deletedShares)
2611 {
2612 JLOG(j.fatal()) << "Invariant failed: deleted vault must also "
2613 "delete shares";
2614 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares deletion invariant");
2615 return !enforce; // That's all we can do here
2616 }
2617
2618 bool result = true;
2619 if (deletedShares->sharesTotal != 0)
2620 {
2621 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
2622 "shares outstanding";
2623 result = false;
2624 }
2625 if (beforeVault.assetsTotal != zero)
2626 {
2627 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
2628 "assets outstanding";
2629 result = false;
2630 }
2631 if (beforeVault.assetsAvailable != zero)
2632 {
2633 JLOG(j.fatal()) << "Invariant failed: deleted vault must have no "
2634 "assets available";
2635 result = false;
2636 }
2637
2638 return result;
2639 }
2640 else if (txnType == ttVAULT_DELETE)
2641 {
2642 JLOG(j.fatal()) << "Invariant failed: vault deletion succeeded without "
2643 "deleting a vault";
2644 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault deletion invariant");
2645 return !enforce; // That's all we can do here
2646 }
2647
2648 // Note, `afterVault_.empty()` is handled above
2649 auto const& afterVault = afterVault_[0];
2650 XRPL_ASSERT(
2651 beforeVault_.empty() || beforeVault_[0].key == afterVault.key,
2652 "xrpl::ValidVault::finalize : single vault operation");
2653
2654 auto const updatedShares = [&]() -> std::optional<Shares> {
2655 // At this moment we only know that a vault is being updated and there
2656 // might be some MPTokenIssuance objects which are also updated in the
2657 // same transaction. Find the one matching the shares to this vault.
2658 // Note, we expect updatedMPTs collection to be extremely small. For
2659 // such collections linear search is faster than lookup.
2660 for (auto const& e : afterMPTs_)
2661 {
2662 if (e.share.getMptID() == afterVault.shareMPTID)
2663 return e;
2664 }
2665
2666 auto const sleShares = view.read(keylet::mptIssuance(afterVault.shareMPTID));
2667
2668 return sleShares ? std::optional<Shares>(Shares::make(*sleShares)) : std::nullopt;
2669 }();
2670
2671 bool result = true;
2672
2673 // Universal transaction checks
2674 if (!beforeVault_.empty())
2675 {
2676 auto const& beforeVault = beforeVault_[0];
2677 if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId ||
2678 afterVault.shareMPTID != beforeVault.shareMPTID)
2679 {
2680 JLOG(j.fatal()) << "Invariant failed: violation of vault immutable data";
2681 result = false;
2682 }
2683 }
2684
2685 if (!updatedShares)
2686 {
2687 JLOG(j.fatal()) << "Invariant failed: updated vault must have shares";
2688 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault has shares invariant");
2689 return !enforce; // That's all we can do here
2690 }
2691
2692 if (updatedShares->sharesTotal == 0)
2693 {
2694 if (afterVault.assetsTotal != zero)
2695 {
2696 JLOG(j.fatal()) << "Invariant failed: updated zero sized "
2697 "vault must have no assets outstanding";
2698 result = false;
2699 }
2700 if (afterVault.assetsAvailable != zero)
2701 {
2702 JLOG(j.fatal()) << "Invariant failed: updated zero sized "
2703 "vault must have no assets available";
2704 result = false;
2705 }
2706 }
2707 else if (updatedShares->sharesTotal > updatedShares->sharesMaximum)
2708 {
2709 JLOG(j.fatal()) //
2710 << "Invariant failed: updated shares must not exceed maximum " << updatedShares->sharesMaximum;
2711 result = false;
2712 }
2713
2714 if (afterVault.assetsAvailable < zero)
2715 {
2716 JLOG(j.fatal()) << "Invariant failed: assets available must be positive";
2717 result = false;
2718 }
2719
2720 if (afterVault.assetsAvailable > afterVault.assetsTotal)
2721 {
2722 JLOG(j.fatal()) << "Invariant failed: assets available must "
2723 "not be greater than assets outstanding";
2724 result = false;
2725 }
2726 else if (afterVault.lossUnrealized > afterVault.assetsTotal - afterVault.assetsAvailable)
2727 {
2728 JLOG(j.fatal()) //
2729 << "Invariant failed: loss unrealized must not exceed "
2730 "the difference between assets outstanding and available";
2731 result = false;
2732 }
2733
2734 if (afterVault.assetsTotal < zero)
2735 {
2736 JLOG(j.fatal()) << "Invariant failed: assets outstanding must be positive";
2737 result = false;
2738 }
2739
2740 if (afterVault.assetsMaximum < zero)
2741 {
2742 JLOG(j.fatal()) << "Invariant failed: assets maximum must be positive";
2743 result = false;
2744 }
2745
2746 // Thanks to this check we can simply do `assert(!beforeVault_.empty()` when
2747 // enforcing invariants on transaction types other than ttVAULT_CREATE
2748 if (beforeVault_.empty() && txnType != ttVAULT_CREATE)
2749 {
2750 JLOG(j.fatal()) << //
2751 "Invariant failed: vault created by a wrong transaction type";
2752 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault creation invariant");
2753 return !enforce; // That's all we can do here
2754 }
2755
2756 if (!beforeVault_.empty() && afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
2757 txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
2758 {
2759 JLOG(j.fatal()) << //
2760 "Invariant failed: vault transaction must not change loss "
2761 "unrealized";
2762 result = false;
2763 }
2764
2765 auto const beforeShares = [&]() -> std::optional<Shares> {
2766 if (beforeVault_.empty())
2767 return std::nullopt;
2768 auto const& beforeVault = beforeVault_[0];
2769
2770 for (auto const& e : beforeMPTs_)
2771 {
2772 if (e.share.getMptID() == beforeVault.shareMPTID)
2773 return std::move(e);
2774 }
2775 return std::nullopt;
2776 }();
2777
2778 if (!beforeShares &&
2779 (tx.getTxnType() == ttVAULT_DEPOSIT || //
2780 tx.getTxnType() == ttVAULT_WITHDRAW || //
2781 tx.getTxnType() == ttVAULT_CLAWBACK))
2782 {
2783 JLOG(j.fatal()) << "Invariant failed: vault operation succeeded "
2784 "without updating shares";
2785 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : shares noop invariant");
2786 return !enforce; // That's all we can do here
2787 }
2788
2789 auto const& vaultAsset = afterVault.asset;
2790 auto const deltaAssets = [&](AccountID const& id) -> std::optional<Number> {
2791 auto const get = //
2792 [&](auto const& it, std::int8_t sign = 1) -> std::optional<Number> {
2793 if (it == deltas_.end())
2794 return std::nullopt;
2795
2796 return it->second * sign;
2797 };
2798
2799 return std::visit(
2800 [&]<typename TIss>(TIss const& issue) {
2801 if constexpr (std::is_same_v<TIss, Issue>)
2802 {
2803 if (isXRP(issue))
2804 return get(deltas_.find(keylet::account(id).key));
2805 return get(deltas_.find(keylet::line(id, issue).key), id > issue.getIssuer() ? -1 : 1);
2806 }
2807 else if constexpr (std::is_same_v<TIss, MPTIssue>)
2808 {
2809 return get(deltas_.find(keylet::mptoken(issue.getMptID(), id).key));
2810 }
2811 },
2812 vaultAsset.value());
2813 };
2814 auto const deltaAssetsTxAccount = [&]() -> std::optional<Number> {
2815 auto ret = deltaAssets(tx[sfAccount]);
2816 // Nothing returned or not XRP transaction
2817 if (!ret.has_value() || !vaultAsset.native())
2818 return ret;
2819
2820 // Delegated transaction; no need to compensate for fees
2821 if (auto const delegate = tx[~sfDelegate]; delegate.has_value() && *delegate != tx[sfAccount])
2822 return ret;
2823
2824 *ret += fee.drops();
2825 if (*ret == zero)
2826 return std::nullopt;
2827
2828 return ret;
2829 };
2830 auto const deltaShares = [&](AccountID const& id) -> std::optional<Number> {
2831 auto const it = [&]() {
2832 if (id == afterVault.pseudoId)
2833 return deltas_.find(keylet::mptIssuance(afterVault.shareMPTID).key);
2834 return deltas_.find(keylet::mptoken(afterVault.shareMPTID, id).key);
2835 }();
2836
2837 return it != deltas_.end() ? std::optional<Number>(it->second) : std::nullopt;
2838 };
2839
2840 auto const vaultHoldsNoAssets = [&](Vault const& vault) {
2841 return vault.assetsAvailable == 0 && vault.assetsTotal == 0;
2842 };
2843
2844 // Technically this does not need to be a lambda, but it's more
2845 // convenient thanks to early "return false"; the not-so-nice
2846 // alternatives are several layers of nested if/else or more complex
2847 // (i.e. brittle) if statements.
2848 result &=
2849 [&]() {
2850 switch (txnType)
2851 {
2852 case ttVAULT_CREATE: {
2853 bool result = true;
2854
2855 if (!beforeVault_.empty())
2856 {
2857 JLOG(j.fatal()) //
2858 << "Invariant failed: create operation must not have "
2859 "updated a vault";
2860 result = false;
2861 }
2862
2863 if (afterVault.assetsAvailable != zero || afterVault.assetsTotal != zero ||
2864 afterVault.lossUnrealized != zero || updatedShares->sharesTotal != 0)
2865 {
2866 JLOG(j.fatal()) //
2867 << "Invariant failed: created vault must be empty";
2868 result = false;
2869 }
2870
2871 if (afterVault.pseudoId != updatedShares->share.getIssuer())
2872 {
2873 JLOG(j.fatal()) //
2874 << "Invariant failed: shares issuer and vault "
2875 "pseudo-account must be the same";
2876 result = false;
2877 }
2878
2879 auto const sleSharesIssuer = view.read(keylet::account(updatedShares->share.getIssuer()));
2880 if (!sleSharesIssuer)
2881 {
2882 JLOG(j.fatal()) //
2883 << "Invariant failed: shares issuer must exist";
2884 return false;
2885 }
2886
2887 if (!isPseudoAccount(sleSharesIssuer))
2888 {
2889 JLOG(j.fatal()) //
2890 << "Invariant failed: shares issuer must be a "
2891 "pseudo-account";
2892 result = false;
2893 }
2894
2895 if (auto const vaultId = (*sleSharesIssuer)[~sfVaultID]; !vaultId || *vaultId != afterVault.key)
2896 {
2897 JLOG(j.fatal()) //
2898 << "Invariant failed: shares issuer pseudo-account "
2899 "must point back to the vault";
2900 result = false;
2901 }
2902
2903 return result;
2904 }
2905 case ttVAULT_SET: {
2906 bool result = true;
2907
2908 XRPL_ASSERT(!beforeVault_.empty(), "xrpl::ValidVault::finalize : set updated a vault");
2909 auto const& beforeVault = beforeVault_[0];
2910
2911 auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
2912 if (vaultDeltaAssets)
2913 {
2914 JLOG(j.fatal()) << //
2915 "Invariant failed: set must not change vault balance";
2916 result = false;
2917 }
2918
2919 if (beforeVault.assetsTotal != afterVault.assetsTotal)
2920 {
2921 JLOG(j.fatal()) << //
2922 "Invariant failed: set must not change assets "
2923 "outstanding";
2924 result = false;
2925 }
2926
2927 if (afterVault.assetsMaximum > zero && afterVault.assetsTotal > afterVault.assetsMaximum)
2928 {
2929 JLOG(j.fatal()) << //
2930 "Invariant failed: set assets outstanding must not "
2931 "exceed assets maximum";
2932 result = false;
2933 }
2934
2935 if (beforeVault.assetsAvailable != afterVault.assetsAvailable)
2936 {
2937 JLOG(j.fatal()) << //
2938 "Invariant failed: set must not change assets "
2939 "available";
2940 result = false;
2941 }
2942
2943 if (beforeShares && updatedShares && beforeShares->sharesTotal != updatedShares->sharesTotal)
2944 {
2945 JLOG(j.fatal()) << //
2946 "Invariant failed: set must not change shares "
2947 "outstanding";
2948 result = false;
2949 }
2950
2951 return result;
2952 }
2953 case ttVAULT_DEPOSIT: {
2954 bool result = true;
2955
2956 XRPL_ASSERT(!beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault");
2957 auto const& beforeVault = beforeVault_[0];
2958
2959 auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
2960
2961 if (!vaultDeltaAssets)
2962 {
2963 JLOG(j.fatal()) << //
2964 "Invariant failed: deposit must change vault balance";
2965 return false; // That's all we can do
2966 }
2967
2968 if (*vaultDeltaAssets > tx[sfAmount])
2969 {
2970 JLOG(j.fatal()) << //
2971 "Invariant failed: deposit must not change vault "
2972 "balance by more than deposited amount";
2973 result = false;
2974 }
2975
2976 if (*vaultDeltaAssets <= zero)
2977 {
2978 JLOG(j.fatal()) << //
2979 "Invariant failed: deposit must increase vault balance";
2980 result = false;
2981 }
2982
2983 // Any payments (including deposits) made by the issuer
2984 // do not change their balance, but create funds instead.
2985 bool const issuerDeposit = [&]() -> bool {
2986 if (vaultAsset.native())
2987 return false;
2988 return tx[sfAccount] == vaultAsset.getIssuer();
2989 }();
2990
2991 if (!issuerDeposit)
2992 {
2993 auto const accountDeltaAssets = deltaAssetsTxAccount();
2994 if (!accountDeltaAssets)
2995 {
2996 JLOG(j.fatal()) << //
2997 "Invariant failed: deposit must change depositor "
2998 "balance";
2999 return false;
3000 }
3001
3002 if (*accountDeltaAssets >= zero)
3003 {
3004 JLOG(j.fatal()) << //
3005 "Invariant failed: deposit must decrease depositor "
3006 "balance";
3007 result = false;
3008 }
3009
3010 if (*accountDeltaAssets * -1 != *vaultDeltaAssets)
3011 {
3012 JLOG(j.fatal()) << //
3013 "Invariant failed: deposit must change vault and "
3014 "depositor balance by equal amount";
3015 result = false;
3016 }
3017 }
3018
3019 if (afterVault.assetsMaximum > zero && afterVault.assetsTotal > afterVault.assetsMaximum)
3020 {
3021 JLOG(j.fatal()) << //
3022 "Invariant failed: deposit assets outstanding must not "
3023 "exceed assets maximum";
3024 result = false;
3025 }
3026
3027 auto const accountDeltaShares = deltaShares(tx[sfAccount]);
3028 if (!accountDeltaShares)
3029 {
3030 JLOG(j.fatal()) << //
3031 "Invariant failed: deposit must change depositor "
3032 "shares";
3033 return false; // That's all we can do
3034 }
3035
3036 if (*accountDeltaShares <= zero)
3037 {
3038 JLOG(j.fatal()) << //
3039 "Invariant failed: deposit must increase depositor "
3040 "shares";
3041 result = false;
3042 }
3043
3044 auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
3045 if (!vaultDeltaShares || *vaultDeltaShares == zero)
3046 {
3047 JLOG(j.fatal()) << //
3048 "Invariant failed: deposit must change vault shares";
3049 return false; // That's all we can do
3050 }
3051
3052 if (*vaultDeltaShares * -1 != *accountDeltaShares)
3053 {
3054 JLOG(j.fatal()) << //
3055 "Invariant failed: deposit must change depositor and "
3056 "vault shares by equal amount";
3057 result = false;
3058 }
3059
3060 if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
3061 {
3062 JLOG(j.fatal()) << "Invariant failed: deposit and assets "
3063 "outstanding must add up";
3064 result = false;
3065 }
3066 if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
3067 {
3068 JLOG(j.fatal()) << "Invariant failed: deposit and assets "
3069 "available must add up";
3070 result = false;
3071 }
3072
3073 return result;
3074 }
3075 case ttVAULT_WITHDRAW: {
3076 bool result = true;
3077
3078 XRPL_ASSERT(
3079 !beforeVault_.empty(),
3080 "xrpl::ValidVault::finalize : withdrawal updated a "
3081 "vault");
3082 auto const& beforeVault = beforeVault_[0];
3083
3084 auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
3085
3086 if (!vaultDeltaAssets)
3087 {
3088 JLOG(j.fatal()) << "Invariant failed: withdrawal must "
3089 "change vault balance";
3090 return false; // That's all we can do
3091 }
3092
3093 if (*vaultDeltaAssets >= zero)
3094 {
3095 JLOG(j.fatal()) << "Invariant failed: withdrawal must "
3096 "decrease vault balance";
3097 result = false;
3098 }
3099
3100 // Any payments (including withdrawal) going to the issuer
3101 // do not change their balance, but destroy funds instead.
3102 bool const issuerWithdrawal = [&]() -> bool {
3103 if (vaultAsset.native())
3104 return false;
3105 auto const destination = tx[~sfDestination].value_or(tx[sfAccount]);
3106 return destination == vaultAsset.getIssuer();
3107 }();
3108
3109 if (!issuerWithdrawal)
3110 {
3111 auto const accountDeltaAssets = deltaAssetsTxAccount();
3112 auto const otherAccountDelta = [&]() -> std::optional<Number> {
3113 if (auto const destination = tx[~sfDestination];
3114 destination && *destination != tx[sfAccount])
3115 return deltaAssets(*destination);
3116 return std::nullopt;
3117 }();
3118
3119 if (accountDeltaAssets.has_value() == otherAccountDelta.has_value())
3120 {
3121 JLOG(j.fatal()) << //
3122 "Invariant failed: withdrawal must change one "
3123 "destination balance";
3124 return false;
3125 }
3126
3127 auto const destinationDelta = //
3128 accountDeltaAssets ? *accountDeltaAssets : *otherAccountDelta;
3129
3130 if (destinationDelta <= zero)
3131 {
3132 JLOG(j.fatal()) << //
3133 "Invariant failed: withdrawal must increase "
3134 "destination balance";
3135 result = false;
3136 }
3137
3138 if (*vaultDeltaAssets * -1 != destinationDelta)
3139 {
3140 JLOG(j.fatal()) << //
3141 "Invariant failed: withdrawal must change vault "
3142 "and destination balance by equal amount";
3143 result = false;
3144 }
3145 }
3146
3147 auto const accountDeltaShares = deltaShares(tx[sfAccount]);
3148 if (!accountDeltaShares)
3149 {
3150 JLOG(j.fatal()) << //
3151 "Invariant failed: withdrawal must change depositor "
3152 "shares";
3153 return false;
3154 }
3155
3156 if (*accountDeltaShares >= zero)
3157 {
3158 JLOG(j.fatal()) << //
3159 "Invariant failed: withdrawal must decrease depositor "
3160 "shares";
3161 result = false;
3162 }
3163
3164 auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
3165 if (!vaultDeltaShares || *vaultDeltaShares == zero)
3166 {
3167 JLOG(j.fatal()) << //
3168 "Invariant failed: withdrawal must change vault shares";
3169 return false; // That's all we can do
3170 }
3171
3172 if (*vaultDeltaShares * -1 != *accountDeltaShares)
3173 {
3174 JLOG(j.fatal()) << //
3175 "Invariant failed: withdrawal must change depositor "
3176 "and vault shares by equal amount";
3177 result = false;
3178 }
3179
3180 // Note, vaultBalance is negative (see check above)
3181 if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
3182 {
3183 JLOG(j.fatal()) << "Invariant failed: withdrawal and "
3184 "assets outstanding must add up";
3185 result = false;
3186 }
3187
3188 if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
3189 {
3190 JLOG(j.fatal()) << "Invariant failed: withdrawal and "
3191 "assets available must add up";
3192 result = false;
3193 }
3194
3195 return result;
3196 }
3197 case ttVAULT_CLAWBACK: {
3198 bool result = true;
3199
3200 XRPL_ASSERT(!beforeVault_.empty(), "xrpl::ValidVault::finalize : clawback updated a vault");
3201 auto const& beforeVault = beforeVault_[0];
3202
3203 if (vaultAsset.native() || vaultAsset.getIssuer() != tx[sfAccount])
3204 {
3205 // The owner can use clawback to force-burn shares when the
3206 // vault is empty but there are outstanding shares
3207 if (!(beforeShares && beforeShares->sharesTotal > 0 && vaultHoldsNoAssets(beforeVault) &&
3208 beforeVault.owner == tx[sfAccount]))
3209 {
3210 JLOG(j.fatal()) << //
3211 "Invariant failed: clawback may only be performed "
3212 "by the asset issuer, or by the vault owner of an "
3213 "empty vault";
3214 return false; // That's all we can do
3215 }
3216 }
3217
3218 auto const vaultDeltaAssets = deltaAssets(afterVault.pseudoId);
3219 if (vaultDeltaAssets)
3220 {
3221 if (*vaultDeltaAssets >= zero)
3222 {
3223 JLOG(j.fatal()) << //
3224 "Invariant failed: clawback must decrease vault "
3225 "balance";
3226 result = false;
3227 }
3228
3229 if (beforeVault.assetsTotal + *vaultDeltaAssets != afterVault.assetsTotal)
3230 {
3231 JLOG(j.fatal()) << //
3232 "Invariant failed: clawback and assets outstanding "
3233 "must add up";
3234 result = false;
3235 }
3236
3237 if (beforeVault.assetsAvailable + *vaultDeltaAssets != afterVault.assetsAvailable)
3238 {
3239 JLOG(j.fatal()) << //
3240 "Invariant failed: clawback and assets available "
3241 "must add up";
3242 result = false;
3243 }
3244 }
3245 else if (!vaultHoldsNoAssets(beforeVault))
3246 {
3247 JLOG(j.fatal()) << //
3248 "Invariant failed: clawback must change vault balance";
3249 return false; // That's all we can do
3250 }
3251
3252 auto const accountDeltaShares = deltaShares(tx[sfHolder]);
3253 if (!accountDeltaShares)
3254 {
3255 JLOG(j.fatal()) << //
3256 "Invariant failed: clawback must change holder shares";
3257 return false; // That's all we can do
3258 }
3259
3260 if (*accountDeltaShares >= zero)
3261 {
3262 JLOG(j.fatal()) << //
3263 "Invariant failed: clawback must decrease holder "
3264 "shares";
3265 result = false;
3266 }
3267
3268 auto const vaultDeltaShares = deltaShares(afterVault.pseudoId);
3269 if (!vaultDeltaShares || *vaultDeltaShares == zero)
3270 {
3271 JLOG(j.fatal()) << //
3272 "Invariant failed: clawback must change vault shares";
3273 return false; // That's all we can do
3274 }
3275
3276 if (*vaultDeltaShares * -1 != *accountDeltaShares)
3277 {
3278 JLOG(j.fatal()) << //
3279 "Invariant failed: clawback must change holder and "
3280 "vault shares by equal amount";
3281 result = false;
3282 }
3283
3284 return result;
3285 }
3286
3287 case ttLOAN_SET:
3288 case ttLOAN_MANAGE:
3289 case ttLOAN_PAY: {
3290 // TBD
3291 return true;
3292 }
3293
3294 default:
3295 // LCOV_EXCL_START
3296 UNREACHABLE("xrpl::ValidVault::finalize : unknown transaction type");
3297 return false;
3298 // LCOV_EXCL_STOP
3299 }
3300 }();
3301
3302 if (!result)
3303 {
3304 // The comment at the top of this file starting with "assert(enforce)"
3305 // explains this assert.
3306 XRPL_ASSERT(enforce, "xrpl::ValidVault::finalize : vault invariants");
3307 return !enforce;
3308 }
3309
3310 return true;
3311}
3312
3313} // 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_
void visitEntry(bool, std::shared_ptr< SLE const > const &, std::shared_ptr< SLE const > const &)
std::vector< SleStatus > sleStatus_
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)