1#include <xrpl/tx/invariants/MPTInvariant.h>
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/base_uint.h>
5#include <xrpl/beast/utility/Journal.h>
6#include <xrpl/beast/utility/Zero.h>
7#include <xrpl/beast/utility/instrumentation.h>
8#include <xrpl/ledger/ReadView.h>
9#include <xrpl/ledger/View.h>
10#include <xrpl/ledger/helpers/AccountRootHelpers.h>
11#include <xrpl/ledger/helpers/MPTokenHelpers.h>
12#include <xrpl/protocol/AccountID.h>
13#include <xrpl/protocol/Feature.h>
14#include <xrpl/protocol/Indexes.h>
15#include <xrpl/protocol/LedgerFormats.h>
16#include <xrpl/protocol/MPTIssue.h>
17#include <xrpl/protocol/Protocol.h>
18#include <xrpl/protocol/Rules.h>
19#include <xrpl/protocol/SField.h>
20#include <xrpl/protocol/STLedgerEntry.h>
21#include <xrpl/protocol/STTx.h>
22#include <xrpl/protocol/TER.h>
23#include <xrpl/protocol/TxFormats.h>
24#include <xrpl/protocol/UintTypes.h>
25#include <xrpl/protocol/XRPAmount.h>
26#include <xrpl/tx/invariants/InvariantCheckPrivilege.h>
37constexpr auto kConfidentialMptTxTypes = std::to_array<TxType>({
38 ttCONFIDENTIAL_MPT_SEND,
39 ttCONFIDENTIAL_MPT_CONVERT,
40 ttCONFIDENTIAL_MPT_CONVERT_BACK,
41 ttCONFIDENTIAL_MPT_MERGE_INBOX,
42 ttCONFIDENTIAL_MPT_CLAWBACK,
49toSignedMPTAmount(std::uint64_t amount)
55addMPTAmountDelta(std::int64_t delta, std::uint64_t amount)
57 return delta + toSignedMPTAmount(amount);
61subtractMPTAmountDelta(std::int64_t delta, std::uint64_t amount)
63 return delta - toSignedMPTAmount(amount);
78 if (
after &&
after->getType() == ltMPTOKEN_ISSUANCE)
87 if (fix320Enabled &&
after->isFieldPresent(sfReferenceHolding))
90 else if (fix320Enabled)
93 bool const beforePresent = before->isFieldPresent(sfReferenceHolding);
94 bool const afterPresent =
after->isFieldPresent(sfReferenceHolding);
95 if (beforePresent != afterPresent ||
97 before->getFieldH256(sfReferenceHolding) !=
98 after->getFieldH256(sfReferenceHolding)))
124 if (fix320Enabled && isDelete &&
after &&
after->getType() == ltRIPPLE_STATE)
136 auto const& rules = view.
rules();
137 bool const mptV2Enabled = rules.
enabled(featureMPTokensV2);
145 if (rules.enabled(fixCleanup3_2_0))
147 bool invariantPasses =
true;
150 JLOG(j.
fatal()) <<
"Invariant failed: sfReferenceHolding was modified "
151 "on an existing MPTokenIssuance";
152 invariantPasses =
false;
156 JLOG(j.
fatal()) <<
"Invariant failed: sfReferenceHolding set on a new "
157 "MPTokenIssuance by a non-VaultCreate transaction";
158 invariantPasses =
false;
162 auto const isVaultPseudo = [&](
AccountID const& acct) {
164 return sle && sle->isFieldPresent(sfVaultID);
168 bool offending =
false;
169 if (sleHolding->getType() == ltMPTOKEN)
171 offending = isVaultPseudo(sleHolding->at(sfAccount));
175 auto const lowLimit = sleHolding->getFieldAmount(sfLowLimit);
176 auto const highLimit = sleHolding->getFieldAmount(sfHighLimit);
181 isVaultPseudo(lowLimit.getIssuer()) || isVaultPseudo(highLimit.getIssuer());
185 JLOG(j.
fatal()) <<
"Invariant failed: vault pseudo-account holding "
186 "deleted by a non-VaultDelete transaction";
187 invariantPasses =
false;
191 if (!invariantPasses)
198 bool const enforceCreatedByIssuer =
199 rules.enabled(featureSingleAssetVault) || rules.enabled(featureLendingProtocol);
202 JLOG(j.
fatal()) <<
"Invariant failed: MPToken created for the MPT issuer";
206 enforceCreatedByIssuer,
"xrpl::ValidMPTIssuance::finalize",
"no issuer MPToken");
207 if (enforceCreatedByIssuer)
216 JLOG(j.
fatal()) <<
"Invariant failed: transaction "
217 "succeeded without creating a MPT issuance";
221 JLOG(j.
fatal()) <<
"Invariant failed: transaction "
222 "succeeded while removing MPT issuances";
226 JLOG(j.
fatal()) <<
"Invariant failed: transaction "
227 "succeeded but created multiple issuances";
237 JLOG(j.
fatal()) <<
"Invariant failed: MPT issuance deletion "
238 "succeeded without removing a MPT issuance";
242 JLOG(j.
fatal()) <<
"Invariant failed: MPT issuance deletion "
243 "succeeded while creating MPT issuances";
247 JLOG(j.
fatal()) <<
"Invariant failed: MPT issuance deletion "
248 "succeeded but deleted multiple issuances";
254 bool const lendingProtocolEnabled = rules.enabled(featureLendingProtocol);
258 bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) &&
259 (rules.enabled(featureSingleAssetVault) || lendingProtocolEnabled);
266 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize "
267 "succeeded but created MPT issuances";
272 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize "
273 "succeeded but deleted issuances";
277 (txnType == ttAMM_WITHDRAW || txnType == ttAMM_CLAWBACK))
281 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize "
282 "submitted by issuer succeeded "
283 "but created bad number of mptokens";
293 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize succeeded "
294 "but created/deleted bad number of mptokens";
300 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize succeeded "
301 "but created/deleted bad number mptokens";
306 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize submitted by issuer "
307 "succeeded but created/deleted mptokens";
316 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize submitted by holder "
317 "succeeded but created/deleted bad number of mptokens";
330 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize "
331 "succeeded but created MPT issuances";
336 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize "
337 "succeeded but deleted issuances";
342 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize "
343 "succeeded but deleted MPTokens";
352 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize "
353 "succeeded but created bad number of mptokens";
356 if (submittedByIssuer)
358 JLOG(j.
fatal()) <<
"Invariant failed: MPT authorize submitted by issuer "
359 "succeeded but created mptokens";
369 if (txnType == ttESCROW_FINISH)
375 !enforceEscrowFinish,
"xrpl::ValidMPTIssuance::finalize",
"not escrow finish tx");
387 JLOG(j.
fatal()) <<
"Invariant failed: a MPT issuance was created";
391 JLOG(j.
fatal()) <<
"Invariant failed: a MPT issuance was deleted";
395 JLOG(j.
fatal()) <<
"Invariant failed: a MPToken was created";
399 JLOG(j.
fatal()) <<
"Invariant failed: a MPToken was deleted";
412 auto makeKey = [](
SLE const& sle) {
413 if (sle.getType() == ltMPTOKEN_ISSUANCE)
414 return makeMptID(sle[sfSequence], sle[sfIssuer]);
415 return sle[sfMPTokenIssuanceID];
418 auto update = [&](
SLE const& sle,
Order order) ->
bool {
419 auto const type = sle.
getType();
420 if (type == ltMPTOKEN_ISSUANCE)
422 auto const outstanding = sle[sfOutstandingAmount];
428 data_[makeKey(sle)].outstanding[
static_cast<std::size_t>(order)] = outstanding;
430 else if (type == ltMPTOKEN)
432 auto const mptAmt = sle[sfMPTAmount];
433 auto const lockedAmt = sle[~sfLockedAmount].value_or(0);
440 auto const res =
static_cast<std::int64_t>(mptAmt + lockedAmt);
444 data_[makeKey(sle)].mptAmount -= res;
448 data_[makeKey(sle)].mptAmount += res;
459 if (
after->getType() == ltMPTOKEN_ISSUANCE)
483 kConfidentialMptTxTypes.end())
488 bool const invariantPasses = !view.
rules().
enabled(featureMPTokensV2);
491 JLOG(j.
fatal()) <<
"Invariant failed: OutstandingAmount overflow";
492 return invariantPasses;
496 for (
auto const& [
id, data] :
data_)
501 bool const addOverflows =
502 (data.mptAmount > 0 && data.outstanding[kIBefore] > (signedMax - data.mptAmount)) ||
503 (data.mptAmount < 0 && data.outstanding[kIBefore] < (-signedMax - data.mptAmount));
505 data.outstanding[kIAfter] != (data.outstanding[kIBefore] + data.mptAmount))
507 JLOG(j.
fatal()) <<
"Invariant failed: invalid OutstandingAmount balance "
508 << data.outstanding[kIBefore] <<
" " << data.outstanding[kIAfter]
509 <<
" " << data.mptAmount;
510 return invariantPasses;
528 if (sle->getType() == ltMPTOKEN)
529 return sle->getFieldH192(sfMPTokenIssuanceID);
530 if (sle->getType() == ltMPTOKEN_ISSUANCE)
531 return makeMptID(sle->getFieldU32(sfSequence), sle->getAccountID(sfIssuer));
535 if (before && before->getType() == ltMPTOKEN)
537 uint192 const id = getMptID(before);
539 change.mptAmountDelta =
540 subtractMPTAmountDelta(change.mptAmountDelta, before->getFieldU64(sfMPTAmount));
545 bool const hasPublicBalance = before->getFieldU64(sfMPTAmount) > 0;
546 bool const hasEncryptedFields = before->isFieldPresent(sfConfidentialBalanceSpending) ||
547 before->isFieldPresent(sfConfidentialBalanceInbox) ||
548 before->isFieldPresent(sfIssuerEncryptedBalance) ||
549 before->isFieldPresent(sfAuditorEncryptedBalance);
551 if (hasPublicBalance || hasEncryptedFields)
552 changes_[id].deletedWithEncrypted =
true;
560 change.mptAmountDelta =
561 addMPTAmountDelta(change.mptAmountDelta,
after->getFieldU64(sfMPTAmount));
564 bool const hasIssuerBalance =
after->isFieldPresent(sfIssuerEncryptedBalance);
565 bool const hasHolderInbox =
after->isFieldPresent(sfConfidentialBalanceInbox);
566 bool const hasHolderSpending =
after->isFieldPresent(sfConfidentialBalanceSpending);
567 bool const hasAuditorBalance =
after->isFieldPresent(sfAuditorEncryptedBalance);
571 if (hasHolderInbox != hasHolderSpending || hasHolderInbox != hasIssuerBalance ||
572 (hasAuditorBalance && !hasIssuerBalance))
575 auto const confidentialBalanceFieldChanged = [&before, &
after](
auto const& field) {
576 auto const afterValue = (*after)[~field];
580 if (!before || before->getType() != ltMPTOKEN)
583 return (*before)[~field] != afterValue;
586 if (confidentialBalanceFieldChanged(sfConfidentialBalanceInbox) ||
587 confidentialBalanceFieldChanged(sfConfidentialBalanceSpending) ||
588 confidentialBalanceFieldChanged(sfIssuerEncryptedBalance) ||
589 confidentialBalanceFieldChanged(sfAuditorEncryptedBalance))
591 changes_[id].changesConfidentialFields =
true;
595 if (before && before->getType() == ltMPTOKEN_ISSUANCE)
597 uint192 const id = getMptID(before);
599 if (before->isFieldPresent(sfConfidentialOutstandingAmount))
601 change.coaDelta = subtractMPTAmountDelta(
602 change.coaDelta, before->getFieldU64(sfConfidentialOutstandingAmount));
604 change.outstandingDelta = subtractMPTAmountDelta(
605 change.outstandingDelta, before->getFieldU64(sfOutstandingAmount));
608 if (
after &&
after->getType() == ltMPTOKEN_ISSUANCE)
613 bool const hasCOA =
after->isFieldPresent(sfConfidentialOutstandingAmount);
614 std::uint64_t const coa = (*after)[~sfConfidentialOutstandingAmount].value_or(0);
618 change.coaDelta = addMPTAmountDelta(change.coaDelta, coa);
620 change.outstandingDelta = addMPTAmountDelta(change.outstandingDelta, oa);
621 change.issuance =
after;
625 change.badCOA =
true;
628 if (before &&
after && before->getType() == ltMPTOKEN &&
after->getType() == ltMPTOKEN)
633 auto const spendingBefore = (*before)[~sfConfidentialBalanceSpending];
634 auto const spendingAfter = (*after)[~sfConfidentialBalanceSpending];
635 auto const versionBefore = (*before)[~sfConfidentialBalanceVersion];
636 auto const versionAfter = (*after)[~sfConfidentialBalanceVersion];
638 if (spendingBefore.has_value() && spendingBefore != spendingAfter)
640 if (versionBefore == versionAfter)
657 for (
auto const& [
id, checks] :
changes_)
662 return checks.issuance;
672 if (checks.deletedWithEncrypted)
674 if ((*issuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
677 <<
"Invariant failed: MPToken deleted with encrypted fields while COA > 0";
683 if (checks.badConsistency)
685 JLOG(j.
fatal()) <<
"Invariant failed: MPToken encrypted field "
686 "existence inconsistency";
693 JLOG(j.
fatal()) <<
"Invariant failed: Confidential outstanding amount "
694 "exceeds total outstanding amount";
701 if (checks.changesConfidentialFields)
703 if (!issuance->isFlag(lsfMPTCanHoldConfidentialBalance))
705 JLOG(j.
fatal()) <<
"Invariant failed: MPToken has encrypted "
706 "fields but Issuance does not have "
707 "lsfMPTCanHoldConfidentialBalance set";
723 if (checks.coaDelta != 0)
725 if (checks.mptAmountDelta + checks.coaDelta != checks.outstandingDelta)
727 JLOG(j.
fatal()) <<
"Invariant failed: Token conservation "
735 kConfidentialMptTxTypes.end())
739 if (checks.mptAmountDelta != 0)
741 JLOG(j.
fatal()) <<
"Invariant failed: MPTAmount changed by confidential "
742 "transaction that should not modify this field."
751 if (checks.outstandingDelta != 0)
753 JLOG(j.
fatal()) <<
"Invariant failed: OutstandingAmount changed "
754 "by confidential transaction that should not "
761 if (checks.badVersion)
764 <<
"Invariant failed: MPToken sfConfidentialBalanceVersion not updated when "
765 "sfConfidentialBalanceSpending changed";
781 auto update = [&](
SLE const& sle,
bool isBefore) {
782 if (sle.
getType() == ltMPTOKEN)
784 auto const issuanceID = sle[sfMPTokenIssuanceID];
785 auto const account = sle[sfAccount];
786 auto const amount = sle[sfMPTAmount];
789 amount_[issuanceID][account].amtBefore = amount;
793 amount_[issuanceID][account].amtAfter = amount;
795 if (isDelete && isBefore)
803 update(*before,
true);
806 update(*
after,
false);
822 if (
isPseudoAccount(view, holder, {&sfVaultID, &sfLoanBrokerID, &sfAMMID}))
828 return !reqAuth || it->second;
840 auto const fix330Enabled = view.
rules().
enabled(fixCleanup3_3_0);
849 auto const isDEX = [&] {
850 if (txnType == ttPAYMENT)
854 auto const amount = tx[sfAmount];
855 return tx[~sfSendMax].value_or(amount).asset() != amount.asset();
857 return txnType == ttAMM_CREATE || txnType == ttAMM_DEPOSIT || txnType == ttOFFER_CREATE;
862 auto const invariantPasses = !view.
rules().
enabled(featureMPTokensV2);
864 for (
auto const& [mptID, values] :
amount_)
868 bool invalidTransfer =
false;
878 auto const waivesCanTransfer = txnType == ttAMM_WITHDRAW ||
879 (view.
rules().enabled(fixCleanup3_2_0) &&
880 (txnType == ttVAULT_WITHDRAW || txnType == ttLOAN_BROKER_COVER_WITHDRAW ||
881 txnType == ttLOAN_PAY));
882 auto const canTransfer = sleIssuance->isFlag(lsfMPTCanTransfer) || waivesCanTransfer;
883 auto const canTrade = sleIssuance->isFlag(lsfMPTCanTrade);
884 auto const reqAuth = sleIssuance->isFlag(lsfMPTRequireAuth);
886 for (
auto const& [account, value] : values)
891 if (value.amtAfter.has_value() && value.amtBefore.value_or(0) != *value.amtAfter)
893 if (!value.amtBefore.has_value() || *value.amtAfter > *value.amtBefore)
912 auto const legacyAccountFrozen = [&] {
915 bool const isReceiver =
916 !value.amtBefore.has_value() || *value.amtAfter > *value.amtBefore;
917 if (txnType == ttAMM_WITHDRAW && isReceiver)
921 bool const accountFrozen =
922 fix330Enabled ?
isFrozen(view, account, issue) : legacyAccountFrozen();
923 if (!invalidTransfer &&
924 (accountFrozen || !
isAuthorized(view, mptID, account, reqAuth)))
926 invalidTransfer =
true;
936 JLOG(j.
fatal()) <<
"Invariant failed: invalid MPToken transfer between holders";
937 return invariantPasses;
A generic endpoint for log messages.
AccountID const & getIssuer() const
virtual Rules const & rules() const =0
Returns the tx processing rules.
virtual SLE::const_pointer 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.
uint256 const & key() const
Returns the 'key' (or 'index') of this item.
LedgerEntryType getType() const
std::shared_ptr< STLedgerEntry const > const & const_ref
bool isFlag(std::uint32_t) const
bool isFieldPresent(SField const &field) const
TxType getTxnType() const
std::map< uint192, Changes > changes_
void visitEntry(bool isDelete, std::shared_ptr< SLE const > const &before, std::shared_ptr< SLE const > const &after)
Track confidential MPT balance, issuance, and version changes.
bool finalize(STTx const &tx, TER const result, XRPAmount const fee, ReadView const &view, beast::Journal const &j)
Verify confidential MPT accounting and encrypted-field invariants.
std::uint32_t mptokensCreated_
bool referenceHoldingSetOnCreate_
sfReferenceHolding is intended to be set exactly once at vault creation and immutable thereafter; tru...
std::uint32_t mptokensDeleted_
bool finalize(STTx const &tx, TER const result, XRPAmount const fee, ReadView const &view, beast::Journal const &j) const
Verify MPT issuance invariants after transaction application.
bool referenceHoldingMutated_
True when sfReferenceHolding was mutated on an existing MPTokenIssuance.
std::uint32_t mptIssuancesCreated_
void visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
Track MPT issuance and holding creations, deletions, and mutations.
std::vector< std::shared_ptr< SLE const > > deletedHoldings_
MPTokens and RippleStates deleted during apply.
std::uint32_t mptIssuancesDeleted_
hash_map< uint192, MPTData > data_
bool finalize(STTx const &tx, TER const result, XRPAmount const fee, ReadView const &view, beast::Journal const &j)
Verify public MPT payment accounting invariants.
void visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
Track MPT amount and outstanding amount changes.
hash_map< uint256, bool > deletedAuthorized_
bool isAuthorized(ReadView const &view, MPTID const &mptid, AccountID const &holder, bool requireAuth) const
Check whether a holder is authorized to send or receive an MPToken.
hash_map< uint192, hash_map< AccountID, Value > > amount_
void visitEntry(bool isDelete, std::shared_ptr< SLE const > const &before, std::shared_ptr< SLE const > const &after)
Track MPT balance changes and deleted authorization state.
bool finalize(STTx const &tx, TER const result, XRPAmount const fee, ReadView const &view, beast::Journal const &j)
Verify MPT transfer authorization invariants.
Keylet mptokenIssuance(std::uint32_t seq, AccountID const &issuer) noexcept
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Keylet account(AccountID const &id) noexcept
AccountID root.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
std::int64_t maxMPTAmount(SLE const &sleIssuance)
bool isFeatureEnabled(uint256 const &feature, bool resultIfNoRules)
Check whether a feature is enabled in the current ledger rules.
bool isIndividualFrozen(ReadView const &view, AccountID const &account, MPTIssue const &mptIssue)
TER canTransfer(ReadView const &view, MPTIssue const &mptIssue, AccountID const &from, AccountID const &to, WaiveMPTCanTransfer waive=WaiveMPTCanTransfer::No, std::uint8_t depth=0)
Check whether to may receive the given MPT from from.
std::string to_string(BaseUInt< Bits, Tag > const &a)
TER canTrade(ReadView const &view, Asset const &asset, std::uint8_t depth=0)
Check whether asset may be traded on the DEX.
bool isVaultPseudoAccountFrozen(ReadView const &view, AccountID const &account, MPTIssue const &mptShare, std::uint8_t depth)
bool isGlobalFrozen(ReadView const &view, AccountID const &issuer)
Check if the issuer has the global freeze flag set.
bool hasPrivilege(STTx const &tx, Privilege priv)
BaseUInt< 192 > MPTID
MPTID is a 192-bit value representing MPT Issuance ID, which is a concatenation of a 32-bit sequence ...
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
bool isFrozen(ReadView const &view, AccountID const &account, MPTIssue const &mptIssue, std::uint8_t depth=0)
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
bool isTesSuccess(TER x) noexcept
TERSubset< CanCvtToTER > TER
TER requireAuth(ReadView const &view, MPTIssue const &mptIssue, AccountID const &account, AuthType authType=AuthType::Legacy, std::uint8_t depth=0)
Check if the account lacks required authorization for MPT.
bool isPseudoAccount(SLE::const_pointer sleAcct, std::set< SField const * > const &pseudoFieldFilter={})
Returns true if and only if sleAcct is a pseudo-account or specific pseudo-accounts in pseudoFieldFil...
constexpr std::uint64_t kMaxMpTokenAmount
The maximum amount of MPTokenIssuance.
MPTID makeMptID(std::uint32_t sequence, AccountID const &account)