From 60a888619b57612013a5e3dfa69a087160110a04 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 12 Mar 2025 19:30:01 -0400 Subject: [PATCH] Add Loan Broker and Loan ledger objects: - Also add new SFields, Keylet functions, and an Invariant to verify no illegal field modification --- include/xrpl/protocol/Indexes.h | 18 ++++ include/xrpl/protocol/STLedgerEntry.h | 2 + .../xrpl/protocol/detail/ledger_entries.macro | 55 +++++++++++ include/xrpl/protocol/detail/sfields.macro | 26 +++++ src/libxrpl/protocol/Indexes.cpp | 14 +++ src/xrpld/app/tx/detail/InvariantCheck.cpp | 97 +++++++++++++++++++ src/xrpld/app/tx/detail/InvariantCheck.h | 32 +++++- 7 files changed, 243 insertions(+), 1 deletion(-) diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 43e64dc27a..4a92504060 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -339,6 +339,24 @@ vault(uint256 const& vaultKey) return {ltVAULT, vaultKey}; } +Keylet +loanbroker(AccountID const& owner, std::uint32_t seq) noexcept; + +inline Keylet +loanbroker(uint256 const& vaultKey) +{ + return {ltLOAN_BROKER, vaultKey}; +} + +Keylet +loan(AccountID const& owner, uint256 loanBrokerID, std::uint32_t seq) noexcept; + +inline Keylet +loan(uint256 const& vaultKey) +{ + return {ltLOAN, vaultKey}; +} + Keylet permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept; diff --git a/include/xrpl/protocol/STLedgerEntry.h b/include/xrpl/protocol/STLedgerEntry.h index 96b37af0b9..0348f29e8c 100644 --- a/include/xrpl/protocol/STLedgerEntry.h +++ b/include/xrpl/protocol/STLedgerEntry.h @@ -36,6 +36,8 @@ class STLedgerEntry final : public STObject, public CountedObject public: using pointer = std::shared_ptr; using ref = const std::shared_ptr&; + using const_pointer = std::shared_ptr; + using const_ref = const std::shared_ptr&; /** Create an empty object with the given key and type. */ explicit STLedgerEntry(Keylet const& k); diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 0e6c8c19ce..6ca6f5d446 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -167,6 +167,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({ {sfFirstNFTokenSequence, soeOPTIONAL}, {sfAMMID, soeOPTIONAL}, // pseudo-account designator {sfVaultID, soeOPTIONAL}, // pseudo-account designator + {sfLoanBrokerID, soeOPTIONAL}, // pseudo-account designator })) /** A ledger object which contains a list of object identifiers. @@ -485,6 +486,60 @@ LEDGER_ENTRY(ltVAULT, 0x0083, Vault, vault, ({ // no PermissionedDomainID ever (use MPTIssuance.sfDomainID) })) +/** A ledger object representing a loan broker + + \sa keylet::loanbroker + */ +LEDGER_ENTRY(ltLOAN_BROKER, 0x0084, LoanBroker, loan_broker, ({ + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfSequence, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfVaultNode, soeREQUIRED}, + {sfVaultID, soeREQUIRED}, + {sfAccount, soeREQUIRED}, + {sfOwner, soeREQUIRED}, + {sfData, soeDEFAULT}, + {sfManagementFeeRate, soeDEFAULT}, + {sfOwnerCount, soeREQUIRED}, + {sfDebtTotal, soeREQUIRED}, + {sfDebtMaximum, soeREQUIRED}, + {sfCoverAvailable, soeREQUIRED}, + {sfCoverRateMinimum, soeREQUIRED}, + {sfCoverRateLiquidation, soeREQUIRED}, +})) + +/** A ledger object representing a loan between a Borrower and a Loan Broker + + \sa keylet::loan + */ +LEDGER_ENTRY(ltLOAN, 0x0085, Loan, loan, ({ + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, + {sfSequence, soeREQUIRED}, + {sfOwnerNode, soeREQUIRED}, + {sfLoanBrokerNode, soeREQUIRED}, + {sfLoanBrokerID, soeREQUIRED}, + {sfBorrower, soeREQUIRED}, + {sfLoanOriginationFee, soeREQUIRED}, + {sfLoanServiceFee, soeREQUIRED}, + {sfLatePaymentFee, soeREQUIRED}, + {sfClosePaymentFee, soeREQUIRED}, + {sfOverpaymentFee, soeREQUIRED}, + {sfInterestRate, soeREQUIRED}, + {sfLateInterestRate, soeREQUIRED}, + {sfCloseInterestRate, soeREQUIRED}, + {sfOverpaymentInterestRate, soeREQUIRED}, + {sfStartDate, soeREQUIRED}, + {sfPaymentInterval, soeREQUIRED}, + {sfGracePeriod, soeREQUIRED}, + {sfPreviousPaymentDate, soeREQUIRED}, + {sfNextPaymentDueDate, soeREQUIRED}, + {sfPaymentRemaining, soeREQUIRED}, + {sfAssetAvailable, soeREQUIRED}, + {sfPrincipalOutstanding, soeREQUIRED}, +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index e8ebbafd33..caf69bdbc8 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -59,6 +59,13 @@ TYPED_SFIELD(sfHookEmitCount, UINT16, 18) TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19) TYPED_SFIELD(sfHookApiVersion, UINT16, 20) TYPED_SFIELD(sfLedgerFixType, UINT16, 21) +TYPED_SFIELD(sfManagementFeeRate, UINT16, 22) +TYPED_SFIELD(sfCoverRateMinimum, UINT16, 23) +TYPED_SFIELD(sfCoverRateLiquidation, UINT16, 24) +TYPED_SFIELD(sfInterestRate, UINT16, 25) +TYPED_SFIELD(sfLateInterestRate, UINT16, 26) +TYPED_SFIELD(sfCloseInterestRate, UINT16, 27) +TYPED_SFIELD(sfOverpaymentInterestRate, UINT16, 28) // 32-bit integers (common) TYPED_SFIELD(sfNetworkID, UINT32, 1) @@ -113,6 +120,12 @@ TYPED_SFIELD(sfEmitGeneration, UINT32, 46) TYPED_SFIELD(sfVoteWeight, UINT32, 48) TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50) TYPED_SFIELD(sfOracleDocumentID, UINT32, 51) +TYPED_SFIELD(sfStartDate, UINT32, 52) +TYPED_SFIELD(sfPaymentInterval, UINT32, 53) +TYPED_SFIELD(sfGracePeriod, UINT32, 54) +TYPED_SFIELD(sfPreviousPaymentDate, UINT32, 55) +TYPED_SFIELD(sfNextPaymentDueDate, UINT32, 56) +TYPED_SFIELD(sfPaymentRemaining, UINT32, 57) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -143,6 +156,8 @@ TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SFie TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default) TYPED_SFIELD(sfIssuerNode, UINT64, 27) TYPED_SFIELD(sfSubjectNode, UINT64, 28) +TYPED_SFIELD(sfVaultNode, UINT64, 29) +TYPED_SFIELD(sfLoanBrokerNode, UINT64, 30) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -194,6 +209,7 @@ TYPED_SFIELD(sfHookNamespace, UINT256, 32) TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfVaultID, UINT256, 35) +TYPED_SFIELD(sfLoanBrokerID, UINT256, 35) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -201,6 +217,15 @@ TYPED_SFIELD(sfAssetsAvailable, NUMBER, 2) TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3) TYPED_SFIELD(sfAssetsTotal, NUMBER, 4) TYPED_SFIELD(sfLossUnrealized, NUMBER, 5) +TYPED_SFIELD(sfDebtTotal, NUMBER, 6) +TYPED_SFIELD(sfDebtMaximum, NUMBER, 7) +TYPED_SFIELD(sfCoverAvailable, NUMBER, 8) +TYPED_SFIELD(sfLoanOriginationFee, NUMBER, 9) +TYPED_SFIELD(sfLoanServiceFee, NUMBER, 9) +TYPED_SFIELD(sfLatePaymentFee, NUMBER, 10) +TYPED_SFIELD(sfClosePaymentFee, NUMBER, 11) +TYPED_SFIELD(sfOverpaymentFee, NUMBER, 12) +TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) @@ -295,6 +320,7 @@ TYPED_SFIELD(sfAttestationRewardAccount, ACCOUNT, 21) TYPED_SFIELD(sfLockingChainDoor, ACCOUNT, 22) TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23) TYPED_SFIELD(sfSubject, ACCOUNT, 24) +TYPED_SFIELD(sfBorrower, ACCOUNT, 25) // vector of 256-bit TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::sMD_Never) diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 80971c689a..14c24f0c69 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -95,6 +95,8 @@ enum class LedgerNameSpace : std::uint16_t { CREDENTIAL = 'D', PERMISSIONED_DOMAIN = 'm', VAULT = 'V', + LOAN_BROKER = 'l', // lower-case L + LOAN = 'L', // No longer used or supported. Left here to reserve the space // to avoid accidental reuse. @@ -550,6 +552,18 @@ vault(AccountID const& owner, std::uint32_t seq) noexcept return vault(indexHash(LedgerNameSpace::VAULT, owner, seq)); } +Keylet +loanbroker(AccountID const& owner, std::uint32_t seq) noexcept +{ + return loanbroker(indexHash(LedgerNameSpace::LOAN_BROKER, owner, seq)); +} + +Keylet +loan(AccountID const& owner, uint256 loanBrokerID, std::uint32_t seq) noexcept +{ + return loan(indexHash(LedgerNameSpace::LOAN, loanBrokerID, seq)); +} + Keylet permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept { diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 8bca1670de..81106936c5 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -1578,4 +1578,101 @@ ValidPermissionedDomain::finalize( (sleStatus_[1] ? check(*sleStatus_[1], j) : true); } +//------------------------------------------------------------------------------ + +void +NoModifiedUnmodifiableFields::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete || !before) + // Creation and deletion are ignored + return; + + auto const type = after->getType(); + if (knownTypes_.contains(type)) + { + changedEntries_.emplace(before, after); + } +} + +bool +NoModifiedUnmodifiableFields::finalize( + STTx const& tx, + TER const, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + static auto const fieldChanged = [](auto const& before, + auto const& after, + auto const& field) { + return before->isFieldPresent(field) != after->isFieldPresent(field) || + before->at(field) != after->at(field); + }; + for (auto const& slePair : changedEntries_) + { + auto const& before = slePair.first; + auto const& after = slePair.second; + auto const type = after->getType(); + bool bad = false; + [[maybe_unused]] bool enforce = false; + switch (type) + { + case ltLOAN_BROKER: + /* + * We check this invariant regardless of lending protocol + * amendment status, allowing for detection and logging of + * potential issues even when the amendment is disabled. + */ + enforce = view.rules().enabled(featureLendingProtocol); + bad = fieldChanged(before, after, sfVaultID) || + fieldChanged(before, after, sfAccount) || + fieldChanged(before, after, sfOwner) || + fieldChanged(before, after, sfManagementFeeRate) || + fieldChanged(before, after, sfCoverRateMinimum) || + fieldChanged(before, after, sfCoverRateLiquidation); + break; + case ltLOAN: + /* + * We check this invariant regardless of lending protocol + * amendment status, allowing for detection and logging of + * potential issues even when the amendment is disabled. + */ + enforce = view.rules().enabled(featureLendingProtocol); + bad = fieldChanged(before, after, sfLoanBrokerID) || + fieldChanged(before, after, sfBorrower) || + fieldChanged(before, after, sfLoanOriginationFee) || + fieldChanged(before, after, sfLoanServiceFee) || + fieldChanged(before, after, sfLatePaymentFee) || + fieldChanged(before, after, sfClosePaymentFee) || + fieldChanged(before, after, sfOverpaymentFee) || + fieldChanged(before, after, sfInterestRate) || + fieldChanged(before, after, sfLateInterestRate) || + fieldChanged(before, after, sfCloseInterestRate) || + fieldChanged(before, after, sfOverpaymentInterestRate) || + fieldChanged(before, after, sfPaymentInterval); + break; + default: + UNREACHABLE( + "ripple::NoModifiedUnmodifiableFields::finalize : unknown " + "SLE type"); + } + XRPL_ASSERT( + !bad || enforce, + "ripple::NoModifiedUnmodifiableFields::finalize : no bad " + "changes or enforce invariant"); + if (bad) + { + JLOG(j.fatal()) + << "Invariant failed: changed an unchangable field for " + << tx.getTransactionID(); + if (enforce) + return false; + } + } + return true; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 6819780114..0ba5bdf747 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -618,6 +618,35 @@ public: beast::Journal const&); }; +/** + * @brief Invariants: Some fields are unmodifiable + * + * Check that any fields specified as unmodifiable are not modified when the + * object is modified. Creation and deletion are ignored. + * + */ +class NoModifiedUnmodifiableFields +{ + std::set const knownTypes_{ltLOAN_BROKER, ltLOAN}; + + std::set> changedEntries_; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + // additional invariant checks can be declared above and then added to this // tuple using InvariantChecks = std::tuple< @@ -637,7 +666,8 @@ using InvariantChecks = std::tuple< NFTokenCountTracking, ValidClawback, ValidMPTIssuance, - ValidPermissionedDomain>; + ValidPermissionedDomain, + NoModifiedUnmodifiableFields>; /** * @brief get a tuple of all invariant checks